Skip to content

Commit 363a923

Browse files
committed
fix: duplicate message teleportation
1 parent 60d4d66 commit 363a923

File tree

5 files changed

+145
-4
lines changed

5 files changed

+145
-4
lines changed

package/src/components/Message/Message.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
import { FileTypes } from '../../types/types';
6363
import {
6464
checkMessageEquality,
65+
generateRandomId,
6566
hasOnlyEmojis,
6667
isBlockedMessage,
6768
isBouncedMessage,
@@ -71,6 +72,8 @@ import type { Thumbnail } from '../Attachment/utils/buildGallery/types';
7172
import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
7273
import { BottomSheetModal } from '../UIComponents';
7374

75+
const createMessageOverlayId = () => `message-overlay-${generateRandomId()}`;
76+
7477
export type TouchableEmitter =
7578
| 'failed-image'
7679
| 'fileAttachment'
@@ -325,6 +328,15 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
325328
() => isMessageAIGenerated(message),
326329
[message, isMessageAIGenerated],
327330
);
331+
const messageOverlayIdRef = useRef(createMessageOverlayId());
332+
const previousMessageIdRef = useRef(message.id);
333+
334+
if (previousMessageIdRef.current !== message.id) {
335+
previousMessageIdRef.current = message.id;
336+
messageOverlayIdRef.current = createMessageOverlayId();
337+
}
338+
339+
const messageOverlayId = messageOverlayIdRef.current;
328340
const isMessageTypeDeleted = message.type === 'deleted';
329341
const { client } = chatContext;
330342

@@ -339,7 +351,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
339351
const layout = await measureInWindow(messageWrapperRef, insets);
340352
setRect(layout);
341353
setOverlayMessageH(layout);
342-
openOverlay(message.id);
354+
openOverlay(messageOverlayId);
343355
} catch (e) {
344356
console.error(e);
345357
}
@@ -685,7 +697,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
685697
};
686698

687699
const frozenMessage = useRef(message);
688-
const { active: overlayActive } = useIsOverlayActive(message.id);
700+
const { active: overlayActive } = useIsOverlayActive(messageOverlayId);
689701

