@messenger-box/platform-mobile 10.0.3-alpha.22 → 10.0.3-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/screens/inbox/DialogThreads.js +52 -12
  3. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  4. package/lib/screens/inbox/components/ThreadsViewItem.js +66 -44
  5. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  6. package/lib/screens/inbox/containers/ConversationView.js +36 -34
  7. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  8. package/lib/screens/inbox/containers/Dialogs.js +82 -37
  9. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  10. package/lib/screens/inbox/containers/ThreadConversationView.js +282 -219
  11. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  12. package/lib/screens/inbox/containers/ThreadsView.js +83 -50
  13. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  14. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  15. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  16. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  17. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  18. package/package.json +2 -2
  19. package/src/screens/inbox/DialogThreads.tsx +49 -53
  20. package/src/screens/inbox/components/SmartLoader.tsx +61 -0
  21. package/src/screens/inbox/components/ThreadsViewItem.tsx +177 -265
  22. package/src/screens/inbox/containers/ConversationView.tsx +32 -30
  23. package/src/screens/inbox/containers/Dialogs.tsx +57 -22
  24. package/src/screens/inbox/containers/ThreadConversationView.tsx +467 -484
  25. package/src/screens/inbox/containers/ThreadsView.tsx +102 -183
  26. package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
  27. package/src/screens/inbox/index.ts +37 -0
  28. package/src/screens/inbox/machines/threadsMachine.ts +147 -0
  29. package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
@@ -13,6 +13,7 @@ import { userSelector } from '@adminide-stack/user-auth0-client';
13
13
  import { CHANGE_SETTINGS_ACTION } from '@admin-layout/client';
14
14
  import { ThreadViewItem } from '../components/ThreadsViewItem';
15
15
  import { config } from '../config';
16
+ import { Actions } from '../workflow/dialog-threads-xstate';
16
17
  const { MESSAGES_PER_PAGE } = config;
17
18
 
