Skip to content

Commit dbecc8e

Browse files
authored
fix: allow moving text inside message content (#3723)
## 🎯 Goal Even though we had for reordering message content (text, gallery, poll etc) text had a fixed position. This PR enables updating text's position too. https://github.com/GetStream/docs-content/pull/1417/changes ## 🛠 Implementation details ## 🎨 UI Changes No UI changes, the following URL contains before-after shots verifying no change: https://claude.ai/code/artifact/c071bb00-30f3-48ba-ac12-708204549007?via=auto_preview (if the link doesn't work, try the attached screenshots) [message-content-ordering-report.zip](https://github.com/user-attachments/files/29860209/message-content-ordering-report.zip) ## 🧪 Testing Tested manually on android and ios too ## ☑️ 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 eb94ff8 commit dbecc8e

6 files changed

Lines changed: 263 additions & 123 deletions

File tree

package/src/components/Channel/Channel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,8 +468,8 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
468468
'poll',
469469
'ai_text',
470470
'attachments',
471-
'text',
472471
'location',
472+
'text',
473473
],
474474
messageOverlayTargetId,
475475
messageInputFloating = false,

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,25 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
220220

221221
const { setNativeScrollability } = useMessageListItemContext();
222222
const hasContentSideViews = !!(MessageContentLeadingView || MessageContentTrailingView);
223+
const gap = primitives.spacingXs;
224+
225+
const messageTextContainerStyles = useMemo(() => {
226+
return {
227+
textContainer: {
228+
// Cancel the container's 8px inter-item gap so only the caption's own
229+
// paragraph marginTop shows. Skip for the first item: there is no gap to
230+
// cancel
231+
marginTop: -gap,
232+
},
233+
};
234+
}, [gap]);
235+
223236
const contentBody = (
224237
<>
225238
<View
226239
style={[
227240
{
228-
gap: primitives.spacingXs,
241+
gap,
229242
paddingTop: hidePaddingTop ? 0 : primitives.spacingXs,
230243
paddingHorizontal: hidePaddingHorizontal ? 0 : primitives.spacingXs,
231244
paddingBottom: hidePaddingBottom ? 0 : primitives.spacingXs,
@@ -313,14 +326,21 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
313326
key={`ai_message_text_container_${messageContentOrderIndex}`}
314327
/>
315328
) : null;
329+
case 'text': {
330+
const suppressed =
331+
(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated;
332+
return suppressed ? null : (
333+
<MessageTextContainer
334+
key={`message_text_container_${messageContentOrderIndex}`}
335+
styles={messageContentOrderIndex === 0 ? undefined : messageTextContainerStyles}
336+
/>
337+
);
338+
}
316339
default:
317340
return null;
318341
}
319342
})}
320343
</View>
321-
{(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated ? null : (
322-
<MessageTextContainer />
323-
)}
324344
</>
325345
);
326346
const a11yPressableLabel = useMemo(() => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage';
2121
import { primitives } from '../../../theme';
2222

2323
const styles = StyleSheet.create({
24-
textContainer: { maxWidth: 256, paddingHorizontal: primitives.spacingSm },
24+
textContainer: { maxWidth: 256, paddingHorizontal: primitives.spacingXxs },
2525
});
2626

2727
export type MessageTextProps = MessageTextContainerProps & {

package/src/components/Message/MessageItemView/__tests__/MessageContent.test.tsx

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,122 @@ describe('MessageContent', () => {
445445
});
446446
});
447447

