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

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 (26) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/screens/inbox/components/CachedImage/index.js +0 -19
  3. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  4. package/lib/screens/inbox/components/DialogsListItem.js +423 -50
  5. package/lib/screens/inbox/components/DialogsListItem.js.map +1 -1
  6. package/lib/screens/inbox/components/ServiceDialogsListItem.js +375 -51
  7. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +1 -1
  8. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js +175 -0
  9. package/lib/screens/inbox/components/workflow/dialogs-list-item-xstate.js.map +1 -0
  10. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js +191 -0
  11. package/lib/screens/inbox/components/workflow/service-dialogs-list-item-xstate.js.map +1 -0
  12. package/lib/screens/inbox/containers/Dialogs.js +536 -66
  13. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  14. package/lib/screens/inbox/containers/ThreadConversationView.js +95 -23
  15. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  16. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js +211 -0
  17. package/lib/screens/inbox/containers/workflow/dialogs-xstate.js.map +1 -0
  18. package/package.json +2 -2
  19. package/src/screens/inbox/components/CachedImage/index.tsx +9 -9
  20. package/src/screens/inbox/components/DialogsListItem.tsx +624 -107
  21. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +506 -114
  22. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +35 -17
  23. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
  24. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
  25. package/src/screens/inbox/containers/Dialogs.tsx +711 -169
  26. package/src/screens/inbox/containers/ThreadConversationView.tsx +151 -35
@@ -1,27 +1,45 @@
1
- import React, { useCallback, useMemo, useEffect, useState } 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';
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
+ };
25
43
 
26
44
  export interface InboxProps {
27
45
  channelFilters?: Record<string, unknown>;
@@ -29,23 +47,295 @@ export interface InboxProps {
29
47
  supportServices: boolean;
30
48
  }
31
49
 
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
+
32
231
  const DialogsComponent = (props: InboxProps) => {
33
232
  const { channelFilters: channelFilterProp, channelRole, supportServices } = props;
34
233
  const channelFilters = { ...channelFilterProp };
35
234
  channelFilters.type = channelFilters?.type ?? RoomType.Direct;
36
235
  const { params } = useRoute<any>();
37
236
  const auth = useSelector(userSelector);
38
- const dispatch = useDispatch();
39
237
  const navigation = useNavigation<any>();
40
- const isFocused = useIsFocused();
41
- const [refreshing, setRefresh] = useState<boolean>(false);
42
- // const [userDirectChannel, setUserDirectChannel] = useState<any>([]);
43
-
44
- const {
45
- data: userChannels,
46
- loading: userChannelsLoading,
47
- refetch: getChannelsRefetch,
48
- } = useGetChannelsByUserWithServiceChannelsQuery({
238
+
239
+ // Create a ref to track if component is mounted
240
+ 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({
49
339
  variables: {
50
340
  role: channelRole,
51
341
  criteria: channelFilters,
@@ -53,197 +343,449 @@ const DialogsComponent = (props: InboxProps) => {
53
343
  supportServiceCriteria: {
54
344
  type: RoomType.Service,
55
345
  },
346
+ limit: 15,
347
+ skip: 0,
56
348
  },
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
57
353
  });
58
354
 
59
- // const {
60
- // data: userChannels,
61
- // loading: userChannelsLoading,
62
- // refetch: getChannelsRefetch,
63
- // } = useGetChannelsByUserQuery({
64
- // variables: {
65
- // role: channelRole,
66
- // criteria: channelFilters,
67
- // },
68
- // onCompleted: (data: any) => {
69
- // setRefresh(false);
70
- // },
71
- // });
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})`);
72
361
 
73
- useFocusEffect(
74
- React.useCallback(() => {
75
- // Do something when the screen is focused
76
- setRefresh(false);
77
- //getChannelsRefetch({ role: channelRole, criteria: channelFilters });
78
- getChannelsRefetch({
362
+ // Calculate skip based on page number (pagination)
363
+ const skipCount = (pageNum - 1) * 15;
364
+
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
+ });
375
+
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})`);
386
+
387
+ if (isMountedRef.current) {
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 });
402
+ }
403
+ } catch (error) {
404
+ console.error('Error fetching channels:', error);
405
+ if (isMountedRef.current) {
406
+ safeSend({
407
+ type: Actions.ERROR_HANDLED,
408
+ data: { message: 'Failed to fetch channels' },
409
+ });
410
+ }
411
+ }
412
+ },
413
+ [getChannelsRefetch, channelRole, channelFilters, supportServices, auth?.id, safeSend],
414
+ );
415
+
416
+ // Add a safety timeout to ensure loading state is eventually cleared
417
+ // even if the fetchChannelsDirectly function fails to complete
418
+ useEffect(() => {
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);
428
+ }
429
+ }, [loading, safeSend]);
430
+
431
+ // Add a faster refresh function with smaller dataset and timeout
432
+ const fastRefresh = useCallback(async () => {
433
+ try {
434
+ console.log('🔄 Fast refreshing channels...');
435
+
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({
79
446
  role: channelRole,
80
447
  criteria: channelFilters,
81
448
  supportServices: supportServices ? true : false,
82
449
  supportServiceCriteria: {
83
450
  type: RoomType.Service,
84
451
  },
452
+ limit: 10,
453
+ skip: 0,
85
454
  });
455
+
456
+ // Cancel the timeout since we got a response
457
+ clearTimeout(clearRefreshingTimeout);
458
+
459
+ if (!isMountedRef.current) return;
460
+
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
+ }
486
+ }
487
+ }, [getChannelsRefetch, channelRole, channelFilters, supportServices, auth?.id, safeSend]);
488
+
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]);
511
+
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
+ });
534
+
535
+ safeSend({
536
+ type: Actions.INITIAL_CONTEXT,
537
+ data: {
538
+ channelRole,
539
+ channelFilters,
540
+ supportServices,
541
+ selectedChannelId: params?.channelId,
542
+ },
543
+ });
544
+
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 },
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);
600
+ } else {
601
+ console.log('⏩ Skipping focus refresh:', {
602
+ isAlreadyFetching,
603
+ hasTriggeredRefresh,
604
+ shouldRefresh,
605
+ timeGap: lastRefresh === null ? 'first refresh' : now - lastRefresh,
606
+ });
607
+ }
608
+
86
609
  return () => {
87
- // Do something when the screen is unfocused
88
- // Useful for cleanup functions
610
+ // Reset flag when focus is lost
611
+ hasTriggeredRefresh = false;
89
612
  };