690702
const messageHasOnlySingleAttachment =
691703
!message.text && !message.quoted_message && message.attachments?.length === 1;
@@ -709,6 +721,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
709721
lastGroupMessage: groupStyles?.[0] === 'single' || groupStyles?.[0] === 'bottom',
710722
members,
711723
message: overlayActive ? frozenMessage.current : message,
724+
messageOverlayId,
712725
messageContentOrder,
713726
messageHasOnlySingleAttachment,
714727
myMessageTheme: messagesContext.myMessageTheme,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React, { PropsWithChildren } from 'react';
2+
3+
import { act, cleanup, renderHook } from '@testing-library/react-native';
4+
5+
import {
6+
MessageContextValue,
7+
MessageProvider,
8+
} from '../../../../contexts/messageContext/MessageContext';
9+
import { generateMessage } from '../../../../mock-builders/generator/message';
10+
import { finalizeCloseOverlay, openOverlay, overlayStore } from '../../../../state-store';
11+
12+
import { useShouldUseOverlayStyles } from '../useShouldUseOverlayStyles';
13+
14+
const createMessageContextValue = (overrides: Partial<MessageContextValue>): MessageContextValue =>
15+
({
16+
actionsEnabled: false,
17+
alignment: 'left',
18+
channel: {} as MessageContextValue['channel'],
19+
deliveredToCount: 0,
20+
dismissOverlay: jest.fn(),
21+
files: [],
22+
groupStyles: [],
23+
handleAction: jest.fn(),
24+
handleToggleReaction: jest.fn(),
25+
hasReactions: false,
26+
images: [],
27+
isMessageAIGenerated: jest.fn(),
28+
isMyMessage: false,
29+
lastGroupMessage: false,
30+
members: {},
31+
message: generateMessage({ id: 'shared-message-id' }),
32+
messageContentOrder: [],
33+
messageHasOnlySingleAttachment: false,
34+
messageOverlayId: 'message-overlay-default',
35+
onLongPress: jest.fn(),
36+
onlyEmojis: false,
37+
onOpenThread: jest.fn(),
38+
onPress: jest.fn(),
39+
onPressIn: null,
40+
otherAttachments: [],
41+
reactions: [],
42+
readBy: false,
43+
setQuotedMessage: jest.fn(),
44+
showAvatar: false,
45+
showMessageOverlay: jest.fn(),
46+
showReactionsOverlay: jest.fn(),
47+
showMessageStatus: false,
48+
threadList: false,
49+
videos: [],
50+
...overrides,
51+
}) as MessageContextValue;
52+
53+
const createWrapper = (value: MessageContextValue) => {
54+
const Wrapper = ({ children }: PropsWithChildren) => (
55+
<MessageProvider value={value}>{children}</MessageProvider>
56+
);
57+
58+
return Wrapper;
59+
};
60+
61+
describe('useShouldUseOverlayStyles', () => {
62+
beforeEach(() => {
63+
act(() => {
64+
finalizeCloseOverlay();
65+
overlayStore.next({
66+
closing: false,
67+
closingPortalHostBlacklist: [],
68+
id: undefined,
69+
});
70+
});
71+
});
72+
73+
afterEach(() => {
74+
cleanup();
75+
76+
act(() => {
77+
finalizeCloseOverlay();
78+
overlayStore.next({
79+
closing: false,
80+
closingPortalHostBlacklist: [],
81+
id: undefined,
82+
});
83+
});
84+
});
85+
86+
it('tracks overlay activity by messageOverlayId instead of message.id', () => {
87+
const sharedMessage = generateMessage({ id: 'same-message-id' });
88+
89+
const first = renderHook(() => useShouldUseOverlayStyles(), {
90+
wrapper: createWrapper(
91+
createMessageContextValue({
92+
message: sharedMessage,
93+
messageOverlayId: 'message-overlay-first',
94+
}),
95+
),
96+
});
97+
98+
const second = renderHook(() => useShouldUseOverlayStyles(), {
99+
wrapper: createWrapper(
100+
createMessageContextValue({
101+
message: sharedMessage,
102+
messageOverlayId: 'message-overlay-second',
103+
}),
104+
),
105+
});
106+
107+
expect(first.result.current).toBe(false);
108+
expect(second.result.current).toBe(false);
109+
110+
act(() => {
111+
openOverlay('message-overlay-first');
112+
});
113+
114+
expect(first.result.current).toBe(true);
115+
expect(second.result.current).toBe(false);
116+
117+
first.unmount();
118+
second.unmount();
119+
});
120+
});

package/src/components/Message/hooks/useCreateMessageContext.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const useCreateMessageContext = ({
3434
lastGroupMessage,
3535
members,
3636
message,
37+
messageOverlayId,
3738
messageContentOrder,
3839
myMessageTheme,
3940
onLongPress,
@@ -86,6 +87,7 @@ export const useCreateMessageContext = ({
8687
lastGroupMessage,
8788
members,
8889
message,
90+
messageOverlayId,
8991
messageContentOrder,
9092
myMessageTheme,
9193
onLongPress,
@@ -117,6 +119,7 @@ export const useCreateMessageContext = ({
117119
lastGroupMessage,
118120
membersValue,
119121
myMessageThemeString,
122+
messageOverlayId,
120123
reactionsValue,
121124
stringifiedMessage,
122125
stringifiedQuotedMessage,

package/src/components/Message/hooks/useShouldUseOverlayStyles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { useMessageContext } from '../../../contexts';
22
import { useIsOverlayActive } from '../../../state-store';
33

44
export const useShouldUseOverlayStyles = () => {
5-
const { message } = useMessageContext();
6-
const { active, closing } = useIsOverlayActive(message?.id);
5+
const { messageOverlayId } = useMessageContext();
6+
const { active, closing } = useIsOverlayActive(messageOverlayId);
77

88
return active && !closing;
99
};

package/src/contexts/messageContext/MessageContext.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export type MessageContextValue = {
5656
lastGroupMessage: boolean;
5757
/** Current [message object](https://getstream.io/chat/docs/#message_format) */
5858
message: LocalMessage;
59+
/**
60+
* Stable UI-instance identifier for the rendered message.
61+
* Used for overlay state so two rendered instances of the same message do not collide.
62+
*/
63+
messageOverlayId: string;
5964
/** Order to render the message content */
6065
messageContentOrder: MessageContentType[];
6166
/**

0 commit comments

Comments
 (0)