Skip to content

Commit 4a6efdc

Browse files
committed
refactor: lift up the flatListRef to the provider and created MessageList context
1 parent 2523ddd commit 4a6efdc

File tree

5 files changed

+187
-111
lines changed

5 files changed

+187
-111
lines changed

packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx

Lines changed: 28 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import React, { useContext, useEffect, useRef } from 'react';
2-
import type { FlatList } from 'react-native';
1+
import React, { useContext, useEffect } from 'react';
32

43
import { useChannelHandler } from '@sendbird/uikit-chat-hooks';
54
import { useToast } from '@sendbird/uikit-react-native-foundation';
@@ -10,72 +9,40 @@ import ChannelMessageList from '../../../components/ChannelMessageList';
109
import { MESSAGE_FOCUS_ANIMATION_DELAY, MESSAGE_SEARCH_SAFE_SCROLL_DELAY } from '../../../constants';
1110
import { useLocalization, useSendbirdChat } from '../../../hooks/useContext';
1211
import { GroupChannelContexts } from '../module/moduleContext';
13-
import type { GroupChannelProps, GroupChannelScrollToMessageFunc } from '../types';
12+
import type { GroupChannelProps } from '../types';
1413

1514
const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
1615
const toast = useToast();
1716
const { STRINGS } = useLocalization();
1817
const { sdk } = useSendbirdChat();
19-
const { setMessageToEdit, setMessageToReply, __internalSetScrollToMessageFunc } = useContext(
20-
GroupChannelContexts.Fragment,
21-
);
18+
const { setMessageToEdit, setMessageToReply } = useContext(GroupChannelContexts.Fragment);
2219
const { subscribe } = useContext(GroupChannelContexts.PubSub);
20+
const { flatListRef, lazyScrollToBottom, lazyScrollToIndex } = useContext(GroupChannelContexts.MessageList);
2321

2422
const id = useUniqHandlerId('GroupChannelMessageList');
25-
const ref = useRef<FlatList<SendbirdMessage>>(null);
2623
const isFirstMount = useIsFirstMount();
2724

