@messenger-box/platform-mobile 10.0.3-alpha.18 → 10.0.3-alpha.20

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 (23) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/screens/inbox/components/DialogsListItem.js +423 -50
  3. package/lib/screens/inbox/components/DialogsListItem.js.map +1 -1
  4. package/lib/screens/inbox/components/ServiceDialogsListItem.js +375 -51
  5. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +1 -1
  6. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js +175 -0
  7. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js.map +1 -0
  8. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js +191 -0
  9. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js.map +1 -0
  10. package/lib/screens/inbox/containers/Dialogs.js +382 -181
  11. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  12. package/lib/screens/inbox/containers/ThreadConversationView.js +536 -48
  13. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  14. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +1 -25
  15. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +1 -1
  16. package/package.json +2 -2
  17. package/src/screens/inbox/components/DialogsListItem.tsx +624 -107
  18. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +506 -114
  19. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +35 -17
  20. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
  21. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
  22. package/src/screens/inbox/containers/Dialogs.tsx +531 -281
  23. package/src/screens/inbox/containers/ThreadConversationView.tsx +812 -37
@@ -1,28 +1,45 @@
1
- import React, { useCallback, useMemo, useEffect, useState, useRef } from 'react';
2
- import {
3
- FlatList,
4
- Box,
5
- Heading,
6
- Input,
7
- InputField,
8
- Text,
9
- Icon,
10
- Center,
11
- Spinner,
12
- } from '@admin-layout/gluestack-ui-mobile';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { FlatList, Box, Heading, Input, InputField, Text, Center, Spinner } from '@admin-layout/gluestack-ui-mobile';
13
3
  import { Ionicons } from '@expo/vector-icons';
14
- import { useSelector, useDispatch } from 'react-redux';
15
- import { useNavigation, useRoute, useIsFocused, useFocusEffect } from '@react-navigation/native';
16
- import { orderBy, uniqBy, startCase } from 'lodash-es';
4
+ import { useSelector } from 'react-redux';
5
+ import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native';
6
+ import { orderBy } from 'lodash-es';
17
7
  import { DialogsListItem } from '../components/DialogsListItem';
18
8
  import { ServiceDialogsListItem } from '../components/ServiceDialogsListItem';
19
- import { useGetChannelsByUserQuery, useGetChannelsByUserWithServiceChannelsQuery } from 'common/graphql';
9
+ import { useGetChannelsByUserWithServiceChannelsQuery } from 'common/graphql';
20
10
  import { RoomType } from 'common';
21
11
  import { userSelector } from '@adminide-stack/user-auth0-client';
22
- import { CHANGE_SETTINGS_ACTION } from '@admin-layout/client';
23
12
  import { config } from '../config';
24
13
  import colors from 'tailwindcss/colors';
25
- import { dialogsXstate, Actions as DialogsActions, BaseState, MainState } from './workflow/dialogs-xstate';
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
+ };
26
43
 
