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