@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.
- package/CHANGELOG.md +4 -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 +75 -271
- package/lib/screens/inbox/components/DialogsListItem.js.map +1 -1
- package/lib/screens/inbox/components/ServiceDialogsListItem.js +184 -415
- 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 +478 -944
- package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
- package/lib/screens/inbox/containers/Dialogs.js +212 -628
- 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 +3 -3
- package/src/screens/inbox/components/CachedImage/index.tsx +191 -140
- package/src/screens/inbox/components/DialogsListItem.tsx +104 -368
- package/src/screens/inbox/components/ServiceDialogsListItem.tsx +69 -377
- package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +2 -4
- package/src/screens/inbox/containers/ConversationView.tsx +660 -1060
- package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
- package/src/screens/inbox/containers/Dialogs.tsx +301 -763
- 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,9 +1,8 @@
|
|
|
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
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
|
-
//
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
420
|
-
const { refetch
|
|
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
|
-
//
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
[
|
|
108
|
+
[auth?.id],
|
|
529
109
|
);
|
|
530
110
|
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
if (
|
|
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
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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
|
-
//
|
|
604
|
-
const
|
|
125
|
+
// Process and sort the channels
|
|
126
|
+
const channels = sortChannels(processChannels(allChannels));
|
|
605
127
|
|
|
606
|
-
// Set up
|
|
128
|
+
// Set up subscription for real-time message updates
|
|
607
129
|
useEffect(() => {
|
|
608
|
-
if (!auth || !auth.id
|
|
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
|
|
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
|
-
//
|
|
631
|
-
const
|
|
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
|
-
//
|
|
634
|
-
const
|
|
635
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
682
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
messageSubscriptionRef.current();
|
|
698
|
-
messageSubscriptionRef.current = null;
|
|
699
|
-
}
|
|
216
|
+
console.log('📱 Cleaning up dialog message subscription');
|
|
217
|
+
unsubscribe();
|
|
700
218
|
};
|
|
701
|
-
}, [auth?.id,
|
|
219
|
+
}, [auth?.id, subscribeToMore]);
|
|
702
220
|
|
|
703
|
-
//
|
|
221
|
+
// Handle component cleanup
|
|
704
222
|
useEffect(() => {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
}
|
|
721
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
809
|
-
return () => {
|
|
810
|
-
clearTimeout(focusRefreshTimeout);
|
|
811
|
-
};
|
|
812
|
-
}, [safeSend, fastRefresh]),
|
|
272
|
+
return () => clearTimeout(focusRefreshTimeout);
|
|
273
|
+
}, [refetch]),
|
|
813
274
|
);
|
|
814
275
|
|
|
815
|
-
//
|
|
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
|
-
//
|
|
819
|
-
|
|
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(),
|
|
353
|
+
timestamp: new Date().getTime(),
|
|
829
354
|
});
|
|
830
355
|
},
|
|
831
|
-
[navigation, channelRole
|
|
356
|
+
[navigation, channelRole],
|
|
832
357
|
);
|
|
833
358
|
|
|
834
359
|
const handleSelectServiceChannel = useCallback(
|
|
835
360
|
(id, title, postParentId) => {
|
|
836
|
-
|
|
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
|
|
392
|
+
[navigation, channelRole],
|
|
846
393
|
);
|
|
847
394
|
|
|
848
|
-
//
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
395
|
+
// Handle search query changes
|
|
396
|
+
const handleSearchChange = useCallback((text: string) => {
|
|
397
|
+
setSearchQuery(text);
|
|
398
|
+
}, []);
|
|
854
399
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
400
|
+
// Filter channels by search query
|
|
401
|
+
const filteredChannels = useCallback(() => {
|
|
402
|
+
if (!searchQuery.trim()) return channels;
|
|
858
403
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
}, [
|
|
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={
|
|
437
|
+
data={displayChannels}
|
|
894
438
|
onRefresh={handlePullToRefresh}
|
|
895
|
-
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={
|
|
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
|
-
|
|
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}
|
|
932
|
-
initialNumToRender={5}
|
|
933
|
-
maxToRenderPerBatch={5}
|
|
934
|
-
windowSize={5}
|
|
474
|
+
onEndReachedThreshold={0.5}
|
|
475
|
+
initialNumToRender={5}
|
|
476
|
+
maxToRenderPerBatch={5}
|
|
477
|
+
windowSize={5}
|
|
935
478
|
removeClippedSubviews={true}
|
|
936
|
-
updateCellsBatchingPeriod={100}
|
|
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
|
-
|
|
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" />
|