27
44
  export interface InboxProps {
28
45
  channelFilters?: Record<string, unknown>;
@@ -30,28 +47,28 @@ export interface InboxProps {
30
47
  supportServices: boolean;
31
48
  }
32
49
 
33
- // Define state type for the state machine
34
- interface SafeStateType {
35
- context: {
36
- channels: any[];
37
- refreshing: boolean;
38
- loading: boolean;
39
- error: string | null;
40
- searchQuery: string;
41
- selectedChannelId: string | null;
42
- channelRole: string | null;
43
- channelFilters: Record<string, any>;
44
- supportServices: boolean;
45
- };
46
- value: string;
47
- matches?: (stateValue: string) => boolean;
48
- }
49
-
50
- // Interface for the return type to ensure proper typing
51
- type UseSafeMachineReturnType = [SafeStateType & { matches: (stateValue: string) => boolean }, (event: any) => void];
52
-
53
50
  // Create a safer version of useMachine to handle potential errors
54
- function useSafeMachine(machine): UseSafeMachineReturnType {
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
+
55
72
  // Initialize with default state
56
73
  const [state, setState] = useState<SafeStateType>({
57
74
  context: {
@@ -64,6 +81,9 @@ function useSafeMachine(machine): UseSafeMachineReturnType {
64
81
  channelRole: null,
65
82
  channelFilters: {},
66
83
  supportServices: false,
84
+ page: 1,
85
+ hasMoreChannels: true,
86
+ loadingMore: false,
67
87
  },
68
88
  value: 'idle',
69
89
  });
@@ -72,10 +92,10 @@ function useSafeMachine(machine): UseSafeMachineReturnType {
72
92
  const send = useCallback((event) => {
73
93
  try {
74
94
  // Log event for debugging
75
- console.log('Dialogs Event received:', event.type);
95
+ console.log('Event received:', event.type);
76
96
 
77
97
  // Handle specific events manually
78
- if (event.type === DialogsActions.INITIAL_CONTEXT) {
98
+ if (event.type === Actions.INITIAL_CONTEXT) {
79
99
  setState((prev) => ({
80
100
  ...prev,
81
101
  context: {
@@ -84,36 +104,61 @@ function useSafeMachine(machine): UseSafeMachineReturnType {
84
104
  channelFilters: event.data?.channelFilters || {},
85
105
  supportServices: event.data?.supportServices || false,
86
106
  selectedChannelId: event.data?.selectedChannelId || null,
107
+ loading: true,
108
+ page: 1,
109
+ hasMoreChannels: true,
87
110
  },
88
111
  value: BaseState.FetchChannels,
89
112
  }));
90
- } else if (event.type === DialogsActions.SET_SEARCH_QUERY) {
113
+ } else if (event.type === Actions.FETCH_CHANNELS) {
114
+ console.log('Setting channels:', event.data?.channels?.length || 0);
91
115
  setState((prev) => ({
92
116
  ...prev,
93
117
  context: {
94
118
  ...prev.context,
95
- searchQuery: event.data?.searchQuery || '',
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,
96
139
  },
140
+ value: BaseState.Idle,
97
141
  }));
98
- } else if (event.type === DialogsActions.REFRESH_CHANNELS) {
142
+ } else if (event.type === Actions.REFRESH_CHANNELS) {
99
143
  setState((prev) => ({
100
144
  ...prev,
101
145
  context: {
102
146
  ...prev.context,
103
147
  refreshing: true,
148
+ page: 1,
149
+ hasMoreChannels: true,
104
150
  },
105
151
  value: MainState.RefreshChannels,
106
152
  }));
107
- } else if (event.type === DialogsActions.SELECT_CHANNEL) {
153
+ } else if (event.type === Actions.SELECT_CHANNEL) {
108
154
  setState((prev) => ({
109
155
  ...prev,
110
156
  context: {
111
157
  ...prev.context,
112
158
  selectedChannelId: event.data?.channelId || null,
113
159
  },
114
- value: MainState.SelectChannel,
115
160
  }));
116
- } else if (event.type === DialogsActions.START_LOADING) {
161
+ } else if (event.type === Actions.START_LOADING) {
117
162
  setState((prev) => ({
118
163
  ...prev,
119
164
  context: {
@@ -121,62 +166,66 @@ function useSafeMachine(machine): UseSafeMachineReturnType {
121
166
  loading: true,
122
167
  },
123
168
  }));
124
- } else if (event.type === DialogsActions.STOP_LOADING) {
169
+ } else if (event.type === Actions.STOP_LOADING) {
170
+ console.log('Explicitly stopping loading state');
125
171
  setState((prev) => ({
126
172
  ...prev,
127
173
  context: {
128
174
  ...prev.context,
129
175
  loading: false,
176
+ refreshing: false,
177
+ loadingMore: false,
130
178
  },
179
+ value: prev.value === BaseState.FetchChannels ? BaseState.Idle : prev.value,
131
180
  }));
132
- } else if (event.type === 'FETCH_CHANNELS_SUCCESS') {
181
+ } else if (event.type === Actions.LOAD_MORE_CHANNELS) {
133
182
  setState((prev) => ({
134
183
  ...prev,
135
184
  context: {
136
185
  ...prev.context,
137
- channels: event.data?.channels || [],
138
- loading: false,
139
- refreshing: false,
186
+ loadingMore: true,
140
187
  },
141
- value: BaseState.Idle,
188
+ value: MainState.LoadMoreChannels,
142
189
  }));
143
- } else if (event.type === 'REFRESH_CHANNELS_SUCCESS') {
190
+ } else if (event.type === Actions.SET_SEARCH_QUERY) {
144
191
  setState((prev) => ({
145
192
  ...prev,
146
193
  context: {
147
194
  ...prev.context,
148
- channels: event.data?.channels || [],
149
- refreshing: false,
195
+ searchQuery: event.data?.searchQuery || '',
150
196
  },
151
- value: BaseState.Idle,
152
197
  }));
153
- } else if (event.type === 'ERROR') {
198
+ } else if (event.type === Actions.ERROR_HANDLED) {
199
+ console.log('Error handled:', event.data?.message);
154
200
  setState((prev) => ({
155
201
  ...prev,
156
202
  context: {
157
203
  ...prev.context,
158
- error: event.data?.message || 'An error occurred',
204
+ error: event.data?.message || null,
159
205
  loading: false,
160
206
  refreshing: false,
207
+ loadingMore: false,
161
208
  },
162
- value: BaseState.Error,
209
+ value: BaseState.Idle,
163
210
  }));
164
211
  }
165
- } catch (err) {
166
- console.error('Error in send function:', err);
212
+ } catch (error) {
213
+ console.error('Error handling event:', error);
167
214
  }
168
215
  }, []);
169
216
 
170
- // Add a matches method for state compatibility
217
+ // Add a custom matches function to the state
171
218
  const stateWithMatches = useMemo(() => {
172
219
  return {
173
220
  ...state,
174
- matches: (value: string) => state.value === value,
221
+ matches: (checkState) => {
222
+ return state.value === checkState;
223
+ },
175
224
  };
176
225
  }, [state]);
177
226
 
178
227
  // Return as a tuple to match useMachine API
179
- return [stateWithMatches, send];
228
+ return [stateWithMatches, send] as const;
180
229
  }
181
230
 
182
231
  const DialogsComponent = (props: InboxProps) => {
@@ -185,9 +234,9 @@ const DialogsComponent = (props: InboxProps) => {
185
234
  channelFilters.type = channelFilters?.type ?? RoomType.Direct;
186
235
  const { params } = useRoute<any>();
187
236
  const auth = useSelector(userSelector);
188
- const dispatch = useDispatch();
189
237
  const navigation = useNavigation<any>();
190
- const isFocused = useIsFocused();
238
+
239
+ // Create a ref to track if component is mounted
191
240
  const isMountedRef = useRef(true);
192
241
 
193
242
  // Use our safer custom implementation instead of the problematic useMachine
@@ -238,6 +287,16 @@ const DialogsComponent = (props: InboxProps) => {
238
287
  [send],
239
288
  );
240
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
+
241
300
  // Use a ref to track the current machine snapshot for safer access
242
301
  const stateRef = useRef(state);
243
302
 
@@ -246,6 +305,28 @@ const DialogsComponent = (props: InboxProps) => {
246
305
  stateRef.current = state;
247
306
  }, [state]);
248
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
+
249
330
  // Use cleanup function to prevent setting state after unmount
250
331
  useEffect(() => {
251
332
  return () => {
@@ -253,11 +334,8 @@ const DialogsComponent = (props: InboxProps) => {
253
334
  };
254
335
  }, []);
255
336
 
256
- const {
257
- data: userChannels,
258
- loading: userChannelsLoading,
259
- refetch: getChannelsRefetch,
260
- } = useGetChannelsByUserWithServiceChannelsQuery({
337
+ // Apollo query for fetching channels
338
+ const { refetch: getChannelsRefetch } = useGetChannelsByUserWithServiceChannelsQuery({
261
339
  variables: {
262
340
  role: channelRole,
263
341
  criteria: channelFilters,
@@ -265,277 +343,449 @@ const DialogsComponent = (props: InboxProps) => {
265
343
  supportServiceCriteria: {
266
344
  type: RoomType.Service,
267
345
  },
346
+ limit: 15,
347
+ skip: 0,
268
348
  },
269
- onCompleted: (data) => {
270
- if (isMountedRef.current) {
271
- const allChannels = [...(data?.supportServiceChannels ?? []), ...(data?.channelsByUser ?? [])];
272
- safeSend({
273
- type: 'FETCH_CHANNELS_SUCCESS',
274
- data: { channels: allChannels },
275
- });
276
- }
277
- },
278
- onError: (error) => {
279
- if (isMountedRef.current) {
280
- safeSend({
281
- type: 'ERROR',
282
- data: { message: error.message },
283
- });
284
- }
285
- },
349
+ fetchPolicy: 'cache-and-network',
350
+ nextFetchPolicy: 'network-only',
351
+ notifyOnNetworkStatusChange: true,
352
+ skip: true, // Skip automatic fetching as we'll control it via the state machine
286
353
  });
287
354
 
288
- // Set initial context once on mount, but don't fetch data as useFocusEffect will handle it
289
- useEffect(() => {
290
- console.log('Setting initial context');
291
- // Only set the initial context with configuration, but don't trigger data fetching
292
- // This prevents double fetching with useFocusEffect
293
- safeSend({
294
- type: DialogsActions.INITIAL_CONTEXT,
295
- data: {
296
- channelRole,
297
- channelFilters: channelFiltersRef.current,
298
- supportServices,
299
- selectedChannelId: params?.channelId,
300
- },
301
- });
302
- // eslint-disable-next-line react-hooks/exhaustive-deps
303
- }, []); // Empty dependency array ensures this runs only once on mount
304
-
305
- // Ref to track if we've already refreshed to prevent infinite loop
306
- const hasRefreshedRef = useRef(false);
307
- // Ref to store the current channelFilters to detect real changes
308
- const channelFiltersRef = useRef(channelFilters);
309
-
310
- // Shared function to handle channel refreshing
311
- const refreshChannels = useCallback(() => {
312
- // Skip if already refreshing to prevent loops
313
- if (safeContextProperty('refreshing', false)) {
314
- console.log('Skipping refresh - already in progress');
315
- return Promise.resolve();
316
- }
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})`);
317
361
 
318
- console.log('Starting channel refresh');
362
+ // Calculate skip based on page number (pagination)
363
+ const skipCount = (pageNum - 1) * 15;
319
364
 
320
- // Start refreshing
321
- safeSend({
322
- type: DialogsActions.REFRESH_CHANNELS,
323
- });
365
+ const { data } = await getChannelsRefetch({
366
+ role: channelRole,
367
+ criteria: channelFilters,
368
+ supportServices: supportServices ? true : false,
369
+ supportServiceCriteria: {
370
+ type: RoomType.Service,
371
+ },
372
+ limit: 15,
373
+ skip: skipCount,
374
+ });
324
375
 
325
- // Use ref for filters to avoid dependency cycles
326
- const currentFilters = channelFiltersRef.current;
376
+ const allChannels = [...(data?.supportServiceChannels ?? []), ...(data?.channelsByUser ?? [])];
377
+ const filteredChannels =
378
+ allChannels?.filter((c) =>
379
+ c.members.some(
380
+ (u) => u !== null && u?.user?.id != auth?.id && u.user.__typename == 'UserAccount',
381
+ ),
382
+ ) ?? [];
383
+ const sortedChannels = (filteredChannels && orderBy(filteredChannels, ['updatedAt'], ['desc'])) || [];
384
+
385
+ console.log(`📊 Processed channels: ${sortedChannels.length} (page: ${pageNum}, skip: ${skipCount})`);
327
386
 
328
- // Fetch the channels
329
- return getChannelsRefetch({
330
- role: channelRole,
331
- criteria: currentFilters,
332
- supportServices: supportServices ? true : false,
333
- supportServiceCriteria: {
334
- type: RoomType.Service,
335
- },
336
- })
337
- .then((data) => {
338
387
  if (isMountedRef.current) {
339
- const allChannels = [
340
- ...(data?.data?.supportServiceChannels ?? []),
341
- ...(data?.data?.channelsByUser ?? []),
342
- ];
343
- console.log(`Refresh completed, found ${allChannels.length} channels`);
344
- safeSend({
345
- type: 'REFRESH_CHANNELS_SUCCESS',
346
- data: { channels: allChannels },
347
- });
388
+ if (append) {
389
+ safeSend({
390
+ type: Actions.APPEND_CHANNELS,
391
+ data: { channels: sortedChannels },
392
+ });
393
+ } else {
394
+ safeSend({
395
+ type: Actions.FETCH_CHANNELS,
396
+ data: { channels: sortedChannels },
397
+ });
398
+ }
399
+
400
+ // Immediately stop loading state instead of waiting
401
+ safeSend({ type: Actions.STOP_LOADING });
348
402
  }
349
- })
350
- .catch((error) => {
403
+ } catch (error) {
404
+ console.error('Error fetching channels:', error);
351
405
  if (isMountedRef.current) {
352
- console.error('Channel refresh error:', error.message);
353
406
  safeSend({
354
- type: 'ERROR',
355
- data: { message: error.message },
407
+ type: Actions.ERROR_HANDLED,
408
+ data: { message: 'Failed to fetch channels' },
356
409
  });
357
410
  }
358
- });
359
- }, [channelRole, supportServices, getChannelsRefetch, safeSend, safeContextProperty]);
411
+ }
412
+ },
413
+ [getChannelsRefetch, channelRole, channelFilters, supportServices, auth?.id, safeSend],
414
+ );
360
415
 
361
- // Update the channelFiltersRef when channelFilters changes
416
+ // Add a safety timeout to ensure loading state is eventually cleared
417
+ // even if the fetchChannelsDirectly function fails to complete
362
418
  useEffect(() => {
363
- const filtersChanged = JSON.stringify(channelFiltersRef.current) !== JSON.stringify(channelFilters);
364
- if (filtersChanged) {
365
- console.log('Channel filters changed, updating ref');
366
- channelFiltersRef.current = channelFilters;
367
-
368
- // Only refresh if already mounted and initialized
369
- if (isMountedRef.current && safeContextProperty('channelRole') !== null) {
370
- console.log('Refreshing due to filter change');
371
- refreshChannels();
372
- }
419
+ if (loading) {
420
+ const safetyTimeout = setTimeout(() => {
421
+ console.log('⚠️ Safety timeout triggered - forcing loading state to stop');
422
+ if (isMountedRef.current) {
423
+ safeSend({ type: Actions.STOP_LOADING });
424
+ }
425
+ }, 5000); // 5 seconds safety timeout
426
+
427
+ return () => clearTimeout(safetyTimeout);
373
428
  }
374
- }, [channelFilters, refreshChannels, safeContextProperty]);
429
+ }, [loading, safeSend]);
375
430
 
376
- // Handle focus effects
377
- useFocusEffect(
378
- useCallback(() => {
379
- if (!isFocused) return;
431
+ // Add a faster refresh function with smaller dataset and timeout
432
+ const fastRefresh = useCallback(async () => {
433
+ try {
434
+ console.log('🔄 Fast refreshing channels...');
380
435
 
381
- console.log('Screen focused, checking if refresh needed');
436
+ // Set a timeout to ensure refreshing state is cleared if the fetch fails
437
+ const clearRefreshingTimeout = setTimeout(() => {
438
+ if (isMountedRef.current) {
439
+ console.log('⚠️ Fast refresh timeout - stopping refresh state');
440
+ safeSend({ type: Actions.STOP_LOADING });
441
+ }
442
+ }, 3000); // 3 second timeout for refresh
443
+
444
+ // Perform the fetch with a smaller limit for faster results
445
+ const { data } = await getChannelsRefetch({
446
+ role: channelRole,
447
+ criteria: channelFilters,
448
+ supportServices: supportServices ? true : false,
449
+ supportServiceCriteria: {
450
+ type: RoomType.Service,
451
+ },
452
+ limit: 10,
453
+ skip: 0,
454
+ });
382
455
 
383
- // Only refresh on focus if we're not already loading and haven't refreshed
384
- if (isMountedRef.current && !hasRefreshedRef.current) {
385
- console.log('Refreshing on focus');
386
- hasRefreshedRef.current = true;
387
- refreshChannels();
388
- }
456
+ // Cancel the timeout since we got a response
457
+ clearTimeout(clearRefreshingTimeout);
389
458
 
390
- return () => {
391
- // Reset the ref when the screen loses focus
392
- console.log('Screen unfocused, resetting refresh state');
393
- hasRefreshedRef.current = false;
394
- };
395
- }, [isFocused, refreshChannels]),
396
- );
459
+ if (!isMountedRef.current) return;
397
460
 
398
- const handleRefresh = useCallback(() => {
399
- // Don't allow manual refresh while already refreshing
400
- if (safeContextProperty('refreshing', false)) {
401
- console.log('Manual refresh ignored - refresh already in progress');
402
- return;
461
+ const allChannels = [...(data?.supportServiceChannels ?? []), ...(data?.channelsByUser ?? [])];
462
+ const filteredChannels =
463
+ allChannels?.filter((c) =>
464
+ c.members.some((u) => u !== null && u?.user?.id != auth?.id && u.user.__typename == 'UserAccount'),
465
+ ) ?? [];
466
+ const sortedChannels = (filteredChannels && orderBy(filteredChannels, ['updatedAt'], ['desc'])) || [];
467
+
468
+ console.log(`📊 Fast refresh completed: ${sortedChannels.length} channels`);
469
+
470
+ // Update the state with the new channels, but only if still mounted
471
+ if (isMountedRef.current) {
472
+ // Use a single update to prevent UI jumping
473
+ safeSend({
474
+ type: Actions.FETCH_CHANNELS,
475
+ data: {
476
+ channels: sortedChannels,
477
+ stopLoading: true,
478
+ },
479
+ });
480
+ }
481
+ } catch (error) {
482
+ console.error('Error during fast refresh:', error);
483
+ if (isMountedRef.current) {
484
+ safeSend({ type: Actions.STOP_LOADING });
485
+ }
403
486
  }
487
+ }, [getChannelsRefetch, channelRole, channelFilters, supportServices, auth?.id, safeSend]);
404
488
 
405
- console.log('Manual refresh triggered');
406
- refreshChannels();
407
- }, [refreshChannels, safeContextProperty]);
489
+ // Process state changes and execute side effects
490
+ useEffect(() => {
491
+ // Only execute if not already refreshing or loading to prevent loops
492
+ const context = safeGetContext();
493
+ const isAlreadyFetching = context.refreshing || context.loading;
494
+
495
+ if (!isAlreadyFetching) {
496
+ if (safeMatches(BaseState.FetchChannels)) {
497
+ console.log('🔄 Fetching channels...');
498
+ fetchChannelsDirectly(1, false);
499
+ } else if (safeMatches(MainState.RefreshChannels)) {
500
+ console.log('🔄 Refreshing channels...');
501
+ fetchChannelsDirectly(1, false);
502
+ } else if (safeMatches(MainState.LoadMoreChannels)) {
503
+ console.log('🔄 Loading more channels...');
504
+ fetchChannelsDirectly(page, true);
505
+ }
506
+ } else {
507
+ // Log that we're skipping the fetch due to already being in progress
508
+ console.log('⏩ Skipping fetch because isAlreadyFetching:', isAlreadyFetching);
509
+ }
510
+ }, [fetchChannelsDirectly, safeMatches, safeGetContext, state.value, page]);
408
511
 
409
- const channels = React.useMemo(() => {
410
- const allChannels = [...(userChannels?.supportServiceChannels ?? []), ...(userChannels?.channelsByUser ?? [])];
411
- let uChannels: any =
412
- allChannels?.filter((c: any) =>
413
- c.members.some((u: any) => u !== null && u?.user?.id != auth?.id && u.user.__typename == 'UserAccount'),
414
- ) ?? [];
415
- return (uChannels && orderBy(uChannels, ['updatedAt'], ['desc'])) || [];
416
- }, [userChannels, auth?.id]);
512
+ // Add a debug log to track state transitions
513
+ useEffect(() => {
514
+ console.log('State changed to:', state.value);
515
+ console.log(
516
+ 'Context:',
517
+ JSON.stringify({
518
+ channelsCount: channels.length,
519
+ loading,
520
+ refreshing,
521
+ }),
522
+ );
523
+ }, [state.value, channels.length, loading, refreshing]);
524
+
525
+ // Initialize state machine with props on mount
526
+ useEffect(() => {
527
+ if (isMountedRef.current) {
528
+ console.log('🚀 Initializing state machine with props', {
529
+ channelRole,
530
+ channelFilters,
531
+ supportServices,
532
+ selectedChannelId: params?.channelId,
533
+ });
417
534
 
418
- const handleSelectChannel = useCallback(
419
- (id: any, title: any) => {
420
535
  safeSend({
421
- type: DialogsActions.SELECT_CHANNEL,
422
- data: { channelId: id },
536
+ type: Actions.INITIAL_CONTEXT,
537
+ data: {
538
+ channelRole,
539
+ channelFilters,
540
+ supportServices,
541
+ selectedChannelId: params?.channelId,
542
+ },
423
543
  });
424
544
 
425
- if (params?.channelId) {
426
- navigation.navigate(config.INBOX_MESSEGE_PATH as any, {
427
- channelId: params?.channelId,
428
- role: params?.role,
429
- title: params?.title ?? null,
430
- hideTabBar: true,
545
+ // Add a safety measure to ensure loading is stopped even if fetch fails
546
+ const initSafetyTimeout = setTimeout(() => {
547
+ if (isMountedRef.current && loading) {
548
+ console.log('⚠️ Init safety timeout triggered - forcing loading state to stop');
549
+ safeSend({ type: Actions.STOP_LOADING });
550
+ }
551
+ }, 8000); // 8 seconds safety timeout
552
+
553
+ return () => clearTimeout(initSafetyTimeout);
554
+ }
555
+ }, []);
556
+
557
+ // Handle refresh on focus (when navigating back to this screen)
558
+ const focusRefreshRef = useRef<number | null>(null);
559
+
560
+ useFocusEffect(
561
+ useCallback(() => {
562
+ // Use a flag to ensure we only trigger refresh once per focus
563
+ let hasTriggeredRefresh = false;
564
+
565
+ // Reset the focus refresh tracking when component gets focus
566
+ const now = Date.now();
567
+ const lastRefresh = focusRefreshRef.current;
568
+
569
+ // Only refresh if at least 2 seconds have passed since last refresh
570
+ const shouldRefresh = lastRefresh === null || now - lastRefresh > 2000;
571
+
572
+ // Only refresh if component is mounted and not in initial state,
573
+ // and not already refreshing/loading
574
+ const context = safeGetContext();
575
+ const isAlreadyFetching = context.refreshing || context.loading;
576
+
577
+ if (
578
+ isMountedRef.current &&
579
+ !hasTriggeredRefresh &&
580
+ !isAlreadyFetching &&
581
+ shouldRefresh &&
582
+ (channels.length > 0 || safeMatches(BaseState.Idle))
583
+ ) {
584
+ hasTriggeredRefresh = true;
585
+ focusRefreshRef.current = now;
586
+
587
+ // Use fast refresh for better performance
588
+ console.log('🔄 Focus effect: triggering fast refresh');
589
+ safeSend({
590
+ type: Actions.START_LOADING,
591
+ data: { refreshing: true },
431
592
  });
593
+
594
+ // Execute fast refresh with a short delay to prevent UI jank
595
+ setTimeout(() => {
596
+ if (isMountedRef.current) {
597
+ fastRefresh();
598
+ }
599
+ }, 100);
432
600
  } else {
433
- navigation.navigate(config.INBOX_MESSEGE_PATH as any, {
434
- channelId: id,
435
- role: channelRole,
436
- title: title,
437
- hideTabBar: true,
601
+ console.log('⏩ Skipping focus refresh:', {
602
+ isAlreadyFetching,
603
+ hasTriggeredRefresh,
604
+ shouldRefresh,
605
+ timeGap: lastRefresh === null ? 'first refresh' : now - lastRefresh,
438
606
  });
439
607
  }
608
+
609
+ return () => {
610
+ // Reset flag when focus is lost
611
+ hasTriggeredRefresh = false;
612
+ };
613
+ }, [safeSend, channels.length, safeMatches, safeGetContext, fastRefresh]),
614
+ );
615
+
616
+ // Navigation handlers
617
+ const handleSelectChannel = useCallback(
618
+ (id, title) => {
619
+ // Always update the selected channel ID, even if it's the same channel
620
+ safeSend({ type: Actions.SELECT_CHANNEL, data: { channelId: id } });
621
+
622
+ // Force navigation to the channel screen, even if it's already selected
623
+ // This ensures we can reopen the same channel multiple times
624
+ navigation.navigate(config.INBOX_MESSEGE_PATH, {
625
+ channelId: id,
626
+ role: channelRole,
627
+ title: title,
628
+ hideTabBar: true,
629
+ timestamp: new Date().getTime(), // Add timestamp to force a refresh when navigating to the same screen
630
+ });
440
631
  },
441
- [params, navigation, channelRole, safeSend],
632
+ [navigation, channelRole, safeSend],
442
633
  );
443
634
 
444
635
  const handleSelectServiceChannel = useCallback(
445
- (id: any, title: any, postParentId: any) => {
446
- safeSend({
447
- type: DialogsActions.SELECT_CHANNEL,
448
- data: { channelId: id },
636
+ (id, title, postParentId) => {
637
+ safeSend({ type: Actions.SELECT_CHANNEL, data: { channelId: id } });
638
+ navigation.navigate(postParentId || postParentId === 0 ? config.THREAD_MESSEGE_PATH : config.THREADS_PATH, {
639
+ channelId: id,
640
+ role: channelRole,
641
+ title: title,
642
+ postParentId: postParentId,
643
+ hideTabBar: true,
449
644
  });
645
+ },
646
+ [navigation, channelRole, safeSend],
647
+ );
450
648
 
451
- if (params?.channelId) {
452
- navigation.navigate(
453
- params?.postParentId || params?.postParentId == 0
454
- ? config.THREAD_MESSEGE_PATH
455
- : (config.THREADS_PATH as any),
456
- {
457
- channelId: params?.channelId,
458
- role: params?.role,
459
- title: params?.title ?? null,
460
- postParentId: params?.postParentId,
461
- hideTabBar: true,
462
- },
463
- );
464
- } else {
465
- navigation.navigate(
466
- postParentId || postParentId == 0 ? config.THREAD_MESSEGE_PATH : (config.THREADS_PATH as any),
467
- {
468
- channelId: id,
469
- role: channelRole,
470
- title: title,
471
- postParentId: postParentId,
472
- hideTabBar: true,
473
- },
474
- );
475
- }
649
+ // Modified pull-to-refresh handler to use fast refresh
650
+ const handlePullToRefresh = useCallback(() => {
651
+ if (refreshing) {
652
+ console.log('⏩ Skipping refresh because already refreshing');
653
+ return;
654
+ }
655
+
656
+ // Update the last refresh timestamp to prevent simultaneous focus refresh
657
+ const now = Date.now();
658
+ focusRefreshRef.current = now;
659
+
660
+ console.log('🔄 Pull-to-refresh triggered');
661
+ safeSend({
662
+ type: Actions.START_LOADING,
663
+ data: { refreshing: true },
664
+ });
665
+
666
+ // Use the fast refresh approach for pull-to-refresh
667
+ fastRefresh();
668
+ }, [safeSend, refreshing, fastRefresh]);
669
+
670
+ // Search handler
671
+ const handleSearchChange = useCallback(
672
+ (text: string) => {
673
+ safeSend({
674
+ type: Actions.SET_SEARCH_QUERY,
675
+ data: { searchQuery: text },
676
+ });
476
677
  },
477
- [params, navigation, channelRole, safeSend],
678
+ [safeSend],
478
679
  );
479
680
 
480
- const searchQuery = safeContextProperty('searchQuery', '');
681
+ // Add loadMore handler with debounce to prevent multiple calls
682
+ const handleLoadMore = useCallback(() => {
683
+ if (!loadingMore && hasMoreChannels) {
684
+ console.log('Loading more channels at page:', page + 1);
685
+ safeSend({ type: Actions.LOAD_MORE_CHANNELS });
686
+ } else {
687
+ console.log('Skip loading more: loadingMore=', loadingMore, 'hasMoreChannels=', hasMoreChannels);
688
+ }
689
+ }, [safeSend, loadingMore, hasMoreChannels, page]);
481
690
 
482
691
  return (
483
692
  <Box className="p-2">
484
693
  <FlatList
485
- data={channels && channels?.length > 0 ? channels : []}
486
- onRefresh={handleRefresh}
487
- refreshing={safeContextProperty('refreshing', false)}
694
+ data={channels}
695
+ onRefresh={handlePullToRefresh}
696
+ refreshing={refreshing}
488
697
  contentContainerStyle={{ minHeight: '100%' }}
489
698
  ItemSeparatorComponent={() => <Box className="h-0.5 bg-gray-200" />}
490
- renderItem={({ item: channel }: any) =>
491
- channel?.type === RoomType.Service ? (
699
+ renderItem={({ item: channel }) => {
700
+ return channel?.type === RoomType.Service ? (
492
701
  <ServiceDialogsListItem
702
+ key={`service-${channel.id}`}
493
703
  onOpen={handleSelectServiceChannel}
494
704
  currentUser={auth}
495
705
  channel={channel}
496
- refreshing={safeContextProperty('refreshing', false)}
497
- selectedChannelId={safeContextProperty('selectedChannelId', params?.channelId)}
706
+ refreshing={refreshing}
707
+ selectedChannelId={selectedChannelId}
498
708
  role={channelRole}
499
709
  />
500
710
  ) : (
501
711
  <DialogsListItem
712
+ key={`direct-${channel.id}`}
502
713
  onOpen={handleSelectChannel}
503
714
  currentUser={auth}
504
715
  channel={channel}
505
- selectedChannelId={safeContextProperty('selectedChannelId', params?.channelId)}
716
+ selectedChannelId={selectedChannelId}
717
+ forceRefresh={true}
506
718
  />
507
- )
719
+ );
720
+ }}
721
+ ListFooterComponent={() =>
722
+ loadingMore ? (
723
+ <Center className="py-4">
724
+ <Spinner color={colors.blue[500]} size="small" />
725
+ </Center>
726
+ ) : null
508
727
  }
509
- ListEmptyComponent={() => (
510
- <>
511
- {userChannelsLoading || safeContextProperty('loading', false) ? (
512
- <Center className="flex-1 justify-center items-center">
513
- <Spinner color={colors.blue[500]} />
728
+ onEndReached={handleLoadMore}
729
+ onEndReachedThreshold={0.3}
730
+ initialNumToRender={10}
731
+ maxToRenderPerBatch={10}
732
+ windowSize={10}
733
+ removeClippedSubviews={true}
734
+ updateCellsBatchingPeriod={50}
735
+ ListEmptyComponent={() => {
736
+ console.log('Rendering ListEmptyComponent', { loading, refreshing, stateValue: state.value });
737
+
738
+ // Only show spinner during initial loading
739
+ if (loading && channels.length === 0) {
740
+ return (
741
+ <Center className="flex-1 justify-center items-center" style={{ height: 300 }}>
742
+ <Spinner color={colors.blue[500]} size="large" />
743
+ <Text className="mt-4 text-gray-500">Loading conversations...</Text>
514
744
  </Center>
515
- ) : (
516
- <Box className="p-5">
517
- <Heading>Chat</Heading>
518
- <Input className={`h-[50] mt-3 rounded-[50] border-gray-200 border `}>
519
- <InputField
520
- placeholder="Search"
521
- value={searchQuery}
522
- onChangeText={(text) =>
523
- safeSend({
524
- type: DialogsActions.SET_SEARCH_QUERY,
525
- data: { searchQuery: text },
526
- })
527
- }
528
- />
529
- </Input>
530
- <Center className="mt-6">
531
- <Ionicons name="chatbubbles" size={50} />
532
- <Text>You don't have any messages yet!</Text>
533
- </Center>
745
+ );
746
+ }
747
+
748
+ // Show empty state when no channels and not loading
749
+ return (
750
+ <Box className="p-6">
751
+ <Box className="mb-6">
752
+ <Heading className="text-2xl font-bold">Direct Messages</Heading>
753
+ <Text className="text-gray-600 mt-1">Private conversations with other users</Text>
534
754
  </Box>
535
- )}
536
- </>
537
- )}
538
- keyExtractor={(item, index) => 'key' + index}
755
+
756
+ <Input
757
+ className="mb-8 h-[50] rounded-md border-gray-300 border"
758
+ size="md"
759
+ style={{
760
+ paddingVertical: 8,
761
+ marginBottom: 10,
762
+ borderColor: '#d1d5db',
763
+ borderRadius: 10,
764
+ }}
765
+ >
766
+ <InputField
767
+ placeholder="Search messages..."
768
+ onChangeText={handleSearchChange}
769
+ value={searchQuery}
770
+ />
771
+ </Input>
772
+
773
+ <Center className="items-center" style={{ paddingVertical: 5 }}>
774
+ <Box className="w-16 h-16 rounded-full bg-blue-500 flex items-center justify-center mb-5">
775
+ <Ionicons name="chatbubble-ellipses" size={30} color="white" />
776
+ </Box>
777
+
778
+ <Text className="text-2xl font-bold text-center mb-2">No messages yet</Text>
779
+
780
+ <Text className="text-gray-600 text-center mb-8">
781
+ When you start conversations with others,{'\n'}
782
+ they'll appear here.
783
+ </Text>
784
+ </Center>
785
+ </Box>
786
+ );
787
+ }}
788
+ keyExtractor={(item) => `channel-${item.id}`}
539
789
  />
540
790
  </Box>
541
791
  );