28-
// FIXME: Workaround, should run after data has been applied to UI.
29-
const lazyScrollToBottom = (animated = false, timeout = 0) => {
30-
setTimeout(() => {
31-
ref.current?.scrollToOffset({ offset: 0, animated });
32-
}, timeout);
33-
};
34-
35-
// FIXME: Workaround, should run after data has been applied to UI.
36-
const lazyScrollToIndex = (index = 0, animated = false, timeout = 0, viewPosition = 0.5) => {
37-
setTimeout(() => {
38-
ref.current?.scrollToIndex({ index, animated, viewPosition });
39-
}, timeout);
40-
};
41-
42-
const scrollToMessage = useFreshCallback<GroupChannelScrollToMessageFunc>((messageId, options) => {
43-
const foundMessageIndex = props.messages.findIndex((it) => it.messageId === messageId);
44-
const isIncludedInList = foundMessageIndex > -1;
25+
const scrollToMessageWithCreatedAt = useFreshCallback(
26+
(createdAt: number, focusAnimated: boolean, timeout: number): boolean => {
27+
const foundMessageIndex = props.messages.findIndex((it) => it.createdAt === createdAt);
28+
const isIncludedInList = foundMessageIndex > -1;
4529

46-
if (isIncludedInList) {
47-
if (options?.focusAnimated) {
48-
setTimeout(
49-
() => props.onUpdateSearchItem({ startingPoint: props.messages[foundMessageIndex].createdAt }),
50-
MESSAGE_FOCUS_ANIMATION_DELAY,
51-
);
52-
}
53-
lazyScrollToIndex(foundMessageIndex, true, 0, options?.viewPosition);
54-
return true;
55-
} else {
56-
return false;
57-
}
58-
});
59-
60-
const scrollToMessageWithCreatedAt = useFreshCallback((createdAt: number, focusAnimated = false): boolean => {
61-
const foundMessageIndex = props.messages.findIndex((it) => it.createdAt === createdAt);
62-
const isIncludedInList = foundMessageIndex > -1;
63-
64-
if (isIncludedInList) {
65-
if (focusAnimated) {
66-
setTimeout(() => props.onUpdateSearchItem({ startingPoint: createdAt }), MESSAGE_FOCUS_ANIMATION_DELAY);
67-
}
68-
lazyScrollToIndex(foundMessageIndex, true, isFirstMount ? MESSAGE_SEARCH_SAFE_SCROLL_DELAY : 0);
69-
} else {
70-
if (props.channel.messageOffsetTimestamp <= createdAt) {
71-
if (focusAnimated) props.onUpdateSearchItem({ startingPoint: createdAt });
72-
props.onResetMessageListWithStartingPoint(createdAt);
30+
if (isIncludedInList) {
31+
if (focusAnimated) {
32+
setTimeout(() => props.onUpdateSearchItem({ startingPoint: createdAt }), MESSAGE_FOCUS_ANIMATION_DELAY);
33+
}
34+
lazyScrollToIndex({ index: foundMessageIndex, animated: true, timeout });
7335
} else {
74-
return false;
36+
if (props.channel.messageOffsetTimestamp <= createdAt) {
37+
if (focusAnimated) props.onUpdateSearchItem({ startingPoint: createdAt });
38+
props.onResetMessageListWithStartingPoint(createdAt);
39+
} else {
40+
return false;
41+
}
7542
}
76-
}
77-
return true;
78-
});
43+
return true;
44+
},
45+
);
7946

8047
const scrollToBottom = useFreshCallback((animated = false) => {
8148
if (props.hasNext()) {
@@ -84,10 +51,10 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
8451

8552
props.onResetMessageList(() => {
8653
props.onScrolledAwayFromBottom(false);
87-
lazyScrollToBottom(animated);
54+
lazyScrollToBottom({ animated });
8855
});
8956
} else {
90-
lazyScrollToBottom(animated);
57+
lazyScrollToBottom({ animated });
9158
}
9259
});
9360

@@ -98,7 +65,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
9865
const isRecentMessage = recentMessage && recentMessage.messageId === event.messageId;
9966
const scrollReachedBottomAndCanScroll = !props.scrolledAwayFromBottom && !props.hasNext();
10067
if (isRecentMessage && scrollReachedBottomAndCanScroll) {
101-
lazyScrollToBottom(true, 250);
68+
lazyScrollToBottom({ animated: true, timeout: 250 });
10269
}
10370
},
10471
});
@@ -126,23 +93,19 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
12693
// - Search screen + searchItem > mount message list
12794
// - Reset message list + searchItem > re-mount message list
12895
if (isFirstMount && props.searchItem) {
129-
scrollToMessageWithCreatedAt(props.searchItem.startingPoint);
96+
scrollToMessageWithCreatedAt(props.searchItem.startingPoint, false, MESSAGE_SEARCH_SAFE_SCROLL_DELAY);
13097
}
13198
}, [isFirstMount]);
13299

133-
useEffect(() => {
134-
__internalSetScrollToMessageFunc(() => scrollToMessage);
135-
}, []);
136-
137100
const onPressParentMessage = useFreshCallback((message: SendbirdMessage) => {
138-
const canScrollToParent = scrollToMessageWithCreatedAt(message.createdAt, true);
101+
const canScrollToParent = scrollToMessageWithCreatedAt(message.createdAt, true, 0);
139102
if (!canScrollToParent) toast.show(STRINGS.TOAST.FIND_PARENT_MSG_ERROR, 'error');
140103
});
141104

