Skip to content

Commit 84c07c7

Browse files
authored
fix: design ux improvements (#3596)
## 🎯 Goal Fix a set of design regressions across poll headers, audio attachments, message metadata, composer/header borders, bottom navigation and stacked avatars so they match the current design token guidance and visual specs. ## 🛠 Implementation details - Updated Poll Results and Poll Creation headers to use a secondary ghost icon only back button with the new `ArrowLeft` icon - Added `ArrowLeft`, derived from the existing `ArrowUp` path with SVG rotation, including RTL aware direction - Fixed single audio attachments in quoted replies without captions so they render directly on the message bubble surface instead of adding the darker audio fill - Preserved the filled audio background when the reply has a caption or multiple attachments - Updated composer/header separator borders to use `semantics.borderCoreSubtle` - Updated sent message footer spacing so the read receipt and timestamp use `spacingXxs` - Updated SampleApp bottom tabs to use `semantics.backgroundCoreElevation1` and `semantics.borderCoreSubtle` - Added visible semantic borders to `UserAvatarStack` avatars using `semantics.borderCoreInverse` - Added `theme.avatarStack.userAvatarWrapper` so the new avatar stack style remains theme compliant ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## 🧪 Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## ☑️ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
1 parent 9fb334c commit 84c07c7

17 files changed

Lines changed: 421 additions & 31 deletions

File tree

examples/SampleApp/src/components/BottomTabs.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const styles = StyleSheet.create({
2727
paddingVertical: 8,
2828
},
2929
tabListContainer: {
30-
borderTopColor: 'rgba(0, 0, 0, 0.0677)',
3130
borderTopWidth: 1,
3231
flexDirection: 'row',
3332
},
@@ -128,16 +127,18 @@ const Tab = (props: TabProps) => {
128127

129128
export const BottomTabs: React.FC<BottomTabBarProps> = (props) => {
130129
const { navigation, state } = props;
131-
useTheme();
132-
const { white } = useLegacyColors();
130+
const {
131+
theme: { semantics },
132+
} = useTheme();
133133
const { bottom } = useSafeAreaInsets();
134134

135135
return (
136136
<View
137137
style={[
138138
styles.tabListContainer,
139139
{
140-
backgroundColor: white,
140+
backgroundColor: semantics.backgroundCoreElevation1,
141+
borderTopColor: semantics.borderCoreSubtle,
141142
paddingBottom: bottom,
142143
},
143144
]}

package/src/components/Attachment/Attachment.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,14 @@ const useAudioAttachmentStyles = () => {
219219
const {
220220
theme: { semantics },
221221
} = useTheme();
222-
const { isMyMessage, messageHasOnlySingleAttachment } = useMessageContext();
223-
224-
const showBackgroundTransparent = messageHasOnlySingleAttachment;
222+
const { isMyMessage, message, messageHasOnlySingleAttachment } = useMessageContext();
223+
224+
const messageHasSingleAttachment = message.attachments?.length === 1;
225+
const messageHasCaption = !!message.text?.trim();
226+
const messageIsQuotedReply = !!(message.quoted_message || message.quoted_message_id);
227+
const showBackgroundTransparent =
228+
messageHasOnlySingleAttachment ||
229+
(messageIsQuotedReply && messageHasSingleAttachment && !messageHasCaption);
225230

226231
return useMemo(() => {
227232
return StyleSheet.create({

package/src/components/Attachment/__tests__/Attachment.test.tsx

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import type { MessageContextValue } from '../../../contexts/messageContext/Messa
1111
import { MessageProvider } from '../../../contexts/messageContext/MessageContext';
1212
import type { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext';
1313
import { MessagesProvider } from '../../../contexts/messagesContext/MessagesContext';
14-
import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
14+
import { mergeThemes, ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
1515
import {
1616
generateAudioAttachment,
1717
generateFileAttachment,
1818
generateImageAttachment,
1919
generateVideoAttachment,
2020
} from '../../../mock-builders/generator/attachment';
2121
import { generateMessage } from '../../../mock-builders/generator/message';
22+
import { FileTypes } from '../../../types/types';
2223

2324
import { ImageLoadingFailedIndicator } from '../../Attachment/ImageLoadingFailedIndicator';
2425
import { ImageLoadingIndicator } from '../../Attachment/ImageLoadingIndicator';
@@ -48,8 +49,11 @@ jest.mock('../../../hooks/usePendingAttachmentUpload', () => ({
4849
})),
4950
}));
5051

51-
const getAttachmentComponent = (props: ComponentProps<typeof Attachment>) => {
52-
const message = generateMessage();
52+
const getAttachmentComponent = (
53+
props: ComponentProps<typeof Attachment>,
54+
messageContextValue: Partial<MessageContextValue> = {},
55+
) => {
56+
const message = messageContextValue.message ?? generateMessage();
5357
return (
5458
<ThemeProvider>
5559
<AudioPlayerProvider value={{ allowConcurrentAudioPlayback: false }}>
@@ -63,7 +67,9 @@ const getAttachmentComponent = (props: ComponentProps<typeof Attachment>) => {
6367
} as unknown as MessagesContextValue
6468
}
6569
>
66-
<MessageProvider value={{ message } as unknown as MessageContextValue}>
70+
<MessageProvider
71+
value={{ message, ...messageContextValue } as unknown as MessageContextValue}
72+
>
6773
<Attachment {...props} />
6874
</MessageProvider>
6975
</MessagesProvider>
@@ -79,6 +85,8 @@ const getWaveformBarCount = (root: ReactTestInstance) =>
7985
}).length;
8086

8187
describe('Attachment', () => {
88+
const lightTheme = mergeThemes({ scheme: 'light' });
89+
8290
it('should render File component for "audio" type attachment', async () => {
8391
const attachment = generateAudioAttachment();
8492
const { getByTestId } = render(getAttachmentComponent({ attachment }));
@@ -122,6 +130,94 @@ describe('Attachment', () => {
122130
isSoundPackageAvailable.mockReturnValue(false);
123131
});
124132

133+
it('uses a transparent audio player background for quoted replies without captions', async () => {
134+
const { isSoundPackageAvailable } = require('../../../native');
135+
isSoundPackageAvailable.mockReturnValue(true);
136+
const attachment = generateAudioAttachment({
137+
duration: 10,
138+
waveform_data: [0.2, 0.6, 0.4],
139+
});
140+
const quotedMessage = generateMessage();
141+
const message = generateMessage({
142+
attachments: [attachment],
143+
quoted_message: quotedMessage,
144+
quoted_message_id: quotedMessage.id,
145+
text: '',
146+
});
147+
148+
const { getByLabelText } = render(
149+
getAttachmentComponent(
150+
{ attachment },
151+
{ isMyMessage: false, message, messageHasOnlySingleAttachment: false },
152+
),
153+
);
154+
155+
await waitFor(() => {
156+
const style = StyleSheet.flatten(getByLabelText('audio-attachment-preview').props.style);
157+
expect(style.backgroundColor).toBe('transparent');
158+
});
159+
isSoundPackageAvailable.mockReturnValue(false);
160+
});
161+
162+
it('keeps the audio player background for quoted replies with captions', async () => {
163+
const { isSoundPackageAvailable } = require('../../../native');
164+
isSoundPackageAvailable.mockReturnValue(true);
165+
const attachment = generateAudioAttachment({
166+
duration: 10,
167+
type: FileTypes.VoiceRecording,
168+
waveform_data: [0.2, 0.6, 0.4],
169+
});
170+
const quotedMessage = generateMessage();
171+
const message = generateMessage({
172+
attachments: [attachment],
173+
quoted_message: quotedMessage,
174+
quoted_message_id: quotedMessage.id,
175+
text: 'caption',
176+
});
177+
178+
const { getByLabelText } = render(
179+
getAttachmentComponent(
180+
{ attachment },
181+
{ isMyMessage: false, message, messageHasOnlySingleAttachment: false },
182+
),
183+
);
184+
185+
await waitFor(() => {
186+
const style = StyleSheet.flatten(getByLabelText('audio-attachment-preview').props.style);
187+
expect(style.backgroundColor).toBe(lightTheme.semantics.chatBgAttachmentIncoming);
188+
});
189+
isSoundPackageAvailable.mockReturnValue(false);
190+
});
191+
192+
it('keeps the audio player background for quoted replies with multiple attachments and no captions', async () => {
193+
const { isSoundPackageAvailable } = require('../../../native');
194+
isSoundPackageAvailable.mockReturnValue(true);
195+
const attachment = generateAudioAttachment({
196+
duration: 10,
197+
waveform_data: [0.2, 0.6, 0.4],
198+
});
199+
const quotedMessage = generateMessage();
200+
const message = generateMessage({
201+
attachments: [attachment, generateAudioAttachment()],
202+
quoted_message: quotedMessage,
203+
quoted_message_id: quotedMessage.id,
204+
text: '',
205+
});
206+
207+
const { getByLabelText } = render(
208+
getAttachmentComponent(
209+
{ attachment },
210+
{ isMyMessage: false, message, messageHasOnlySingleAttachment: false },
211+
),
212+
);
213+
214+
await waitFor(() => {
215+
const style = StyleSheet.flatten(getByLabelText('audio-attachment-preview').props.style);
216+
expect(style.backgroundColor).toBe(lightTheme.semantics.chatBgAttachmentIncoming);
217+
});
218+
isSoundPackageAvailable.mockReturnValue(false);
219+
});
220+
125221
it('should render UrlPreview component if attachment has title_link or og_scrape_url', async () => {
126222
const attachment = generateImageAttachment({
127223
og_scrape_url: uuidv4(),

package/src/components/ImageGallery/components/ImageGalleryFooter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ const useStyles = () => {
200200
container: {
201201
backgroundColor: semantics.backgroundCoreElevation1,
202202
borderTopWidth: 1,
203-
borderTopColor: semantics.borderCoreDefault,
203+
borderTopColor: semantics.borderCoreSubtle,
204204
...footer.container,
205205
},
206206
centerContainer: {

package/src/components/ImageGallery/components/ImageGalleryHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ const useStyles = () => {
131131
flexDirection: 'row',
132132
justifyContent: 'space-between',
133133
borderBottomWidth: 1,
134-
borderBottomColor: semantics.borderCoreDefault,
134+
borderBottomColor: semantics.borderCoreSubtle,
135135
...header.innerContainer,
136136
},
137137
rightContainer: {

package/src/components/Message/MessageItemView/MessageFooter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ const useStyles = () => {
198198
flexDirection: 'row',
199199
justifyContent: 'center',
200200
paddingVertical: primitives.spacingXxs,
201-
gap: primitives.spacingXs,
201+
gap: primitives.spacingXxs,
202202
},
203203
name: {
204204
flexShrink: 1,

package/src/components/MessageInput/MessageComposer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ const MessageComposerWithContext = (props: MessageComposerPropsWithContext) => {
376376
{
377377
borderTopWidth: 1,
378378
backgroundColor: semantics.backgroundCoreElevation1,
379-
borderColor: semantics.borderCoreDefault,
379+
borderTopColor: semantics.borderCoreSubtle,
380380
// paddingBottom: BOTTOM_OFFSET,
381381
paddingBottom:
382382
selectedPicker && !isKeyboardVisible

package/src/components/MessageInput/SendMessageDisallowedIndicator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const useStyles = () => {
3030
return StyleSheet.create({
3131
container: {
3232
backgroundColor: semantics.backgroundCoreApp,
33-
borderTopColor: semantics.borderCoreDefault,
33+
borderTopColor: semantics.borderCoreSubtle,
3434
height: 48,
3535
...container,
3636
},

package/src/components/MessageList/__tests__/__snapshots__/TypingIndicator.test.tsx.snap

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ exports[`TypingIndicator should match typing indicator snapshot 1`] = `
8181
"borderColor": "rgba(26, 27, 37, 0.1)",
8282
"borderWidth": 1,
8383
},
84-
undefined,
84+
{
85+
"borderColor": "#ffffff",
86+
"borderRadius": 9999,
87+
"borderWidth": 2,
88+
},
8589
]
8690
}
8791
testID="avatar-image"
@@ -143,7 +147,11 @@ exports[`TypingIndicator should match typing indicator snapshot 1`] = `
143147
"borderColor": "rgba(26, 27, 37, 0.1)",
144148
"borderWidth": 1,
145149
},
146-
undefined,
150+
{
151+
"borderColor": "#ffffff",
152+
"borderRadius": 9999,
153+
"borderWidth": 2,
154+
},
147155
]
148156
}
149157
testID="avatar-image"

package/src/components/Poll/components/CreatePollHeader.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { StyleSheet, Text, View } from 'react-native';
44
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
55
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
66
import { Check, IconProps } from '../../../icons';
7-
import { Cross } from '../../../icons/xmark-1';
7+
import { ArrowLeft } from '../../../icons/arrow-left';
88
import { primitives } from '../../../theme';
99
import { Button } from '../../ui';
1010
import { useCanCreatePoll } from '../hooks/useCanCreatePoll';
@@ -54,8 +54,9 @@ export const CreatePollHeader = ({
5454
accessibilityLabelKey='a11y/Close poll creation'
5555
variant='secondary'
5656
onPress={onBackPressHandler}
57-
type='solid'
58-
LeadingIcon={Cross}
57+
type='ghost'
58+
size='md'
59+
LeadingIcon={ArrowLeft}
5960
iconOnly
6061
/>
6162

0 commit comments

Comments
 (0)