90
- }, [channelFilters]),
613
+ }, [safeSend, channels.length, safeMatches, safeGetContext, fastRefresh]),
91
614
  );
92
615
 
93
- // const channels = React.useMemo(() => {
94
- // if (!userChannels?.channelsByUser?.length) return null;
95
- // let uChannels: any =
96
- // userChannels?.channelsByUser?.filter((c: any) =>
97
- // c.members.some((u: any) => u !== null && u?.user?.id != auth?.id && u.user.__typename == 'UserAccount'),
98
- // ) ?? [];
99
- // return (uChannels && orderBy(uChannels, ['updatedAt'], ['desc'])) || [];
100
- // }, [userChannels]);
101
-
102
- const channels = React.useMemo(() => {
103
- const allChannels = [...(userChannels?.supportServiceChannels ?? []), ...(userChannels?.channelsByUser ?? [])];
104
- let uChannels: any =
105
- allChannels?.filter((c: any) =>
106
- c.members.some((u: any) => u !== null && u?.user?.id != auth?.id && u.user.__typename == 'UserAccount'),
107
- ) ?? [];
108
- return (uChannels && orderBy(uChannels, ['updatedAt'], ['desc'])) || [];
109
- }, [userChannels]);
110
-
111
- // useEffect(() => {
112
- // setTimeout(() => {
113
- // dispatch({
114
- // type: CHANGE_SETTINGS_ACTION,
115
- // payload: {
116
- // footerRender: false,
117
- // },
118
- // } as any);
119
- // }, 0);
120
- // return () => {
121
- // dispatch({
122
- // type: CHANGE_SETTINGS_ACTION,
123
- // payload: {
124
- // footerRender: true,
125
- // },
126
- // } as any);
127
- // };
128
- // }, []);
129
-
130
- // useEffect(() => {
131
- // if (userChannels?.channelsByUser) {
132
- // if (userChannels?.channelsByUser?.length == 0) {
133
- // setUserDirectChannel([]);
134
- // }
135
- // //Direct channel
136
- // let userDirectChannels: any =
137
- // userChannels?.channelsByUser
138
- // ?.filter((i: any) => i.type == 'DIRECT')
139
- // ?.filter((c: any) =>
140
- // c.members.some((u: any) => u?.user?.id != auth?.id && u.user.__typename == 'UserAccount'),
141
- // ) ?? [];
142
-
143
- // if (userDirectChannels?.length > 0) setUserDirectChannel(userDirectChannels);
144
- // }
145
- // }, [userChannels?.channelsByUser]);
146
-
147
- const handleSelectChannel = useCallback((id: any, title: any) => {
148
- if (params?.channelId) {
149
- navigation.navigate(config.INBOX_MESSEGE_PATH as any, {
150
- channelId: params?.channelId,
151
- role: params?.role,
152
- title: params?.title ?? null,
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,
153
628
  hideTabBar: true,
629
+ timestamp: new Date().getTime(), // Add timestamp to force a refresh when navigating to the same screen
154
630
  });
155
- } else {
156
- navigation.navigate(config.INBOX_MESSEGE_PATH as any, {
631
+ },
632
+ [navigation, channelRole, safeSend],
633
+ );
634
+
635
+ const handleSelectServiceChannel = useCallback(
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, {
157
639
  channelId: id,
158
640
  role: channelRole,
159
641
  title: title,
642
+ postParentId: postParentId,
160
643
  hideTabBar: true,
161
644
  });
645
+ },
646
+ [navigation, channelRole, safeSend],
647
+ );
648
+
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;
162
654
  }
