@messenger-box/platform-mobile 10.0.3-alpha.34 → 10.0.3-alpha.37

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 (34) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/screens/inbox/components/CachedImage/index.js +125 -93
  3. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  4. package/lib/screens/inbox/components/DialogsListItem.js +80 -256
  5. package/lib/screens/inbox/components/DialogsListItem.js.map +1 -1
  6. package/lib/screens/inbox/components/ServiceDialogsListItem.js +222 -324
  7. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +1 -1
  8. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +0 -2
  9. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  10. package/lib/screens/inbox/containers/ConversationView.js +487 -888
  11. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  12. package/lib/screens/inbox/containers/Dialogs.js +243 -547
  13. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  14. package/lib/screens/inbox/containers/ThreadConversationView.js +409 -1364
  15. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  16. package/package.json +4 -4
  17. package/src/screens/inbox/components/CachedImage/index.tsx +191 -140
  18. package/src/screens/inbox/components/DialogsListItem.tsx +112 -345
  19. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +316 -437
  20. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +2 -4
  21. package/src/screens/inbox/containers/ConversationView.tsx +676 -993
  22. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  23. package/src/screens/inbox/containers/Dialogs.tsx +345 -636
  24. package/src/screens/inbox/containers/ThreadConversationView.tsx +661 -1887
  25. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js +0 -175
  26. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js.map +0 -1
  27. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js +0 -191
  28. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js.map +0 -1
  29. package/lib/screens/inbox/containers/workflow/conversation-xstate.js +0 -380
  30. package/lib/screens/inbox/containers/workflow/conversation-xstate.js.map +0 -1
  31. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +0 -211
  32. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +0 -1
  33. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js +0 -438
  34. package/lib/screens/inbox/containers/workflow/thread-conversation-xstate.js.map +0 -1
@@ -1,45 +1,15 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { FlatList, Box, Heading, Input, InputField, Text, Center, Spinner } from '@admin-layout/gluestack-ui-mobile';
3
3
  import { Ionicons } from '@expo/vector-icons';
4
4
  import { useSelector } from 'react-redux';
5
5
  import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native';
6
- import { orderBy } from 'lodash-es';
7
6
  import { DialogsListItem } from '../components/DialogsListItem';
8
7
  import { ServiceDialogsListItem } from '../components/ServiceDialogsListItem';
9
- import { useGetChannelsByUserWithServiceChannelsQuery } from 'common/graphql';
8
+ import { useGetChannelsByUserWithServiceChannelsQuery, OnChatMessageAddedDocument } from 'common/graphql';
10
9
  import { RoomType } from 'common';
11
10
  import { userSelector } from '@adminide-stack/user-auth0-client';
12
11
  import { config } from '../config';
13
12
  import colors from 'tailwindcss/colors';
14
- import { dialogsXstate } from './workflow/dialogs-xstate';
15
-
16
- // Define custom actions and states for our component
17
- const Actions = {
18
- INITIAL_CONTEXT: 'INITIAL_CONTEXT',
19
- ERROR_HANDLED: 'ERROR_HANDLED',
20
- FETCH_CHANNELS: 'FETCH_CHANNELS',
21
- APPEND_CHANNELS: 'APPEND_CHANNELS',
22
- REFRESH_CHANNELS: 'REFRESH_CHANNELS',
23
- SELECT_CHANNEL: 'SELECT_CHANNEL',
24
- START_LOADING: 'START_LOADING',
25
- STOP_LOADING: 'STOP_LOADING',
26
- LOAD_MORE_CHANNELS: 'LOAD_MORE_CHANNELS',
27
- SET_SEARCH_QUERY: 'SET_SEARCH_QUERY',
28
- };
29
-
30
- const BaseState = {
31
- Idle: 'idle',
32
- Error: 'error',
33
- Loading: 'loading',
34
- Done: 'done',
35
- FetchChannels: 'fetchChannels',
36
- };
37
-
38
- const MainState = {
39
- RefreshChannels: 'refreshChannels',
40
- SelectChannel: 'selectChannel',
41
- LoadMoreChannels: 'loadMoreChannels',
42
- };
43
13
 