18
19
  export interface ThreadsViewProps {
@@ -49,27 +50,46 @@ const ThreadsViewComponent = ({
49
50
  const navigation = useNavigation<any>();
50
51
  const isFocused = useIsFocused();
51
52
  const [refreshing, setRefresh] = useState<boolean>(false);
52
- const [threadData, setThreadsData] = useState<any>([]);
53
53
 
54
- // const {
55
- // data,
56
- // loading: threadLoading,
57
- // error,
58
- // refetch,
59
- // subscribeToMore,
60
- // } = useThreadMessagesQuery({
61
- // variables: {
62
- // channelId: channelId?.toString(),
63
- // role: role?.toString(),
64
- // limit: MESSAGES_PER_PAGE,
65
- // repliesLimit2: 5,
66
- // },
67
- // // fetchPolicy: 'cache-and-network',
68
- // });
69
-
70
- // const { data: threadCreatedUpdated } = useOnThreadCreatedUpdatedSubscription({
71
- // variables: { channelId: channelId?.toString() },
72
- // });
54
+ // Use thread subscription for real-time updates
55
+ const { data: threadCreatedUpdated } = useOnThreadCreatedUpdatedSubscription({
56
+ variables: { channelId: channelId?.toString() },
57
+ });
58
+
59
+ // Subscribe to thread updates
60
+ const subscribeToNewMessages = useCallback(() => {
61
+ return subscribeToMore({
62
+ document: THREAD_CHAT_ADDED,
63
+ variables: { channelId: channelId?.toString() },
64
+ updateQuery: (prev: any, { subscriptionData }: any) => {
65
+ if (!subscriptionData.data) return prev;
66
+
67
+ const newThread = subscriptionData.data.threadCreatedUpdated.data;
68
+ if (!newThread) return prev;
69
+
70
+ const threads = prev.threadMessages.data || [];
71
+ const index = threads.findIndex((t: any) => t.id === newThread.id);
72
+
73
+ let updatedThreads;
74
+ if (index > -1) {
75
+ // Update existing thread
76
+ updatedThreads = [...threads];
77
+ updatedThreads[index] = newThread;
78
+ } else {
79
+ // Add new thread
80
+ updatedThreads = [newThread, ...threads];
81
+ }
82
+
83
+ return {
84
+ ...prev,
85
+ threadMessages: {
86
+ ...prev.threadMessages,
87
+ data: uniqBy(updatedThreads, 'id'),
88
+ },
89
+ };
90
+ },
91
+ });
92
+ }, [channelId, subscribeToMore]);
73
93
 
74
94
  useFocusEffect(
75
95
  React.useCallback(() => {
@@ -81,64 +101,25 @@ const ThreadsViewComponent = ({
81
101
  limit: MESSAGES_PER_PAGE,
82
102
  repliesLimit2: 5,
83
103
  });
84
- // .then(({ data }) => {
85
- // if (!data?.threadMessages?.data) {
86
- // return;
87
- // }
88
104
 
89
- // if (data?.threadMessages?.data?.length) {
90
- // const { data: newThreads } = data?.threadMessages;
91
- // setThreadsData((oldThreads: any) => uniqBy([...oldThreads, ...newThreads], ({ id }) => id));
92
- // }
93
- // });
105
+ // Setup subscription
106
+ const unsubscribe = subscribeToNewMessages();
94
107
 
95
108
  return () => {
96
- // Do something when the screen is unfocused
97
- // Useful for cleanup functions
109
+ // Unsubscribe on cleanup
110
+ if (unsubscribe) unsubscribe();
98
111
  };
99
112
  }, []),
100
113
  );
101
114
 
102
- // React.useEffect(() => {
103
- // if (data?.threadMessages?.data?.length) {
104
- // const { data: newThreads } = data?.threadMessages;
105
- // setThreadsData((oldThreads: any) => uniqBy([...oldThreads, ...newThreads], ({ id }) => id));
106
- // }
107
- // }, [data]);
108
-
109
- // React.useEffect(() => {
110
- // if (threadCreatedUpdated?.threadCreatedUpdated?.data) {
111
- // const { data: newThreads } = threadCreatedUpdated?.threadCreatedUpdated;
112
-
113
- // setThreadsData((oldThreads: any) => {
114
- // const i = oldThreads.findIndex((el: any) => el.id === newThreads?.id);
115
- // if (i > -1) oldThreads[i] = newThreads; // (2)
116
- // else oldThreads.push(newThreads);
117
- // return oldThreads;
118
- // });
119
- // // setThreadsData((oldThreads: any) => uniqBy([...oldThreads, newThreads], ({ id }) => id));
120
- // }
121
- // }, [threadCreatedUpdated]);
122
-
123
- const setData = React.useCallback((newThreads: any) => {
124
- setThreadsData((oldThreads: any) => {
125
- const i = oldThreads.findIndex((el: any) => el.id === newThreads?.id);
126
- if (i > -1) oldThreads[i] = newThreads; // (2)
127
- else oldThreads.push(newThreads);
128
- return oldThreads;
129
- });
130
- // setThreadsData((oldThreads: any) => uniqBy([...oldThreads, newThreads], ({ id }) => id));
131
- }, []);
132
-
133
- // const threads = React.useMemo(() => {
134
- // if (!threadData?.length) return null;
135
- // return orderBy(threadData, ['updatedAt'], ['desc']) || [];
136
- // //return threadData || [];
137
- // }, [threadData]);
115
+ // Effect for thread updates from subscription
116
+ React.useEffect(() => {
117
+ if (threadCreatedUpdated?.threadCreatedUpdated?.data) {
118
+ // The subscription handler will handle updates
119
+ }
120
+ }, [threadCreatedUpdated]);
138
121
 
139
122
  const threads = React.useMemo(() => {
140
- // if (!data?.threadMessages?.data?.length) return null;
141
- // const { data: newThreads } = data?.threadMessages;
142
123
  if (data?.threadMessages?.data?.length == 0) return [];
143
124
  const threadsFiltered = data?.threadMessages?.data
144
125
  ? uniqBy([...data?.threadMessages?.data], ({ id }: any) => id)
@@ -149,7 +130,6 @@ const ThreadsViewComponent = ({
149
130
  const threadReplies = React.useMemo(() => {
150
131
  if (!threads?.length) return null;
151
132
  return threads?.map((t: any) => t?.replies)?.flat(1) || [];
152
- //return threadData || [];
153
133
  }, [threads]);
154
134
 
155
135
  const handleSelectThread = useCallback((id: any, title: any, postParentId: any) => {
@@ -160,146 +140,85 @@ const ThreadsViewComponent = ({
160
140
  postParentId: postParentId,
161
141
  hideTabBar: true,
162
142
  });
163
- // if (params?.channelId) {
164
- // navigation.navigate(config.THREAD_MESSEGE_PATH as any, {
165
- // channelId: params?.channelId,
166
- // title: params?.title ?? null,
167
- // postParentId: postParentId,
168
- // hideTabBar: true,
169
- // });
170
- // } else {
171
- // navigation.navigate(config.THREAD_MESSEGE_PATH as any, {
172
- // channelId: id,
173
- // title: title,
174
- // postParentId: postParentId,
175
- // hideTabBar: true,
176
- // });
177
- // }
178
143
  }, []);
179
144
 
180
145
  const handleRefresh = useCallback(() => {
146
+ setRefresh(true);
181
147
  refetch({
182
148
  channelId: channelId?.toString(),
183
149
  role: role?.toString(),
184
150
  limit: MESSAGES_PER_PAGE,
185
151
  repliesLimit2: 5,
152
+ }).finally(() => {
153
+ setRefresh(false);
186
154
  });
187
- // .then(({ data }) => {
188
- // if (!data?.threadMessages?.data) {
189
- // return;
190
- // }
191
-
192
- // if (data?.threadMessages?.data?.length) {
193
- // const { data: newThreads } = data?.threadMessages;
194
- // setThreadsData((oldThreads: any) => uniqBy([...oldThreads, ...newThreads], ({ id }) => id));
195
- // }
196
- // });
197
155
  }, []);
198
156
 
199
157
  const fetchMoreThreads = useCallback(() => {
200
158
  refetch({
201
159
  channelId: channelId?.toString(),
202
160
  role: role?.toString(),
203
- skip: threads?.length,
161
+ limit: MESSAGES_PER_PAGE,
162
+ repliesLimit2: 5,
204
163
  });
205
164
  }, []);
206
165
 
207
- // if (!threadReplies || threadReplies?.length == 0) {
208
- // return (
209
- // <Center w="full" bg={'f8f8f8'} height={'full'}>
210
- // You don't have any message yet!
211
- // </Center>
212
- // );
213
- // }
214
- console.log('threadLoading', threadLoading, ' threads?.length=', threads?.length);
166
+ if (error) {
167
+ return (
168
+ <Box style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
169
+ <Text>Error loading threads. {error.message}</Text>
170
+ <Button onPress={handleRefresh} style={{ marginTop: 4 }}>
171
+ <Text>Try Again</Text>
172
+ </Button>
173
+ </Box>
174
+ );
175
+ }
215
176
 
216
177
  return (
217
- <Box className="flex-1 p-2 pb-0 bg-gray-100 dark:border-gray-600 dark:bg-gray-700">
218
- <FlatList
219
- style={{ flex: 1 }}
220
- data={threads && threads?.length > 0 ? threads : []}
221
- onRefresh={handleRefresh}
222
- refreshing={threadLoading}
223
- contentContainerStyle={{ paddingBottom: 60 }}
224
- ItemSeparatorComponent={() => <Box className="h-0.5 bg-gray-200" />}
225
- renderItem={({ item: thread, index }) => (
226
- <ThreadViewItem
227
- key={index}
228
- onOpen={handleSelectThread}
229
- currentUser={auth}
230
- thread={thread}
231
- role={role}
232
- setData={setData}
233
- />
234
- )}
235
- ListEmptyComponent={() => (
236
- <>
237
- {/* {!threadLoading || threads?.length == 0} */}
238
- {!threadLoading && threads?.length == 0 && (
239
- <Box className="p-5">
240
- <Center className="mt-6">
241
- <Ionicons name="chatbubbles" size={50} />
242
- <Text>You don't have any message yet!</Text>
243
- </Center>
244
- </Box>
245
- )}
246
- </>
247
- )}
248
- ListFooterComponent={() => (
249
- <>
250
- <SubscriptionHandler
178
+ <Box style={{ flex: 1 }}>
179
+ {threadLoading && !threads?.length ? (
180
+ <Center style={{ flex: 1 }}>
181
+ <Spinner size="large" />
182
+ </Center>
183
+ ) : threads?.length === 0 ? (
184
+ <Box style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
185
+ <Text>No threads found</Text>
186
+ </Box>
187
+ ) : (
188
+ <FlatList
189
+ data={threads}
190
+ keyExtractor={(item) => item.id.toString()}
191
+ renderItem={({ item }) => (
192
+ <ThreadViewItem
193
+ {...item}
194
+ onPress={handleSelectThread}
251
195
  channelId={channelId}
252
- subscribeToNewMessages={() =>
253
- subscribeToMore({
254
- document: THREAD_CHAT_ADDED,
255
- variables: {
256
- channelId: channelId?.toString(),
257
- postParentId: null,
258
- },
259
- updateQuery: (prev, { subscriptionData }: any) => {
260
- if (!subscriptionData.data) return prev;
261
- const newPostThreadData: any =
262
- subscriptionData?.data?.threadCreatedUpdated?.data;
263
- const newMessage: any =
264
- subscriptionData?.data?.threadCreatedUpdated?.lastMessage;
265
- const data = prev?.threadMessages?.data?.map((t: any) =>
266
- t.id === newPostThreadData?.id
267
- ? {
268
- ...t,
269
- replies: [newMessage, ...t?.replies],
270
- replyCount: newPostThreadData?.replyCount,
271
- lastReplyAt: newPostThreadData?.lastReplyAt,
272
- updatedAt: newPostThreadData?.updatedAt,
273
- }
274
- : t,
275
- );
276
-
277
- return Object.assign({}, prev, {
278
- threadMessages: {
279
- ...prev?.threadMessages,
280
- // totalCount: prev?.threadMessages?.totalCount + 1,
281
- data: data,
282
- },
283
- });
284
- },
285
- })
286
- }
196
+ channelsDetail={channelsDetail}
287
197
  />
288
- </>
289
- )}
290
- keyExtractor={(item, index) => 'threads-view-key' + index}
291
- onEndReached={fetchMoreThreads}
292
- onEndReachedThreshold={0.1}
293
- />
198
+ )}
199
+ refreshing={refreshing}
200
+ onRefresh={handleRefresh}
201
+ onEndReached={fetchMoreThreads}
202
+ onEndReachedThreshold={0.5}
203
+ contentContainerStyle={{ padding: 16 }}
204
+ ItemSeparatorComponent={() => <Box style={{ height: 3 }} />}
205
+ />
206
+ )}
207
+ <SubscriptionHandler subscribeToNewMessages={subscribeToNewMessages} channelId={channelId} />
294
208
  </Box>
295
209
  );
296
210
  };
297
211
 
212
+ // Component to handle subscription logic
298
213
  const SubscriptionHandler = ({ subscribeToNewMessages, channelId }: IThreadSubscriptionHandlerProps) => {
299
- useEffect(() => subscribeToNewMessages(), [channelId]);
300
- return <></>;
214
+ useEffect(() => {
215
+ const unsubscribe = subscribeToNewMessages();
216
+ return () => {
217
+ if (unsubscribe) unsubscribe();
218
+ };
219
+ }, [channelId]);
220
+
221
+ return null;
301
222
  };
302
223
 
303
224
  export const ThreadsView = ThreadsViewComponent;
304
-
305
- // export const ThreadsView = React.memo(ThreadsViewComponent);
@@ -0,0 +1,136 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ import { BaseState, Actions, dialogThreadsXstate } from '../workflow/dialog-threads-xstate';
3
+
4
+ // Define proper types for the state and context
5
+ export interface DialogThreadsContext {
6
+ channelId?: string;
7
+ postParentId?: string | number;
8
+ role?: string;
9
+ channelsDetail: any;
10
+ threadData: any[];
11
+ loading: boolean;
12
+ error: any;
13
+ }
14
+
15
+ export interface DialogThreadsState {
16
+ context: DialogThreadsContext;
17
+ value: BaseState;
18
+ matches: (stateValue: string) => boolean;
19
+ }
20
+
21
+ // Define proper action types
22
+ export type DialogThreadsEvent =
23
+ | { type: typeof Actions.INITIALIZE; data: { channelId?: string; postParentId?: string | number; role?: string } }
24
+ | { type: typeof Actions.FETCH_CHANNEL_DETAIL; data: { id: string } }
25
+ | { type: typeof Actions.SET_CHANNEL_DETAIL; data: { channelsDetail: any } }
26
+ | { type: typeof Actions.FETCH_THREADS; data?: any }
27
+ | { type: typeof Actions.SET_THREADS; data: { threadData: any[] } }
28
+ | { type: typeof Actions.ERROR; data: { error: any } };
29
+
30
+ /**
31
+ * Custom hook to safely use the dialog threads state machine
32
+ * This provides a fallback implementation in case useMachine from XState has issues
33
+ */
34
+ export function useSafeDialogThreadsMachine(): [DialogThreadsState, (event: DialogThreadsEvent) => void] {
35
+ // Initialize with default state from the machine's initial context
36
+ const [state, setState] = useState<DialogThreadsState>({
37
+ context: {
38
+ channelId: undefined,
39
+ postParentId: undefined,
40
+ role: undefined,
41
+ channelsDetail: null,
42
+ threadData: [],
43
+ loading: true,
44
+ error: null,
45
+ },
46
+ value: BaseState.Idle,
47
+ matches: (value: string) => value === BaseState.Idle,
48
+ });
49
+
50
+ // Create a memoized function to determine state matching
51
+ const matches = useCallback((value: string) => value === state.value, [state.value]);
52
+
53
+ // Update the matches function when state changes
54
+ useMemo(() => {
55
+ setState((prev) => ({
56
+ ...prev,
57
+ matches,
58
+ }));
59
+ }, [matches]);
60
+
61
+ // Create a safe send function to handle events
62
+ const send = useCallback((event: DialogThreadsEvent) => {
63
+ try {
64
+ // Handle specific events based on the event type
65
+ if (event.type === Actions.INITIALIZE) {
66
+ setState((prev) => ({
67
+ ...prev,
68
+ context: {
69
+ ...prev.context,
70
+ channelId: event.data?.channelId,
71
+ postParentId: event.data?.postParentId,
72
+ role: event.data?.role,
73
+ loading: true,
74
+ error: null,
75
+ },
76
+ value: BaseState.LoadingChannel,
77
+ }));
78
+ } else if (event.type === Actions.SET_CHANNEL_DETAIL) {
79
+ setState((prev) => ({
80
+ ...prev,
81
+ context: {
82
+ ...prev.context,
83
+ channelsDetail: event.data?.channelsDetail,
84
+ loading: false,
85
+ },
86
+ value: BaseState.LoadingThreads,
87
+ }));
88
+ } else if (event.type === Actions.SET_THREADS) {
89
+ setState((prev) => ({
90
+ ...prev,
91
+ context: {
92
+ ...prev.context,
93
+ threadData: event.data?.threadData || [],
94
+ loading: false,
95
+ },
96
+ value: BaseState.Active,
97
+ }));
98
+ } else if (event.type === Actions.FETCH_THREADS) {
99
+ setState((prev) => ({
100
+ ...prev,
101
+ context: {
102
+ ...prev.context,
103
+ loading: true,
104
+ },
105
+ value: BaseState.LoadingThreads,
106
+ }));
107
+ } else if (event.type === Actions.ERROR) {
108
+ setState((prev) => ({
109
+ ...prev,
110
+ context: {
111
+ ...prev.context,
112
+ error: event.data?.error,
113
+ loading: false,
114
+ },
115
+ value: BaseState.Error,
116
+ }));
117
+ }
118
+ } catch (error) {
119
+ // Handle errors gracefully
120
+ setState((prev) => ({
121
+ ...prev,
122
+ context: {
123
+ ...prev.context,
124
+ error,
125
+ loading: false,
126
+ },
127
+ value: BaseState.Error,
128
+ }));
129
+ }
130
+ }, []);
131
+
132
+ // Return as a tuple to match useMachine API
133
+ return [state, send];
134
+ }
135
+
136
+ export default useSafeDialogThreadsMachine;
@@ -0,0 +1,37 @@
1
+ // State machines
2
+ export {
3
+ dialogThreadsXstate,
4
+ Actions as DialogThreadsActions,
5
+ BaseState as DialogThreadsState,
6
+ } from './workflow/dialog-threads-xstate';
7
+ export {
8
+ threadConversationXstate,
9
+ Actions as ThreadConversationActions,
10
+ BaseState as ThreadConversationState,
11
+ } from './containers/workflow/thread-conversation-xstate';
12
+ export {
13
+ conversationXstate,
14
+ Actions as ConversationActions,
15
+ BaseState as ConversationState,
16
+ } from './containers/workflow/conversation-xstate';
17
+ export {
18
+ dialogsXstate,
19
+ Actions as DialogsActions,
20
+ BaseState as DialogsState,
21
+ } from './containers/workflow/dialogs-xstate';
22
+
23
+ // Hooks
24
+ export { default as useSafeDialogThreadsMachine } from './hooks/useSafeDialogThreadsMachine';
25
+
26
+ // Components
27
+ export { DialogThreads } from './DialogThreads';
28
+ export { ThreadsView } from './containers/ThreadsView';
29
+ export { ThreadConversationView } from './containers/ThreadConversationView';
30
+ export { ConversationView } from './containers/ConversationView';
31
+ export { Dialogs } from './containers/Dialogs';
32
+
33
+ // Configuration
34
+ export { config } from './config';
35
+
36
+ // Utility
37
+ export { threadsMachineConfig, setupThreadSubscription } from './machines/threadsMachine';
@@ -0,0 +1,147 @@
1
+ import { orderBy, uniqBy } from 'lodash-es';
2
+ import { config } from '../config';
3
+ import { OnThreadCreatedUpdatedDocument as THREAD_CHAT_ADDED } from 'common/graphql';
4
+
5
+ const { MESSAGES_PER_PAGE } = config;
6
+
7
+ // Define more specific types for threads and thread data
8
+ export interface Thread {
9
+ id: string;
10
+ updatedAt?: string | number; // Make updatedAt optional to match the GraphQL response
11
+ [key: string]: any;
12
+ }
13
+
14
+ export interface ThreadsData {
15
+ threadMessages?: {
16
+ data: Thread[] | any[]; // Allow any[] to accommodate GraphQL response types
17
+ };
18
+ [key: string]: any; // Allow additional properties from GraphQL responses
19
+ }
20
+
21
+ export interface ThreadsContext {
22
+ threads: Thread[];
23
+ loading: boolean;
24
+ error: any;
25
+ channelId?: string;
26
+ role?: string;
27
+ refreshing: boolean;
28
+ }
29
+
30
+ // Type for refetch function
31
+ export type RefetchFunction = (variables: any) => Promise<{ data?: any }>;
32
+
33
+ // Type for subscription function
34
+ export type SubscribeFunction = (options: {
35
+ document: any;
36
+ variables: any;
37
+ updateQuery: (prev: any, { subscriptionData }: any) => any;
38
+ }) => () => void;
39
+
40
+ // Define machine actions and services with proper typing
41
+ export const threadsMachineConfig = {
42
+ // Services
43
+ fetchThreads: (refetch: RefetchFunction, context: ThreadsContext) => {
44
+ if (!context.channelId) return Promise.resolve({ data: undefined });
45
+
46
+ return refetch({
47
+ channelId: context.channelId?.toString(),
48
+ role: context.role?.toString(),
49
+ limit: MESSAGES_PER_PAGE,
50
+ repliesLimit2: 5,
51
+ });
52
+ },
53
+
54
+ fetchMoreThreads: (refetch: RefetchFunction, context: ThreadsContext) => {
55
+ if (!context.channelId || !context.threads?.length) {
56
+ return Promise.resolve({ data: undefined });
57
+ }
58
+
59
+ return refetch({
60
+ channelId: context.channelId?.toString(),
61
+ role: context.role?.toString(),
62
+ skip: context.threads?.length,
63
+ });
64
+ },
65
+
66
+ refreshThreads: (refetch: RefetchFunction, context: ThreadsContext) => {
67
+ if (!context.channelId) return Promise.resolve({ data: undefined });
68
+
69
+ return refetch({
70
+ channelId: context.channelId?.toString(),
71
+ role: context.role?.toString(),
72
+ limit: MESSAGES_PER_PAGE,
73
+ repliesLimit2: 5,
74
+ });
75
+ },
76
+
77
+ // Actions - optimized for performance with proper type handling
78
+ setThreadData: (data: ThreadsData): Thread[] => {
79
+ if (!data?.threadMessages?.data) return [];
80
+
81
+ const threadsData = data.threadMessages.data;
82
+ if (!threadsData.length) return [];
83
+
84
+ // Use a more efficient process with proper typing
85
+ const threadsFiltered = uniqBy(threadsData, 'id');
86
+ return orderBy(threadsFiltered, ['updatedAt'], ['desc']) || [];
87
+ },
88
+
89
+ appendThreadData: (existingThreads: Thread[], data: ThreadsData): Thread[] => {
90
+ if (!data?.threadMessages?.data) return existingThreads;
91
+
92
+ const newThreads = data.threadMessages.data;
93
+ if (!newThreads.length) return existingThreads;
94
+
95
+ // Use a more efficient combination process
96
+ const combinedThreads = uniqBy([...existingThreads, ...newThreads], 'id');
97
+ return orderBy(combinedThreads, ['updatedAt'], ['desc']) || [];
98
+ },
99
+
100
+ updateThreadData: (oldThreads: Thread[], newThread: Thread | null): Thread[] => {
101
+ if (!newThread) return oldThreads;
102
+ if (!oldThreads?.length) return [newThread]; // Handle empty array case efficiently
103
+
104
+ // Check if thread exists to avoid unnecessary array copy
105
+ const index = oldThreads.findIndex((el) => el.id === newThread.id);
106
+
107
+ let updatedThreads: Thread[];
108
+ if (index > -1) {
109
+ // Create a new array only if we're updating
110
+ updatedThreads = [...oldThreads];
111
+ updatedThreads[index] = newThread;
112
+ } else {
113
+ // Simply add the new thread at the beginning for efficiency
114
+ updatedThreads = [newThread, ...oldThreads];
115
+ }
116
+
117
+ return orderBy(updatedThreads, ['updatedAt'], ['desc']) || [];
118
+ },
119
+ };
120
+
121
+ // Optimized function to subscribe to thread updates
122
+ export const setupThreadSubscription = (
123
+ subscribeToMore: SubscribeFunction | undefined,
124
+ channelId: string | undefined,
125
+ ): (() => void) | undefined => {
126
+ if (!subscribeToMore || !channelId) return undefined;
127
+
128
+ return subscribeToMore({
129
+ document: THREAD_CHAT_ADDED,
130
+ variables: { channelId: channelId?.toString() },
131
+ updateQuery: (prev, { subscriptionData }) => {
132
+ if (!subscriptionData?.data) return prev;
133
+
134
+ const newThread = subscriptionData.data.threadCreatedUpdated?.data;
135
+ if (!newThread) return prev;
136
+
137
+ // Only create a new object if needed to optimize for unnecessary renders
138
+ return {
139
+ ...prev,
140
+ threadMessages: {
141
+ ...prev.threadMessages,
142
+ data: uniqBy([newThread, ...(prev.threadMessages?.data || [])], 'id'),
143
+ },
144
+ };
145
+ },
146
+ });
147
+ };