163
- }, []);
164
655
 
165
- const handleSelectServiceChannel = useCallback((id: any, title: any, postParentId: any) => {
166
- if (params?.channelId) {
167
- navigation.navigate(
168
- params?.postParentId || params?.postParentId == 0
169
- ? config.THREAD_MESSEGE_PATH
170
- : (config.THREADS_PATH as any),
171
- {
172
- channelId: params?.channelId,
173
- role: params?.role,
174
- title: params?.title ?? null,
175
- postParentId: params?.postParentId,
176
- hideTabBar: true,
177
- },
178
- );
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
+ });
677
+ },
678
+ [safeSend],
679
+ );
680
+
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 });
179
686
  } else {
180
- navigation.navigate(
181
- postParentId || postParentId == 0 ? config.THREAD_MESSEGE_PATH : (config.THREADS_PATH as any),
182
- {
183
- channelId: id,
184
- role: channelRole,
185
- title: title,
186
- postParentId: postParentId,
187
- hideTabBar: true,
188
- },
189
- );
687
+ console.log('Skip loading more: loadingMore=', loadingMore, 'hasMoreChannels=', hasMoreChannels);
190
688
  }
191
- }, []);
192
-
193
- const handleRefresh = useCallback(() => {
194
- //if(userChannels?.channelsByUser?.length != channels?.length)setRefresh(true);
195
- setRefresh(true);
196
- getChannelsRefetch({ role: channelRole, criteria: channelFilters })?.finally(() => setRefresh(false));
197
- }, []);
689
+ }, [safeSend, loadingMore, hasMoreChannels, page]);
198
690
 
199
691
  return (
200
692
  <Box className="p-2">
201
693
  <FlatList
202
- data={channels && channels?.length > 0 ? channels : []}
203
- onRefresh={handleRefresh}
694
+ data={channels}
695
+ onRefresh={handlePullToRefresh}
204
696
  refreshing={refreshing}
205
697
  contentContainerStyle={{ minHeight: '100%' }}
206
698
  ItemSeparatorComponent={() => <Box className="h-0.5 bg-gray-200" />}
207
- renderItem={({ item: channel }: any) =>
208
- channel?.type === RoomType.Service ? (
699
+ renderItem={({ item: channel }) => {
700
+ return channel?.type === RoomType.Service ? (
209
701
  <ServiceDialogsListItem
702
+ key={`service-${channel.id}`}
210
703
  onOpen={handleSelectServiceChannel}
211
704
  currentUser={auth}
212
705
  channel={channel}
213
706
  refreshing={refreshing}
214
- selectedChannelId={params?.channelId}
707
+ selectedChannelId={selectedChannelId}
215
708
  role={channelRole}
216
709
  />
217
710
  ) : (
218
711
  <DialogsListItem
712
+ key={`direct-${channel.id}`}
219
713
  onOpen={handleSelectChannel}
220
714
  currentUser={auth}
221
715
  channel={channel}
222
- selectedChannelId={params?.channelId}
716
+ selectedChannelId={selectedChannelId}
717
+ forceRefresh={true}
223
718
  />
224
- )
719
+ );
720
+ }}
721
+ ListFooterComponent={() =>
722
+ loadingMore ? (
723
+ <Center className="py-4">
724
+ <Spinner color={colors.blue[500]} size="small" />
725
+ </Center>
726
+ ) : null
225
727
  }
226
- ListEmptyComponent={() => (
227
- <>
228
- {userChannelsLoading ? (
229
- <Center className="flex-1 justify-center items-center">
230
- <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>
231
744
  </Center>
232
- ) : (
233
- <Box className="p-5">
234
- <Heading>Chat</Heading>
235
- <Input className={`h-[50] mt-3 rounded-[50] border-gray-200 border `}>
236
- <InputField placeholder="Search" />
237
- </Input>
238
- <Center className="mt-6">
239
- <Ionicons name="chatbubbles" size={50} />
240
- <Text>You don't have any messages yet!</Text>
241
- </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>
242
754
  </Box>
243
- )}
244
- </>
245
- )}
246
- 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}`}
247
789
  />
248
790
  </Box>
249
791
  );