448+
// Collects the given testIDs in the order they appear in the rendered tree.
449+
const collectOrderedTestIDs = (
450+
root: ReturnType<typeof screen.getByTestId>,
451+
targets: string[],
452+
) => {
453+
const found: string[] = [];
454+
const walk = (node: typeof root | string | null) => {
455+
if (!node || typeof node === 'string') {
456+
return;
457+
}
458+
const testID = node.props?.testID;
459+
if (testID && targets.includes(testID) && !found.includes(testID)) {
460+
found.push(testID);
461+
}
462+
(node.children ?? []).forEach((child) => walk(child as typeof root | string));
463+
};
464+
walk(root);
465+
return found;
466+
};
467+
468+
it('renders text after attachments with the default messageContentOrder', async () => {
469+
const user = generateUser();
470+
const message = generateMessage({
471+
attachments: [
472+
{ image_url: 'https://i.imgur.com/SLx06PP.png', type: 'image' },
473+
{ image_url: 'https://i.imgur.com/iNaC3K7.jpg', type: 'image' },
474+
],
475+
text: 'a caption',
476+
user,
477+
});
478+
479+
renderMessage({ message });
480+
481+
await waitFor(() => {
482+
expect(screen.getByTestId('gallery-container')).toBeTruthy();
483+
expect(screen.getByTestId('message-text-container')).toBeTruthy();
484+
});
485+
486+
expect(
487+
collectOrderedTestIDs(screen.getByTestId('message-content-wrapper'), [
488+
'gallery-container',
489+
'message-text-container',
490+
]),
491+
).toEqual(['gallery-container', 'message-text-container']);
492+
});
493+
494+
it('renders text before attachments when messageContentOrder puts text first', async () => {
495+
const user = generateUser();
496+
const message = generateMessage({
497+
attachments: [
498+
{ image_url: 'https://i.imgur.com/SLx06PP.png', type: 'image' },
499+
{ image_url: 'https://i.imgur.com/iNaC3K7.jpg', type: 'image' },
500+
],
501+
text: 'a caption',
502+
user,
503+
});
504+
505+
render(
506+
<Chat client={chatClient}>
507+
<Channel
508+
channel={channel}
509+
messageContentOrder={[
510+
'text',
511+
'quoted_reply',
512+
'gallery',
513+
'files',
514+
'poll',
515+
'ai_text',
516+
'attachments',
517+
'location',
518+
]}
519+
>
520+
<Message groupStyles={['bottom']} message={message} />
521+
</Channel>
522+
</Chat>,
523+
);
524+
525+
await waitFor(() => {
526+
expect(screen.getByTestId('gallery-container')).toBeTruthy();
527+
expect(screen.getByTestId('message-text-container')).toBeTruthy();
528+
});
529+
530+
expect(
531+
collectOrderedTestIDs(screen.getByTestId('message-content-wrapper'), [
532+
'gallery-container',
533+
'message-text-container',
534+
]),
535+
).toEqual(['message-text-container', 'gallery-container']);
536+
});
537+
538+
it('suppresses the text container for an ephemeral giphy preview with actions', async () => {
539+
const user = generateUser();
540+
const message = generateMessage({
541+
attachments: [
542+
{
543+
...generateGiphyAttachment(),
544+
actions: [
545+
generateAttachmentAction(),
546+
generateAttachmentAction(),
547+
generateAttachmentAction(),
548+
],
549+
},
550+
],
551+
text: '/giphy hello',
552+
user,
553+
});
554+
555+
renderMessage({ message });
556+
557+
await waitFor(() => {
558+
expect(screen.getByTestId('giphy-action-attachment')).toBeTruthy();
559+
});
560+
561+
expect(screen.queryByTestId('message-text-container')).toBeFalsy();
562+
});
563+
448564
it('renders the ReactionList when the message has reactions', async () => {
449565
const user = generateUser();
450566
const reaction = generateReaction();

package/src/components/Message/MessageItemView/__tests__/__snapshots__/MessageTextContainer.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ exports[`MessageTextContainer should render message text container 1`] = `
66
[
77
{
88
"maxWidth": 256,
9-
"paddingHorizontal": 12,
9+
"paddingHorizontal": 4,
1010
},
1111
{},
1212
undefined,

0 commit comments

Comments
 (0)