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