142105
return (
143106
<ChannelMessageList
144107
{...props}
145-
ref={ref}
108+
ref={flatListRef}
146109
onReplyMessage={setMessageToReply}
147110
onEditMessage={setMessageToEdit}
148111
onPressParentMessage={onPressParentMessage}

packages/uikit-react-native/src/domain/groupChannel/module/moduleContext.tsx

Lines changed: 114 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,34 @@
1-
import React, { createContext, useCallback, useState } from 'react';
1+
import React, { createContext, useCallback, useRef, useState } from 'react';
2+
import type { FlatList } from 'react-native';
23

34
import { useChannelHandler } from '@sendbird/uikit-chat-hooks';
45
import {
6+
ContextValue,
57
Logger,
68
NOOP,
79
SendbirdFileMessage,
810
SendbirdGroupChannel,
11+
SendbirdMessage,
912
SendbirdUser,
1013
SendbirdUserMessage,
1114
isDifferentChannel,
15+
useFreshCallback,
1216
useUniqHandlerId,
1317
} from '@sendbird/uikit-utils';
1418

1519
import ProviderLayout from '../../../components/ProviderLayout';
20+
import { MESSAGE_FOCUS_ANIMATION_DELAY } from '../../../constants';
1621
import { useLocalization, useSendbirdChat } from '../../../hooks/useContext';
1722
import type { PubSub } from '../../../utils/pubsub';
18-
import type {
19-
GroupChannelContextsType,
20-
GroupChannelModule,
21-
GroupChannelPubSubContextPayload,
22-
GroupChannelScrollToMessageFunc,
23-
} from '../types';
23+
import type { GroupChannelContextsType, GroupChannelModule, GroupChannelPubSubContextPayload } from '../types';
24+
import { GroupChannelProps } from '../types';
2425

2526
export const GroupChannelContexts: GroupChannelContextsType = {
2627
Fragment: createContext({
2728
headerTitle: '',
2829
channel: {} as SendbirdGroupChannel,
2930
setMessageToEdit: NOOP,
3031
setMessageToReply: NOOP,
31-
scrollToMessage: (() => false) as GroupChannelScrollToMessageFunc,
32-
__internalSetScrollToMessageFunc: (_func: () => GroupChannelScrollToMessageFunc) => {
33-
// noop
34-
},
3532
}),
3633
TypingIndicator: createContext({
3734
typingUsers: [] as SendbirdUser[],
@@ -40,6 +37,16 @@ export const GroupChannelContexts: GroupChannelContextsType = {
4037
publish: NOOP,
4138
subscribe: () => NOOP,
4239
} as PubSub<GroupChannelPubSubContextPayload>),
40+
MessageList: createContext({
41+
flatListRef: { current: null },
42+
scrollToMessage: () => false,
43+
lazyScrollToBottom: () => {
44+
// noop
45+
},
46+
lazyScrollToIndex: () => {
47+
// noop
48+
},
49+
} as MessageListContextValue),
4350
};
4451

4552
export const GroupChannelContextsProvider: GroupChannelModule['Provider'] = ({
@@ -48,6 +55,8 @@ export const GroupChannelContextsProvider: GroupChannelModule['Provider'] = ({
4855
enableTypingIndicator,
4956
keyboardAvoidOffset = 0,
5057
groupChannelPubSub,
58+
messages,
59+
onUpdateSearchItem,
5160
}) => {
5261
if (!channel) throw new Error('GroupChannel is not provided to GroupChannelModule');
5362

@@ -58,11 +67,10 @@ export const GroupChannelContextsProvider: GroupChannelModule['Provider'] = ({
5867
const [typingUsers, setTypingUsers] = useState<SendbirdUser[]>([]);
5968
const [messageToEdit, setMessageToEdit] = useState<SendbirdUserMessage | SendbirdFileMessage>();
6069
const [messageToReply, setMessageToReply] = useState<SendbirdUserMessage | SendbirdFileMessage>();
61-
const [scrollToMessage, __internalSetScrollToMessageFunc] = useState<GroupChannelScrollToMessageFunc>(() => () => {
62-
Logger.error(
63-
'You should render `src/domain/groupChannel/component/GroupChannelMessageList.tsx` component first to use scrollToMessage.',
64-
);
65-
return false;
70+
71+
const { flatListRef, lazyScrollToIndex, lazyScrollToBottom, scrollToMessage } = useScrollActions({
72+
messages,
73+
onUpdateSearchItem,
6674
});
6775

6876
const updateInputMode = (mode: 'send' | 'edit' | 'reply', message?: SendbirdUserMessage | SendbirdFileMessage) => {
@@ -115,16 +123,99 @@ export const GroupChannelContextsProvider: GroupChannelModule['Provider'] = ({
115123
setMessageToEdit: useCallback((message) => updateInputMode('edit', message), []),
116124
messageToReply,
117125
setMessageToReply: useCallback((message) => updateInputMode('reply', message), []),
118-
scrollToMessage,
119-
__internalSetScrollToMessageFunc,
120126
}}
121127
>
122-
<GroupChannelContexts.TypingIndicator.Provider value={{ typingUsers }}>
123-
<GroupChannelContexts.PubSub.Provider value={groupChannelPubSub}>
124-
{children}
125-
</GroupChannelContexts.PubSub.Provider>
126-
</GroupChannelContexts.TypingIndicator.Provider>
128+
<GroupChannelContexts.PubSub.Provider value={groupChannelPubSub}>
129+
<GroupChannelContexts.TypingIndicator.Provider value={{ typingUsers }}>
130+
<GroupChannelContexts.MessageList.Provider
131+
value={{
132+
flatListRef,
133+
scrollToMessage,
134+
lazyScrollToIndex,
135+
lazyScrollToBottom,
136+
}}
137+
>
138+
{children}
139+
</GroupChannelContexts.MessageList.Provider>
140+
</GroupChannelContexts.TypingIndicator.Provider>
141+
</GroupChannelContexts.PubSub.Provider>
127142
</GroupChannelContexts.Fragment.Provider>
128143
</ProviderLayout>
129144
);
130145
};
146+
147+
type MessageListContextValue = ContextValue<GroupChannelContextsType['MessageList']>;
148+
const useScrollActions = (params: Pick<GroupChannelProps['Provider'], 'messages' | 'onUpdateSearchItem'>) => {
149+
const { messages, onUpdateSearchItem } = params;
150+
const flatListRef = useRef<FlatList<SendbirdMessage>>(null);
151+
152+
// FIXME: Workaround, should run after data has been applied to UI.
153+
const lazyScrollToBottom = useFreshCallback<MessageListContextValue['lazyScrollToIndex']>((params) => {
154+
if (!flatListRef.current) {
155+
logFlatListRefWarning();
156+
return;
157+
}
158+
159+
setTimeout(() => {
160+
flatListRef.current?.scrollToOffset({ offset: 0, animated: params?.animated ?? false });
161+
}, params?.timeout ?? 0);
162+
});
163+
164+
// FIXME: Workaround, should run after data has been applied to UI.
165+
const lazyScrollToIndex = useFreshCallback<MessageListContextValue['lazyScrollToIndex']>((params) => {
166+
if (!flatListRef.current) {
167+
logFlatListRefWarning();
168+
return;
169+
}
170+
171+
setTimeout(() => {
172+
flatListRef.current?.scrollToIndex({
173+
index: params?.index ?? 0,
174+
animated: params?.animated ?? false,
175+
viewPosition: params?.viewPosition ?? 0.5,
176+
});
177+
}, params?.timeout ?? 0);
178+
});
179+
180+
const scrollToMessage = useFreshCallback<MessageListContextValue['scrollToMessage']>((messageId, options) => {
181+
if (!flatListRef.current) {
182+
logFlatListRefWarning();
183+
return false;
184+
}
185+
186+
const foundMessageIndex = messages.findIndex((it) => it.messageId === messageId);
187+
const isIncludedInList = foundMessageIndex > -1;
188+
189+
if (isIncludedInList) {
190+
if (options?.focusAnimated) {
191+
setTimeout(
192+
() => onUpdateSearchItem({ startingPoint: messages[foundMessageIndex].createdAt }),
193+
MESSAGE_FOCUS_ANIMATION_DELAY,
194+
);
195+
}
196+
lazyScrollToIndex({
197+
index: foundMessageIndex,
198+
animated: true,
199+
timeout: 0,
200+
viewPosition: options?.viewPosition,
201+
});
202+
return true;
203+
} else {
204+
return false;
205+
}
206+
});
207+
208+
return {
209+
flatListRef,
210+
lazyScrollToIndex,
211+
lazyScrollToBottom,
212+
scrollToMessage,
213+
};
214+
};
215+
216+
const logFlatListRefWarning = () => {
217+
Logger.warn(
218+
'Cannot find flatListRef.current, please render FlatList and pass the flatListRef' +
219+
'or please try again after FlatList has been rendered.',
220+
);
221+
};

0 commit comments

Comments
 (0)