44
14
  export interface InboxProps {
45
15
  channelFilters?: Record<string, unknown>;
@@ -47,187 +17,6 @@ export interface InboxProps {
47
17
  supportServices: boolean;
48
18
  }
49
19
 
50
- // Create a safer version of useMachine to handle potential errors
51
- function useSafeMachine(machine) {
52
- // Define the state type
53
- interface SafeStateType {
54
- context: {
55
- channels: any[];
56
- refreshing: boolean;
57
- loading: boolean;
58
- error: string | null;
59
- searchQuery: string;
60
- selectedChannelId: string | null;
61
- channelRole: string | null;
62
- channelFilters: Record<string, any>;
63
- supportServices: boolean;
64
- page: number;
65
- hasMoreChannels: boolean;
66
- loadingMore: boolean;
67
- };
68
- value: string;
69
- matches?: (stateValue: string) => boolean;
70
- }
71
-
72
- // Initialize with default state
73
- const [state, setState] = useState<SafeStateType>({
74
- context: {
75
- channels: [],
76
- refreshing: false,
77
- loading: false,
78
- error: null,
79
- searchQuery: '',
80
- selectedChannelId: null,
81
- channelRole: null,
82
- channelFilters: {},
83
- supportServices: false,
84
- page: 1,
85
- hasMoreChannels: true,
86
- loadingMore: false,
87
- },
88
- value: 'idle',
89
- });
90
-
91
- // Create a safe send function
92
- const send = useCallback((event) => {
93
- try {
94
- // Log event for debugging
95
- console.log('Event received:', event.type);
96
-
97
- // Handle specific events manually
98
- if (event.type === Actions.INITIAL_CONTEXT) {
99
- setState((prev) => ({
100
- ...prev,
101
- context: {
102
- ...prev.context,
103
- channelRole: event.data?.channelRole || null,
104
- channelFilters: event.data?.channelFilters || {},
105
- supportServices: event.data?.supportServices || false,
106
- selectedChannelId: event.data?.selectedChannelId || null,
107
- loading: true,
108
- page: 1,
109
- hasMoreChannels: true,
110
- },
111
- value: BaseState.FetchChannels,
112
- }));
113
- } else if (event.type === Actions.FETCH_CHANNELS) {
114
- console.log('Setting channels:', event.data?.channels?.length || 0);
115
- setState((prev) => ({
116
- ...prev,
117
- context: {
118
- ...prev.context,
119
- channels: event.data?.channels || [],
120
- hasMoreChannels: (event.data?.channels?.length || 0) > 0,
121
- loading: event.data?.stopLoading ? false : prev.context.loading,
122
- refreshing: event.data?.stopLoading ? false : prev.context.refreshing,
123
- loadingMore: false,
124
- },
125
- value: BaseState.Idle,
126
- }));
127
- } else if (event.type === Actions.APPEND_CHANNELS) {
128
- const newChannels = event.data?.channels || [];
129
- console.log('Appending channels:', newChannels.length);
130
-
131
- setState((prev) => ({
132
- ...prev,
133
- context: {
134
- ...prev.context,
135
- channels: [...prev.context.channels, ...newChannels],
136
- hasMoreChannels: newChannels.length >= 10, // If we got fewer than 10 channels, assume no more are available
137
- page: prev.context.page + 1,
138
- loadingMore: false,
139
- },
140
- value: BaseState.Idle,
141
- }));
142
- } else if (event.type === Actions.REFRESH_CHANNELS) {
143
- setState((prev) => ({
144
- ...prev,
145
- context: {
146
- ...prev.context,
147
- refreshing: true,
148
- page: 1,
149
- hasMoreChannels: true,
150
- },
151
- value: MainState.RefreshChannels,
152
- }));
153
- } else if (event.type === Actions.SELECT_CHANNEL) {
154
- setState((prev) => ({
155
- ...prev,
156
- context: {
157
- ...prev.context,
158
- selectedChannelId: event.data?.channelId || null,
159
- },
160
- }));
161
- } else if (event.type === Actions.START_LOADING) {
162
- setState((prev) => ({
163
- ...prev,
164
- context: {
165
- ...prev.context,
166
- loading: true,
167
- },
168
- }));
169
- } else if (event.type === Actions.STOP_LOADING) {
170
- console.log('Explicitly stopping loading state');
171
- setState((prev) => ({
172
- ...prev,
173
- context: {
174
- ...prev.context,
175
- loading: false,
176
- refreshing: false,
177
- loadingMore: false,
178
- },
179
- value: prev.value === BaseState.FetchChannels ? BaseState.Idle : prev.value,
180
- }));
181
- } else if (event.type === Actions.LOAD_MORE_CHANNELS) {
182
- setState((prev) => ({
183
- ...prev,
184
- context: {
185
- ...prev.context,
186
- loadingMore: true,
187
- },
188
- value: MainState.LoadMoreChannels,
189
- }));
190
- } else if (event.type === Actions.SET_SEARCH_QUERY) {
191
- setState((prev) => ({
192
- ...prev,
193
- context: {
194
- ...prev.context,
195
- searchQuery: event.data?.searchQuery || '',
196
- },
197
- }));
198
- } else if (event.type === Actions.ERROR_HANDLED) {
199
- console.log('Error handled:', event.data?.message);
200
- setState((prev) => ({
201
- ...prev,
202
- context: {
203
- ...prev.context,
204
- error: event.data?.message || null,
205
- loading: false,
206
- refreshing: false,
207
- loadingMore: false,
208
- },
209
- value: BaseState.Idle,
210
- }));
211
- }
212
- } catch (error) {
213
- console.error('Error handling event:', error);
214
- }
215
- }, []);
216
-
217
- // Add a custom matches function to the state
218
- const stateWithMatches = useMemo(() => {
219
- return {
220
- ...state,
221
- matches: (checkState) => {
222
- return state.value === checkState;
223
- },
224
- };
225
- }, [state]);
226
-
227
- // Return as a tuple to match useMachine API
228
- return [stateWithMatches, send] as const;
229
- }
230
-
231
20
  const DialogsComponent = (props: InboxProps) => {
232
21
  const { channelFilters: channelFilterProp, channelRole, supportServices } = props;
233
22
  const channelFilters = { ...channelFilterProp };
@@ -236,106 +25,27 @@ const DialogsComponent = (props: InboxProps) => {
236
25
  const auth = useSelector(userSelector);
237
26
  const navigation = useNavigation<any>();
238
27
 
28
+ // Local state for UI control
29
+ const [searchQuery, setSearchQuery] = useState('');
30
+ const [selectedChannelId, setSelectedChannelId] = useState<string | null>(params?.channelId || null);
31
+ const [page, setPage] = useState(1);
32
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
33
+
239
34
  // Create a ref to track if component is mounted
240
35
  const isMountedRef = useRef(true);
241
-
242
- // Use our safer custom implementation instead of the problematic useMachine
243
- const [state, send] = useSafeMachine(dialogsXstate);
244
-
245
- // Define safe functions first to avoid "used before declaration" errors
246
- const safeContext = useCallback(() => {
247
- try {
248
- return state?.context || {};
249
- } catch (error) {
250
- console.error('Error accessing state.context:', error);
251
- return {};
252
- }
253
- }, [state]);
254
-
255
- const safeContextProperty = useCallback(
256
- (property, defaultValue = null) => {
257
- try {
258
- return state?.context?.[property] ?? defaultValue;
259
- } catch (error) {
260
- console.error(`Error accessing state.context.${property}:`, error);
261
- return defaultValue;
262
- }
263
- },
264
- [state],
265
- );
266
-
267
- const safeMatches = useCallback(
268
- (stateValue) => {
269
- try {
270
- return state?.matches?.(stateValue) || false;
271
- } catch (error) {
272
- console.error(`Error calling state.matches with ${stateValue}:`, error);
273
- return false;
274
- }
275
- },
276
- [state],
277
- );
278
-
279
- const safeSend = useCallback(
280
- (event) => {
281
- try {
282
- send(event);
283
- } catch (error) {
284
- console.error('Error sending event to state machine:', error, event);
285
- }
286
- },
287
- [send],
288
- );
289
-
290
- // Destructure context properties with safe getters
291
- const channels = safeContextProperty('channels', []);
292
- const refreshing = safeContextProperty('refreshing', false);
293
- const loading = safeContextProperty('loading', false);
294
- const searchQuery = safeContextProperty('searchQuery', '');
295
- const selectedChannelId = safeContextProperty('selectedChannelId', null);
296
- const loadingMore = safeContextProperty('loadingMore', false);
297
- const hasMoreChannels = safeContextProperty('hasMoreChannels', true);
298
- const page = safeContextProperty('page', 1);
299
-
300
- // Use a ref to track the current machine snapshot for safer access
301
- const stateRef = useRef(state);
302
-
303
- // Keep the ref updated with the latest snapshot
304
- useEffect(() => {
305
- stateRef.current = state;
306
- }, [state]);
307
-
308
- // Avoid referencing state.context directly in places that might cause undefined errors
309
- const safeGetContext = useCallback(() => {
310
- if (stateRef.current && stateRef.current.context) {
311
- return stateRef.current.context;
312
- }
313
- // Return default values if context is undefined
314
- return {
315
- channels: [],
316
- refreshing: false,
317
- loading: false,
318
- error: null,
319
- searchQuery: '',
320
- selectedChannelId: null,
321
- channelRole: null,
322
- channelFilters: {},
323
- supportServices: false,
324
- page: 1,
325
- hasMoreChannels: true,
326
- loadingMore: false,
327
- };
328
- }, []);
329
-
330
- // Use cleanup function to prevent setting state after unmount
331
- useEffect(() => {
332
- return () => {
333
- isMountedRef.current = false;
334
- };
335
- }, []);
336
-
337
- // Apollo query for fetching channels
338
- const { refetch: getChannelsRefetch } = useGetChannelsByUserWithServiceChannelsQuery({
36
+ const focusRefreshRef = useRef<number | null>(null);
37
+ const lastRefreshTimeRef = useRef(Date.now());
38
+ const MIN_REFRESH_INTERVAL = 2000;
39
+
40
+ // Add lastNavigationTimestamp to track when the user navigates away
41
+ const lastNavigationTimestamp = useRef(0);
42
+ // Track active channel to prevent duplicate clicks on the same channel
43
+ const activeChannelRef = useRef<string | null>(null);
44
+ // Hold a timeout ref to reset active channel status
45
+ const resetActiveChannelTimeoutRef = useRef<NodeJS.Timeout | null>(null);
46
+
47
+ // Apollo query with pagination and optimistic updates
48
+ const { data, loading, refetch, fetchMore, subscribeToMore } = useGetChannelsByUserWithServiceChannelsQuery({
339
49
  variables: {
340
50
  role: channelRole,
341
51
  criteria: channelFilters,
@@ -349,320 +59,328 @@ const DialogsComponent = (props: InboxProps) => {
349
59
  fetchPolicy: 'cache-and-network',
350
60
  nextFetchPolicy: 'network-only',
351
61
  notifyOnNetworkStatusChange: true,
352
- skip: true, // Skip automatic fetching as we'll control it via the state machine
353
62
  });
354
63
 
355
- // Fetch channels implementation
356
- const fetchChannelsDirectly = useCallback(
357
- async (pageNum = 1, append = false) => {
358
- try {
359
- const context = safeGetContext();
360
- console.log(`💫 FETCHING channels (page: ${pageNum}, append: ${append})`);
361
-
362
- // Calculate skip based on page number (pagination)
363
- const skipCount = (pageNum - 1) * 15;
364
-
365
- // Add timeout to prevent hanging requests
366
- const fetchPromise = getChannelsRefetch({
367
- role: channelRole,
368
- criteria: channelFilters,
369
- supportServices: supportServices ? true : false,
370
- supportServiceCriteria: {
371
- type: RoomType.Service,
372
- },
373
- limit: 15,
374
- skip: skipCount,
375
- });
376
-
377
- // Set a timeout to abort long-running requests
378
- const timeoutPromise = new Promise((_, reject) =>
379
- setTimeout(() => reject(new Error('Request timeout')), 8000),
380
- );
381
-
382
- // Race the fetch against the timeout
383
- const result = (await Promise.race([fetchPromise, timeoutPromise])) as any;
384
- const data = result?.data || {};
385
-
386
- const allChannels = [...(data?.supportServiceChannels ?? []), ...(data?.channelsByUser ?? [])];
387
-
388
- // Optimize filtering by using more efficient approach
389
- const filteredChannels =
390
- allChannels?.filter((c) => {
391
- if (!c || !c.members) return false;
392
-
393
- // Early return pattern for better performance
394
- for (const member of c.members) {
395
- if (
396
- member &&
397
- member.user &&
398
- member.user.id !== auth?.id &&
399
- member.user.__typename === 'UserAccount'
400
- ) {
401
- return true;
402
- }
403
- }
404
- return false;
405
- }) ?? [];
406
-
407
- // Use more efficient sorting
408
- const sortedChannels =
409
- filteredChannels.sort((a, b) => {
410
- const dateA = new Date(a.updatedAt);
411
- const dateB = new Date(b.updatedAt);
412
- return dateB.getTime() - dateA.getTime();
413
- }) || [];
414
-
415
- console.log(`📊 Processed channels: ${sortedChannels.length} (page: ${pageNum}, skip: ${skipCount})`);
416
-
417
- if (isMountedRef.current) {
418
- if (append) {
419
- safeSend({
420
- type: Actions.APPEND_CHANNELS,
421
- data: { channels: sortedChannels },
422
- });
423
- } else {
424
- safeSend({
425
- type: Actions.FETCH_CHANNELS,
426
- data: { channels: sortedChannels, stopLoading: true },
427
- });
64
+ // Process the channels from the query response
65
+ const processChannels = useCallback(
66
+ (rawChannels = []) => {
67
+ if (!rawChannels || !rawChannels.length) return [];
68
+
69
+ // Filter out channels without valid members
70
+ const filteredChannels = rawChannels.filter((c) => {
71
+ if (!c || !c.members) return false;
72
+
73
+ // Early return pattern for better performance
74
+ for (const member of c.members) {
75
+ if (
76
+ member &&
77
+ member.user &&
78
+ member.user.id !== auth?.id &&
79
+ member.user.__typename === 'UserAccount'
80
+ ) {
81
+ return true;
428
82
  }
429
-
430
- // No need for another stop loading call as we included stopLoading: true above
431
83
  }
432
- } catch (error) {
433
- console.error('Error fetching channels:', error);
434
- if (isMountedRef.current) {
435
- safeSend({
436
- type: Actions.ERROR_HANDLED,
437
- data: { message: 'Failed to fetch channels' },
438
- });
84
+ return false;
85
+ });
86
+
87
+ // Process channels to ensure lastMessage property is properly structured
88
+ return filteredChannels.map((channel) => {
89
+ // If channel has a lastMessage, ensure it's properly formatted
90
+ if (channel.lastMessage) {
91
+ return {
92
+ ...channel,
93
+ lastMessage: {
94
+ ...channel.lastMessage,
95
+ // Ensure these essential properties exist
96
+ id: channel.lastMessage.id,
97
+ message: channel.lastMessage.message,
98
+ createdAt: channel.lastMessage.createdAt || channel.lastMessage.updatedAt,
99
+ updatedAt: channel.lastMessage.updatedAt || channel.lastMessage.createdAt,
100
+ userId: channel.lastMessage.userId,
101
+ channelId: channel.lastMessage.channelId || channel.id,
102
+ },
103
+ };
439
104
  }
440
- }
105
+ return channel;
106
+ });
441
107
  },
442
- [getChannelsRefetch, channelRole, channelFilters, supportServices, auth?.id, safeSend],
108
+ [auth?.id],
443
109
  );
444
110
 
445
- // Optimize safety timeout to use a shorter duration
111
+ // Sort channels by most recent activity
112
+ const sortChannels = useCallback((channels) => {
113
+ if (!channels || !channels.length) return [];
114
+
115
+ return [...channels].sort((a, b) => {
116
+ const dateA = new Date(a?.updatedAt || a?.createdAt).getTime();
117
+ const dateB = new Date(b?.updatedAt || b?.createdAt).getTime();
118
+ return dateB - dateA; // Newest first
119
+ });
120
+ }, []);
121
+
122
+ // Combine data from both channel types
123
+ const allChannels = [...(data?.supportServiceChannels || []), ...(data?.channelsByUser || [])];
124
+
125
+ // Process and sort the channels
126
+ const channels = sortChannels(processChannels(allChannels));
127
+
128
+ // Set up subscription for real-time message updates
446
129
  useEffect(() => {
447
- if (loading) {
448
- const safetyTimeout = setTimeout(() => {
449
- console.log('⚠️ Safety timeout triggered - forcing loading state to stop');
450
- if (isMountedRef.current) {
451
- safeSend({ type: Actions.STOP_LOADING });
452
- }
453
- }, 3000); // Reduced from 5000 to 3000 ms
130
+ if (!auth || !auth.id) return;
454
131
 
455
- return () => clearTimeout(safetyTimeout);
456
- }
457
- }, [loading, safeSend]);
132
+ console.log('📱 Setting up global message subscription for dialog updates');
458
133
 
459
- // Add a faster refresh function with smaller dataset and timeout
460
- const fastRefresh = useCallback(async () => {
461
- try {
462
- console.log('🔄 Fast refreshing channels...');
134
+ const unsubscribe = subscribeToMore({
135
+ document: OnChatMessageAddedDocument,
136
+ variables: {}, // No specific channel ID - we'll handle all messages
137
+ updateQuery: (prev, { subscriptionData }) => {
138
+ try {
139
+ if (!subscriptionData.data || !isMountedRef.current) return prev;
463
140
 
464
- // Set a timeout to ensure refreshing state is cleared if the fetch fails
465
- const clearRefreshingTimeout = setTimeout(() => {
466
- if (isMountedRef.current) {
467
- console.log('⚠️ Fast refresh timeout - stopping refresh state');
468
- safeSend({ type: Actions.STOP_LOADING });
469
- }
470
- }, 3000); // 3 second timeout for refresh
141
+ const subData = subscriptionData.data as any;
142
+ const newMessage = subData.chatMessageAdded;
471
143
 
472
- // Perform the fetch with a smaller limit for faster results
473
- const { data } = await getChannelsRefetch({
474
- role: channelRole,
475
- criteria: channelFilters,
476
- supportServices: supportServices ? true : false,
477
- supportServiceCriteria: {
478
- type: RoomType.Service,
479
- },
480
- limit: 10,
481
- skip: 0,
482
- });
144
+ console.log('📱 Dialog subscription received message update:', newMessage?.id);
483
145
 
484
- // Cancel the timeout since we got a response
485
- clearTimeout(clearRefreshingTimeout);
486
-
487
- if (!isMountedRef.current) return;
488
-
489
- const allChannels = [...(data?.supportServiceChannels ?? []), ...(data?.channelsByUser ?? [])];
490
- const filteredChannels =
491
- allChannels?.filter((c) =>
492
- c.members.some((u) => u !== null && u?.user?.id != auth?.id && u.user.__typename == 'UserAccount'),
493
- ) ?? [];
494
- const sortedChannels = (filteredChannels && orderBy(filteredChannels, ['updatedAt'], ['desc'])) || [];
495
-
496
- console.log(`📊 Fast refresh completed: ${sortedChannels.length} channels`);
497
-
498
- // Update the state with the new channels, but only if still mounted
499
- if (isMountedRef.current) {
500
- // Use a single update to prevent UI jumping
501
- safeSend({
502
- type: Actions.FETCH_CHANNELS,
503
- data: {
504
- channels: sortedChannels,
505
- stopLoading: true,
506
- },
507
- });
508
- }
509
- } catch (error) {
510
- console.error('Error during fast refresh:', error);
511
- if (isMountedRef.current) {
512
- safeSend({ type: Actions.STOP_LOADING });
513
- }
514
- }
515
- }, [getChannelsRefetch, channelRole, channelFilters, supportServices, auth?.id, safeSend]);
146
+ // Skip if no message or no channelId
147
+ if (!newMessage || !newMessage.channelId) return prev;
516
148
 
517
- // Process state changes and execute side effects
518
- useEffect(() => {
519
- // Only execute if not already refreshing or loading to prevent loops
520
- const context = safeGetContext();
521
- const isAlreadyFetching = context.refreshing || context.loading;
522
-
523
- if (!isAlreadyFetching) {
524
- if (safeMatches(BaseState.FetchChannels)) {
525
- console.log('🔄 Fetching channels...');
526
- fetchChannelsDirectly(1, false);
527
- } else if (safeMatches(MainState.RefreshChannels)) {
528
- console.log('🔄 Refreshing channels...');
529
- fetchChannelsDirectly(1, false);
530
- } else if (safeMatches(MainState.LoadMoreChannels)) {
531
- console.log('🔄 Loading more channels...');
532
- fetchChannelsDirectly(page, true);
533
- }
534
- } else {
535
- // Log that we're skipping the fetch due to already being in progress
536
- console.log('⏩ Skipping fetch because isAlreadyFetching:', isAlreadyFetching);
537
- }
538
- }, [fetchChannelsDirectly, safeMatches, safeGetContext, state.value, page]);
149
+ // Find the channel this message belongs to
150
+ const channelId = newMessage.channelId.toString();
539
151
 
540
- // Add a debug log to track state transitions
541
- useEffect(() => {
542
- console.log('State changed to:', state.value);
543
- console.log(
544
- 'Context:',
545
- JSON.stringify({
546
- channelsCount: channels.length,
547
- loading,
548
- refreshing,
549
- }),
550
- );
551
- }, [state.value, channels.length, loading, refreshing]);
552
-
553
- // Initialize state machine with props on mount
554
- useEffect(() => {
555
- if (isMountedRef.current) {
556
- console.log('🚀 Initializing state machine with props', {
557
- channelRole,
558
- channelFilters,
559
- supportServices,
560
- selectedChannelId: params?.channelId,
561
- });
152
+ // Find which array contains this channel (direct or service)
153
+ let foundInDirectChannels = false;
154
+ let foundInServiceChannels = false;
562
155
 
563
- safeSend({
564
- type: Actions.INITIAL_CONTEXT,
565
- data: {
566
- channelRole,
567
- channelFilters,
568
- supportServices,
569
- selectedChannelId: params?.channelId,
570
- },
571
- });
156
+ // Check if this channel exists in direct channels
157
+ const directChannelIndex = prev.channelsByUser?.findIndex((c) => c.id.toString() === channelId);
158
+ if (directChannelIndex !== undefined && directChannelIndex >= 0) {
159
+ foundInDirectChannels = true;
160
+ }
161
+
162
+ // Check if this channel exists in service channels
163
+ const serviceChannelIndex = prev.supportServiceChannels?.findIndex(
164
+ (c) => c.id.toString() === channelId,
165
+ );
166
+ if (serviceChannelIndex !== undefined && serviceChannelIndex >= 0) {
167
+ foundInServiceChannels = true;
168
+ }
169
+
170
+ // Create a deep copy of the previous state to avoid mutating it
171
+ const result = {
172
+ ...prev,
173
+ channelsByUser: [...(prev.channelsByUser || [])],
174
+ supportServiceChannels: [...(prev.supportServiceChannels || [])],
175
+ };
176
+
177
+ // Optimistically update the channel with the new message
178
+ if (foundInDirectChannels && directChannelIndex >= 0) {
179
+ // Update the direct channel
180
+ const channel = { ...result.channelsByUser[directChannelIndex] } as any;
572
181
 
573
- // Add a safety measure to ensure loading is stopped even if fetch fails
574
- const initSafetyTimeout = setTimeout(() => {
575
- if (isMountedRef.current && loading) {
576
- console.log('⚠️ Init safety timeout triggered - forcing loading state to stop');
577
- safeSend({ type: Actions.STOP_LOADING });
182
+ // Update lastMessage
183
+ channel.lastMessage = newMessage;
184
+
185
+ // Update timestamp to move to top of sorted list
186
+ channel.updatedAt = newMessage.createdAt || new Date().toISOString();
187
+
188
+ // Replace the channel in the array
189
+ result.channelsByUser[directChannelIndex] = channel;
190
+ }
191
+
192
+ if (foundInServiceChannels && serviceChannelIndex >= 0) {
193
+ // Update the service channel
194
+ const channel = { ...result.supportServiceChannels[serviceChannelIndex] } as any;
195
+
196
+ // Update lastMessage
197
+ channel.lastMessage = newMessage;
198
+
199
+ // Update timestamp to move to top of sorted list
200
+ channel.updatedAt = newMessage.createdAt || new Date().toISOString();
201
+
202
+ // Replace the channel in the array
203
+ result.supportServiceChannels[serviceChannelIndex] = channel;
204
+ }
205
+
206
+ return result;
207
+ } catch (error) {
208
+ console.error('Error in dialog subscription handler:', error);
209
+ return prev;
578
210
  }
579
- }, 8000); // 8 seconds safety timeout
211
+ },
212
+ });
580
213
 
581
- return () => clearTimeout(initSafetyTimeout);
582
- }
583
- }, []);
214
+ // Clean up subscription when component unmounts
215
+ return () => {
216
+ console.log('📱 Cleaning up dialog message subscription');
217
+ unsubscribe();
218
+ };
219
+ }, [auth?.id, subscribeToMore]);
584
220
 
585
- // Handle refresh on focus (when navigating back to this screen)
586
- const focusRefreshRef = useRef<number | null>(null);
221
+ // Handle component cleanup
222
+ useEffect(() => {
223
+ return () => {
224
+ isMountedRef.current = false;
225
+ // Clear any active timeouts
226
+ if (resetActiveChannelTimeoutRef.current) {
227
+ clearTimeout(resetActiveChannelTimeoutRef.current);
228
+ }
229
+ };
230
+ }, []);
587
231
 
232
+ // Reset activeChannelRef when returning to this screen
588
233
  useFocusEffect(
589
234
  useCallback(() => {
590
- // Use a flag to ensure we only trigger refresh once per focus
591
- let hasTriggeredRefresh = false;
592
-
593
- // Reset the focus refresh tracking when component gets focus
235
+ // When screen gains focus, check if we're coming back from a detail screen
594
236
  const now = Date.now();
595
- const lastRefresh = focusRefreshRef.current;
596
-
597
- // Only refresh if at least 2 seconds have passed since last refresh
598
- const shouldRefresh = lastRefresh === null || now - lastRefresh > 2000;
599
-
600
- // Only refresh if component is mounted and not in initial state,
601
- // and not already refreshing/loading
602
- const context = safeGetContext();
603
- const isAlreadyFetching = context.refreshing || context.loading;
604
-
605
- if (
606
- isMountedRef.current &&
607
- !hasTriggeredRefresh &&
608
- !isAlreadyFetching &&
609
- shouldRefresh &&
610
- (channels.length > 0 || safeMatches(BaseState.Idle))
611
- ) {
612
- hasTriggeredRefresh = true;
613
- focusRefreshRef.current = now;
614
-
615
- // Use fast refresh for better performance
616
- console.log('🔄 Focus effect: triggering fast refresh');
617
- safeSend({
618
- type: Actions.START_LOADING,
619
- data: { refreshing: true },
620
- });
621
-
622
- // Execute fast refresh with a short delay to prevent UI jank
623
- setTimeout(() => {
624
- if (isMountedRef.current) {
625
- fastRefresh();
626
- }
627
- }, 100);
628
- } else {
629
- console.log('⏩ Skipping focus refresh:', {
630
- isAlreadyFetching,
631
- hasTriggeredRefresh,
632
- shouldRefresh,
633
- timeGap: lastRefresh === null ? 'first refresh' : now - lastRefresh,
634
- });
237
+
238
+ // Reset active channel ref if enough time has passed since last navigation
239
+ if (now - lastNavigationTimestamp.current > 300) {
240
+ activeChannelRef.current = null;
241
+ console.log('Reset active channel reference on focus');
635
242
  }
636
243
 
637
244
  return () => {
638
- // Reset flag when focus is lost
639
- hasTriggeredRefresh = false;
245
+ // When losing focus, update the timestamp
246
+ lastNavigationTimestamp.current = Date.now();
247
+ };
248
+ }, []),
249
+ );
250
+
251
+ // Handle refresh on focus
252
+ useFocusEffect(
253
+ useCallback(() => {
254
+ console.log('📱 Focus effect triggered for Dialogs screen');
255
+
256
+ // Refresh when returning to the screen if enough time has passed
257
+ const performRefresh = () => {
258
+ const now = Date.now();
259
+ if (now - lastRefreshTimeRef.current < MIN_REFRESH_INTERVAL) {
260
+ console.log('⏩ Skipping refresh: too soon after previous refresh');
261
+ return;
262
+ }
263
+
264
+ console.log('🔄 Performing refresh on screen focus');
265
+ if (isMountedRef.current) {
266
+ lastRefreshTimeRef.current = now;
267
+ refetch();
268
+ }
640
269
  };
641
- }, [safeSend, channels.length, safeMatches, safeGetContext, fastRefresh]),
270
+
271
+ const focusRefreshTimeout = setTimeout(performRefresh, 100);
272
+ return () => clearTimeout(focusRefreshTimeout);
273
+ }, [refetch]),
642
274
  );
643
275
 
644
- // Navigation handlers
276
+ // Handle pull-to-refresh
277
+ const handlePullToRefresh = useCallback(() => {
278
+ const now = Date.now();
279
+ focusRefreshRef.current = now;
280
+
281
+ console.log('🔄 Pull-to-refresh triggered');
282
+ refetch();
283
+ }, [refetch]);
284
+
285
+ // Load more channels
286
+ const handleLoadMore = useCallback(() => {
287
+ if (isLoadingMore || !data || channels.length < 10) {
288
+ console.log('Skip loading more: already loading or all data loaded');
289
+ return;
290
+ }
291
+
292
+ console.log('Loading more channels at page:', page + 1);
293
+ setIsLoadingMore(true);
294
+
295
+ fetchMore({
296
+ variables: {
297
+ skip: page * 15,
298
+ },
299
+ updateQuery: (prev, { fetchMoreResult }) => {
300
+ setIsLoadingMore(false);
301
+ setPage((prevPage) => prevPage + 1);
302
+
303
+ if (!fetchMoreResult) return prev;
304
+
305
+ // Combine previous and new results
306
+ return {
307
+ ...fetchMoreResult,
308
+ channelsByUser: [...(prev.channelsByUser || []), ...(fetchMoreResult.channelsByUser || [])],
309
+ supportServiceChannels: [
310
+ ...(prev.supportServiceChannels || []),
311
+ ...(fetchMoreResult.supportServiceChannels || []),
312
+ ],
313
+ };
314
+ },
315
+ }).catch((error) => {
316
+ console.error('Error loading more channels:', error);
317
+ setIsLoadingMore(false);
318
+ });
319
+ }, [fetchMore, isLoadingMore, data, channels.length, page]);
320
+
321
+ // Navigation handlers with debounce to prevent double taps
645
322
  const handleSelectChannel = useCallback(
646
323
  (id, title) => {
647
- // Always update the selected channel ID, even if it's the same channel
648
- safeSend({ type: Actions.SELECT_CHANNEL, data: { channelId: id } });
324
+ // Return early if this channel is already active (prevents double navigation)
325
+ if (activeChannelRef.current === id) {
326
+ console.log('📱 Ignoring repeated tap on channel:', id);
327
+ return;
328
+ }
329
+
330
+ // Set this channel as active
331
+ activeChannelRef.current = id;
332
+
333
+ // Clear any existing timeout
334
+ if (resetActiveChannelTimeoutRef.current) {
335
+ clearTimeout(resetActiveChannelTimeoutRef.current);
336
+ }
337
+
338
+ // Set a timeout to clear the active channel after 2 seconds
339
+ // This prevents the active state from getting stuck if navigation fails
340
+ resetActiveChannelTimeoutRef.current = setTimeout(() => {
341
+ activeChannelRef.current = null;
342
+ }, 2000);
343
+
344
+ setSelectedChannelId(id);
345
+
346
+ console.log('📱 Navigating to channel:', id);
649
347
 
650
- // Force navigation to the channel screen, even if it's already selected
651
- // This ensures we can reopen the same channel multiple times
652
348
  navigation.navigate(config.INBOX_MESSEGE_PATH, {
653
349
  channelId: id,
654
350
  role: channelRole,
655
351
  title: title,
656
352
  hideTabBar: true,
657
- timestamp: new Date().getTime(), // Add timestamp to force a refresh when navigating to the same screen
353
+ timestamp: new Date().getTime(),
658
354
  });
659
355
  },
660
- [navigation, channelRole, safeSend],
356
+ [navigation, channelRole],
661
357
  );
662
358
 
663
359
  const handleSelectServiceChannel = useCallback(
664
360
  (id, title, postParentId) => {
665
- safeSend({ type: Actions.SELECT_CHANNEL, data: { channelId: id } });
361
+ // Return early if this channel is already active (prevents double navigation)
362
+ if (activeChannelRef.current === id) {
363
+ console.log('📱 Ignoring repeated tap on service channel:', id);
364
+ return;
365
+ }
366
+
367
+ // Set this channel as active
368
+ activeChannelRef.current = id;
369
+
370
+ // Clear any existing timeout
371
+ if (resetActiveChannelTimeoutRef.current) {
372
+ clearTimeout(resetActiveChannelTimeoutRef.current);
373
+ }
374
+
375
+ // Set a timeout to clear the active channel after 2 seconds
376
+ resetActiveChannelTimeoutRef.current = setTimeout(() => {
377
+ activeChannelRef.current = null;
378
+ }, 2000);
379
+
380
+ setSelectedChannelId(id);
381
+
382
+ console.log('📱 Navigating to service channel:', id);
383
+
666
384
  navigation.navigate(postParentId || postParentId === 0 ? config.THREAD_MESSEGE_PATH : config.THREADS_PATH, {
667
385
  channelId: id,
668
386
  role: channelRole,
@@ -671,61 +389,57 @@ const DialogsComponent = (props: InboxProps) => {
671
389
  hideTabBar: true,
672
390
  });
673
391
  },
674
- [navigation, channelRole, safeSend],
392
+ [navigation, channelRole],
675
393
  );
676
394
 
677
- // Modified pull-to-refresh handler to use fast refresh
678
- const handlePullToRefresh = useCallback(() => {
679
- if (refreshing) {
680
- console.log('⏩ Skipping refresh because already refreshing');
681
- return;
682
- }
395
+ // Handle search query changes
396
+ const handleSearchChange = useCallback((text: string) => {
397
+ setSearchQuery(text);
398
+ }, []);
683
399
 
684
- // Update the last refresh timestamp to prevent simultaneous focus refresh
685
- const now = Date.now();
686
- focusRefreshRef.current = now;
400
+ // Filter channels by search query
401
+ const filteredChannels = useCallback(() => {
402
+ if (!searchQuery.trim()) return channels;
687
403
 
688
- console.log('🔄 Pull-to-refresh triggered');
689
- safeSend({
690
- type: Actions.START_LOADING,
691
- data: { refreshing: true },
692
- });
404
+ const query = searchQuery.toLowerCase();
405
+ return channels.filter((channel) => {
406
+ // Check if the channel title contains the search query
407
+ if (channel.title && channel.title.toLowerCase().includes(query)) {
408
+ return true;
409
+ }
693
410
 
694
- // Use the fast refresh approach for pull-to-refresh
695
- fastRefresh();
696
- }, [safeSend, refreshing, fastRefresh]);
411
+ // Check if any member's name contains the search query
412
+ if (channel.members) {
413
+ for (const member of channel.members) {
414
+ const user = member?.user;
415
+ if (!user) continue;
697
416
 
698
- // Search handler
699
- const handleSearchChange = useCallback(
700
- (text: string) => {
701
- safeSend({
702
- type: Actions.SET_SEARCH_QUERY,
703
- data: { searchQuery: text },
704
- });
705
- },
706
- [safeSend],
707
- );
417
+ const fullName = `${user.givenName || ''} ${user.familyName || ''}`.toLowerCase();
418
+ if (fullName.includes(query)) {
419
+ return true;
420
+ }
708
421
 
709
- // Add loadMore handler with debounce to prevent multiple calls
710
- const handleLoadMore = useCallback(() => {
711
- if (!loadingMore && hasMoreChannels) {
712
- console.log('Loading more channels at page:', page + 1);
713
- safeSend({ type: Actions.LOAD_MORE_CHANNELS });
714
- } else {
715
- console.log('Skip loading more: loadingMore=', loadingMore, 'hasMoreChannels=', hasMoreChannels);
716
- }
717
- }, [safeSend, loadingMore, hasMoreChannels, page]);
422
+ if (user.username && user.username.toLowerCase().includes(query)) {
423
+ return true;
424
+ }
425
+ }
426
+ }
427
+
428
+ return false;
429
+ });
430
+ }, [channels, searchQuery]);
431
+
432
+ const displayChannels = filteredChannels();
718
433
 
719
434
  return (
720
435
  <Box className="p-2">
721
436
  <FlatList
722
- data={channels}
437
+ data={displayChannels}
723
438
  onRefresh={handlePullToRefresh}
724
- refreshing={refreshing}
439
+ refreshing={loading && !isLoadingMore}
725
440
  contentContainerStyle={{ minHeight: '100%' }}
726
441
  ItemSeparatorComponent={() => <Box className="h-0.5 bg-gray-200" />}
727
442
  renderItem={({ item: channel }) => {
728
- // Use memoized key for better list performance
729
443
  const key = `${channel.type === RoomType.Service ? 'service' : 'direct'}-${channel.id}`;
730
444
 
731
445
  return channel?.type === RoomType.Service ? (
@@ -734,7 +448,7 @@ const DialogsComponent = (props: InboxProps) => {
734
448
  onOpen={handleSelectServiceChannel}
735
449
  currentUser={auth}
736
450
  channel={channel}
737
- refreshing={refreshing}
451
+ refreshing={loading}
738
452
  selectedChannelId={selectedChannelId}
739
453
  role={channelRole}
740
454
  />
@@ -745,34 +459,29 @@ const DialogsComponent = (props: InboxProps) => {
745
459
  currentUser={auth}
746
460
  channel={channel}
747
461
  selectedChannelId={selectedChannelId}
748
- forceRefresh={false} // Change to false to avoid unnecessary refreshes
462
+ forceRefresh={true}
749
463
  />
750
464
  );
751
465
  }}
752
466
  ListFooterComponent={() =>
753
- loadingMore ? (
467
+ isLoadingMore ? (
754
468
  <Center className="py-4">
755
469
  <Spinner color={colors.blue[500]} size="small" />
756
470
  </Center>
757
471
  ) : null
758
472
  }
759
473
  onEndReached={handleLoadMore}
760
- onEndReachedThreshold={0.5} // Increased from 0.3 for earlier loading
761
- initialNumToRender={5} // Reduced from 10 for faster initial render
762
- maxToRenderPerBatch={5} // Reduced from 10 for smoother rendering
763
- windowSize={5} // Reduced from 10 to maintain fewer items in memory
474
+ onEndReachedThreshold={0.5}
475
+ initialNumToRender={5}
476
+ maxToRenderPerBatch={5}
477
+ windowSize={5}
764
478
  removeClippedSubviews={true}
765
- updateCellsBatchingPeriod={100} // Increased from 50 to batch updates better
766
- getItemLayout={(data, index) =>
767
- // Pre-calculate item dimensions for more efficient rendering
768
- ({ length: 80, offset: 80 * index, index })
769
- }
479
+ updateCellsBatchingPeriod={100}
480
+ getItemLayout={(data, index) => ({ length: 80, offset: 80 * index, index })}
770
481
  keyExtractor={(item) => `channel-${item.id}`}
771
482
  ListEmptyComponent={() => {
772
- console.log('Rendering ListEmptyComponent', { loading, refreshing, stateValue: state.value });
773
-
774
- // Only show spinner during initial loading
775
- if (loading && channels.length === 0) {
483
+ // Show spinner during initial loading
484
+ if (loading && displayChannels.length === 0) {
776
485
  return (
777
486
  <Center className="flex-1 justify-center items-center" style={{ height: 300 }}>
778
487
  <Spinner color={colors.blue[500]} size="large" />