@messenger-box/platform-mobile 10.0.3-alpha.7 → 10.0.3-alpha.72

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 (98) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/lib/compute.js +2 -3
  3. package/lib/compute.js.map +1 -1
  4. package/lib/index.js.map +1 -1
  5. package/lib/queries/inboxQueries.js +65 -0
  6. package/lib/queries/inboxQueries.js.map +1 -0
  7. package/lib/routes.json +2 -3
  8. package/lib/screens/inbox/DialogMessages.js +1 -1
  9. package/lib/screens/inbox/DialogMessages.js.map +1 -1
  10. package/lib/screens/inbox/DialogThreadMessages.js +4 -8
  11. package/lib/screens/inbox/DialogThreadMessages.js.map +1 -1
  12. package/lib/screens/inbox/DialogThreads.js +57 -12
  13. package/lib/screens/inbox/DialogThreads.js.map +1 -1
  14. package/lib/screens/inbox/Inbox.js +1 -1
  15. package/lib/screens/inbox/Inbox.js.map +1 -1
  16. package/lib/screens/inbox/components/CachedImage/consts.js +1 -1
  17. package/lib/screens/inbox/components/CachedImage/consts.js.map +1 -1
  18. package/lib/screens/inbox/components/CachedImage/index.js +168 -46
  19. package/lib/screens/inbox/components/CachedImage/index.js.map +1 -1
  20. package/lib/screens/inbox/components/DialogItem.js +169 -0
  21. package/lib/screens/inbox/components/DialogItem.js.map +1 -0
  22. package/lib/screens/inbox/components/GiftedChatInboxComponent.js +313 -0
  23. package/lib/screens/inbox/components/GiftedChatInboxComponent.js.map +1 -0
  24. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js +147 -31
  25. package/lib/screens/inbox/components/SlackMessageContainer/SlackBubble.js.map +1 -1
  26. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js +6 -1
  27. package/lib/screens/inbox/components/SlackMessageContainer/SlackMessage.js.map +1 -1
  28. package/lib/screens/inbox/components/SubscriptionHandler.js +24 -0
  29. package/lib/screens/inbox/components/SubscriptionHandler.js.map +1 -0
  30. package/lib/screens/inbox/components/ThreadsViewItem.js +66 -55
  31. package/lib/screens/inbox/components/ThreadsViewItem.js.map +1 -1
  32. package/lib/screens/inbox/config/config.js +2 -2
  33. package/lib/screens/inbox/config/config.js.map +1 -1
  34. package/lib/screens/inbox/containers/ConversationView.js +1111 -434
  35. package/lib/screens/inbox/containers/ConversationView.js.map +1 -1
  36. package/lib/screens/inbox/containers/Dialogs.js +193 -80
  37. package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
  38. package/lib/screens/inbox/containers/ThreadConversationView.js +725 -216
  39. package/lib/screens/inbox/containers/ThreadConversationView.js.map +1 -1
  40. package/lib/screens/inbox/containers/ThreadsView.js +83 -50
  41. package/lib/screens/inbox/containers/ThreadsView.js.map +1 -1
  42. package/lib/screens/inbox/hooks/useInboxMessages.js +31 -0
  43. package/lib/screens/inbox/hooks/useInboxMessages.js.map +1 -0
  44. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js +108 -0
  45. package/lib/screens/inbox/hooks/useSafeDialogThreadsMachine.js.map +1 -0
  46. package/lib/screens/inbox/workflow/dialog-threads-xstate.js +151 -0
  47. package/lib/screens/inbox/workflow/dialog-threads-xstate.js.map +1 -0
  48. package/package.json +4 -4
  49. package/src/compute.ts +5 -6
  50. package/src/index.ts +2 -0
  51. package/src/navigation/InboxNavigation.tsx +3 -3
  52. package/src/queries/inboxQueries.ts +299 -0
  53. package/src/queries/index.d.ts +2 -0
  54. package/src/queries/index.ts +1 -0
  55. package/src/screens/inbox/DialogMessages.tsx +1 -1
  56. package/src/screens/inbox/DialogThreadMessages.tsx +7 -14
  57. package/src/screens/inbox/DialogThreads.tsx +55 -61
  58. package/src/screens/inbox/Inbox.tsx +1 -1
  59. package/src/screens/inbox/components/Actionsheet.tsx +30 -0
  60. package/src/screens/inbox/components/CachedImage/consts.ts +4 -3
  61. package/src/screens/inbox/components/CachedImage/index.tsx +232 -61
  62. package/src/screens/inbox/components/DialogItem.tsx +306 -0
  63. package/src/screens/inbox/components/DialogsHeader.tsx +6 -13
  64. package/src/screens/inbox/components/DialogsListItem.tsx +262 -198
  65. package/src/screens/inbox/components/ExpandableInput.tsx +460 -0
  66. package/src/screens/inbox/components/ExpandableInputActionSheet.tsx +518 -0
  67. package/src/screens/inbox/components/GiftedChatInboxComponent.tsx +411 -0
  68. package/src/screens/inbox/components/ServiceDialogsListItem.tsx +337 -194
  69. package/src/screens/inbox/components/SlackInput.tsx +23 -0
  70. package/src/screens/inbox/components/SlackMessageContainer/SlackBubble.tsx +233 -23
  71. package/src/screens/inbox/components/SlackMessageContainer/SlackMessage.tsx +1 -1
  72. package/src/screens/inbox/components/SmartLoader.tsx +61 -0
  73. package/src/screens/inbox/components/SubscriptionHandler.tsx +41 -0
  74. package/src/screens/inbox/components/SupportServiceDialogsListItem.tsx +53 -55
  75. package/src/screens/inbox/components/ThreadsViewItem.tsx +178 -285
  76. package/src/screens/inbox/components/workflow/dialogs-list-item-xstate.ts +145 -0
  77. package/src/screens/inbox/components/workflow/service-dialogs-list-item-xstate.ts +159 -0
  78. package/src/screens/inbox/config/config.ts +2 -2
  79. package/src/screens/inbox/containers/ConversationView.tsx +1843 -702
  80. package/src/screens/inbox/containers/ConversationView.tsx.bk +1467 -0
  81. package/src/screens/inbox/containers/Dialogs.tsx +402 -204
  82. package/src/screens/inbox/containers/SupportServiceDialogs.tsx +4 -4
  83. package/src/screens/inbox/containers/ThreadConversationView.tsx +1350 -319
  84. package/src/screens/inbox/containers/ThreadsView.tsx +105 -193
  85. package/src/screens/inbox/containers/workflow/apollo/handleResult.ts +20 -0
  86. package/src/screens/inbox/containers/workflow/conversation-xstate.ts +313 -0
  87. package/src/screens/inbox/containers/workflow/dialogs-xstate.ts +196 -0
  88. package/src/screens/inbox/containers/workflow/thread-conversation-xstate.ts +401 -0
  89. package/src/screens/inbox/hooks/useInboxMessages.ts +34 -0
  90. package/src/screens/inbox/hooks/useSafeDialogThreadsMachine.ts +136 -0
  91. package/src/screens/inbox/index.ts +37 -0
  92. package/src/screens/inbox/machines/threadsMachine.ts +147 -0
  93. package/src/screens/inbox/workflow/dialog-threads-xstate.ts +163 -0
  94. package/tsconfig.json +11 -54
  95. package/lib/screens/inbox/components/DialogsListItem.js +0 -171
  96. package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
  97. package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -171
  98. package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
@@ -1,91 +1,252 @@
1
- import React, { useEffect, useState, useRef } from 'react';
2
- import { Image } from 'react-native';
1
+ import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
2
+ import { Image, View, Text } from 'react-native';
3
3
  // import { Image } from "react-native"
4
4
  import * as FileSystem from 'expo-file-system';
5
5
 
6
6
  import * as CONST from './consts';
7
7
 
8
- const CachedImage = (props: any) => {
9
- const { source, cacheKey, placeholderContent } = props;
10
- const { uri, headers, expiresIn } = source;
11
- const fileURI = `${CONST.IMAGE_CACHE_FOLDER}${cacheKey}`;
8
+ // Global download tracking to prevent duplicate downloads
9
+ const downloadTracker = new Map();
12
10
 
13
- const [imgUri, setImgUri] = useState<any>(fileURI);
11
+ // Ensure the cache directory exists
12
+ const ensureCacheDirectory = async () => {
13
+ try {
14
+ const dirInfo = await FileSystem.getInfoAsync(CONST.IMAGE_CACHE_FOLDER);
15
+ if (!dirInfo.exists) {
16
+ // console.log('Creating cache directory:', CONST.IMAGE_CACHE_FOLDER);
17
+ await FileSystem.makeDirectoryAsync(CONST.IMAGE_CACHE_FOLDER, { intermediates: true });
18
+ }
19
+ return true;
20
+ } catch (error) {
21
+ // console.error('Failed to create cache directory:', error);
22
+ return false;
23
+ }
24
+ };
14
25
 
15
- const componentIsMounted = useRef(true);
16
- const requestOption = headers ? { headers } : {};
26
+ // Validate and sanitize the image URL
27
+ const validateImageUri = (uri: string): string | null => {
28
+ if (!uri) return null;
17
29
 
18
- const _callback = (downloadProgress: any) => {
19
- if (componentIsMounted.current === false) {
20
- downloadResumableRef.current.pauseAsync();
21
- FileSystem.deleteAsync(fileURI, { idempotent: true }); // delete file locally if it was not downloaded properly
22
- }
23
- };
30
+ // Trim whitespace
31
+ uri = uri.trim();
24
32
 
25
- const downloadResumableRef = useRef(FileSystem.createDownloadResumable(uri, fileURI, requestOption, _callback));
33
+ // Check if it's a valid URL format
34
+ try {
35
+ new URL(uri);
36
+ } catch (e) {
37
+ // console.log('Invalid URL format:', uri);
38
+ return null;
39
+ }
26
40
 
27
- useEffect(() => {
28
- loadImage();
29
- return () => {
30
- componentIsMounted.current = false;
31
- };
32
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
41
+ // Add more validation as needed for your specific case
42
+ return uri;
43
+ };
33
44
 
34
- const loadImage = async () => {
35
- try {
36
- // Use the cached image if it exists
37
- const metadata: any = await FileSystem.getInfoAsync(fileURI);
38
- const expired = expiresIn && new Date().getTime() / 1000 - metadata?.modificationTime > expiresIn;
39
- // console.log({expiresIn, expired})
40
-
41
- // console.log({modificationTime: metadata.modificationTime, currentTime: new Date().getTime() / 1000})
42
- // console.log({metadata})
43
- if (!metadata.exists || metadata?.size === 0 || expired) {
44
- if (componentIsMounted.current) {
45
- setImgUri(null);
45
+ const CachedImage = React.memo(
46
+ (props: any) => {
47
+ const { source, cacheKey, placeholderContent, style, resizeMode, alt, ...restProps } = props;
48
+ const { uri: originalUri, headers, expiresIn = 86400 } = source;
49
+
50
+ // Validate and sanitize the URI
51
+ const uri = validateImageUri(originalUri);
52
+ const fileURI = `${CONST.IMAGE_CACHE_FOLDER}${cacheKey}`;
53
+
54
+ const [imgUri, setImgUri] = useState<string | null>(null);
55
+ const [loadError, setLoadError] = useState<boolean>(false);
56
+ const [useFallbackUri, setUseFallbackUri] = useState<boolean>(false);
57
+ const [isLoading, setIsLoading] = useState<boolean>(true);
58
+
59
+ const componentIsMounted = useRef(true);
60
+ const hasAttemptedLoad = useRef(false);
61
+ const requestOption = headers ? { headers } : {};
62
+
63
+ // Create a memoized download callback
64
+ const downloadCallback = useCallback(
65
+ (downloadProgress: any) => {
66
+ if (componentIsMounted.current === false) {
67
+ downloadTracker.delete(cacheKey);
68
+ downloadResumableRef.current?.pauseAsync();
69
+ FileSystem.deleteAsync(fileURI, { idempotent: true });
70
+ }
71
+ },
72
+ [fileURI, cacheKey],
73
+ );
46
74
 
47
- if (expired) {
75
+ // Create a memoized download resumable
76
+ const downloadResumableRef = useRef(
77
+ uri ? FileSystem.createDownloadResumable(uri, fileURI, requestOption, downloadCallback) : null,
78
+ );
79
+
80
+ // Memoized load image function to avoid recreating on every render
81
+ const loadImage = useCallback(async () => {
82
+ if (!uri || hasAttemptedLoad.current) return;
83
+
84
+ hasAttemptedLoad.current = true;
85
+ setIsLoading(true);
86
+
87
+ try {
88
+ // Check if this image is already being downloaded
89
+ if (downloadTracker.has(cacheKey)) {
90
+ // Wait for the existing download to complete
91
+ await downloadTracker.get(cacheKey);
92
+ if (componentIsMounted.current) {
93
+ setImgUri(fileURI);
94
+ setIsLoading(false);
95
+ }
96
+ return;
97
+ }
98
+
99
+ // Use the cached image if it exists
100
+ const metadata: any = await FileSystem.getInfoAsync(fileURI);
101
+ const expired = expiresIn && new Date().getTime() / 1000 - metadata?.modificationTime > expiresIn;
102
+
103
+ if (!metadata.exists || metadata?.size === 0 || expired) {
104
+ if (!componentIsMounted.current) return;
105
+
106
+ if (expired && metadata.exists) {
48
107
  await FileSystem.deleteAsync(fileURI, { idempotent: true });
49
108
  }
50
- // download to cache
51
- setImgUri(null);
52
109
 
53
- const response: any = await downloadResumableRef.current.downloadAsync();
54
- if (componentIsMounted.current && response.status === 200) {
55
- setImgUri(`${fileURI}?`); // deep clone to force re-render
110
+ if (!uri) {
111
+ setLoadError(true);
112
+ setIsLoading(false);
113
+ return;
114
+ }
115
+
116
+ if (!downloadResumableRef.current) {
117
+ setUseFallbackUri(true);
118
+ setImgUri(uri);
119
+ setIsLoading(false);
120
+ return;
56
121
  }
57
- if (response.status !== 200) {
58
- FileSystem.deleteAsync(fileURI, { idempotent: true }); // delete file locally if it was not downloaded properly
122
+
123
+ try {
124
+ // Record this download in the global tracker
125
+ const downloadPromise = downloadResumableRef.current.downloadAsync();
126
+ downloadTracker.set(cacheKey, downloadPromise);
127
+
128
+ const response: any = await downloadPromise;
129
+
130
+ // Remove from tracker when done
131
+ downloadTracker.delete(cacheKey);
132
+
133
+ if (componentIsMounted.current) {
134
+ if (response && response.status === 200) {
135
+ setImgUri(fileURI);
136
+ } else {
137
+ setUseFallbackUri(true);
138
+ setImgUri(uri);
139
+ FileSystem.deleteAsync(fileURI, { idempotent: true });
140
+ }
141
+ setIsLoading(false);
142
+ }
143
+ } catch (downloadError) {
144
+ downloadTracker.delete(cacheKey);
145
+ if (componentIsMounted.current) {
146
+ setUseFallbackUri(true);
147
+ setImgUri(uri);
148
+ setIsLoading(false);
149
+ }
59
150
  }
151
+ } else {
152
+ // Use cached version
153
+ setImgUri(fileURI);
154
+ setIsLoading(false);
155
+ }
156
+ } catch (err) {
157
+ if (componentIsMounted.current) {
158
+ setUseFallbackUri(true);
159
+ setImgUri(uri);
160
+ setIsLoading(false);
60
161
  }
61
162
  }
62
- } catch (err) {
63
- // console.log({ err })
163
+ }, [uri, fileURI, cacheKey, expiresIn]);
164
+
165
+ // Setup effect
166
+ useEffect(() => {
167
+ const setup = async () => {
168
+ const directoryExists = await ensureCacheDirectory();
169
+ if (directoryExists && uri) {
170
+ loadImage();
171
+ } else {
172
+ setLoadError(true);
173
+ setIsLoading(false);
174
+ }
175
+ };
176
+
177
+ setup();
178
+
179
+ return () => {
180
+ componentIsMounted.current = false;
181
+ };
182
+ }, [uri, loadImage]);
183
+
184
+ // Default placeholder
185
+ const defaultPlaceholder = useMemo(
186
+ () => (
187
+ <View
188
+ style={[
189
+ {
190
+ width: '100%',
191
+ height: '100%',
192
+ backgroundColor: '#e1e1e1',
193
+ justifyContent: 'center',
194
+ alignItems: 'center',
195
+ borderRadius: 3,
196
+ },
197
+ style,
198
+ ]}
199
+ >
200
+ <Text>Image not available</Text>
201
+ </View>
202
+ ),
203
+ [style],
204
+ );
205
+
206
+ // Show placeholder while loading or if there's an error
207
+ if (isLoading || loadError || !imgUri) {
208
+ return placeholderContent || defaultPlaceholder;
64
209
  }
65
- };
66
- // console.log({placeholderContent})
67
- if (!imgUri) return placeholderContent || null;
68
-
69
- return (
70
- <Image
71
- // eslint-disable-next-line react/jsx-props-no-spreading
72
- {...props}
73
- source={{
74
- ...source,
75
- uri: imgUri,
76
- }}
77
- />
78
- );
79
- };
210
+
211
+ // Actual image component
212
+ return (
213
+ <Image
214
+ {...restProps}
215
+ style={style}
216
+ source={{
217
+ ...source,
218
+ uri: useFallbackUri ? uri : imgUri,
219
+ }}
220
+ resizeMode={resizeMode}
221
+ onError={(e) => {
222
+ if (useFallbackUri) {
223
+ setLoadError(true);
224
+ setImgUri(null);
225
+ } else {
226
+ setUseFallbackUri(true);
227
+ setImgUri(uri);
228
+ }
229
+ }}
230
+ />
231
+ );
232
+ },
233
+ (prevProps, nextProps) => {
234
+ // Custom comparison function for memoization
235
+ return (
236
+ prevProps.cacheKey === nextProps.cacheKey &&
237
+ prevProps.source.uri === nextProps.source.uri &&
238
+ prevProps.style === nextProps.style
239
+ );
240
+ },
241
+ );
80
242
 
81
243
  export const CacheManager = {
82
244
  addToCache: async ({ file, key }: any) => {
245
+ await ensureCacheDirectory();
83
246
  await FileSystem.copyAsync({
84
247
  from: file,
85
248
  to: `${CONST.IMAGE_CACHE_FOLDER}${key}`,
86
249
  });
87
- // const uri = await FileSystem.getContentUriAsync(`${CONST.IMAGE_CACHE_FOLDER}${key}`)
88
- // return uri
89
250
  const uri = await CacheManager.getCachedUri({ key });
90
251
  return uri;
91
252
  },
@@ -96,8 +257,18 @@ export const CacheManager = {
96
257
  },
97
258
 
98
259
  downloadAsync: async ({ uri, key, options }: any) => {
260
+ await ensureCacheDirectory();
99
261
  return await FileSystem.downloadAsync(uri, `${CONST.IMAGE_CACHE_FOLDER}${key}`, options);
100
262
  },
263
+
264
+ clearCache: async () => {
265
+ try {
266
+ await FileSystem.deleteAsync(CONST.IMAGE_CACHE_FOLDER, { idempotent: true });
267
+ await ensureCacheDirectory();
268
+ } catch (error) {
269
+ console.error('Error clearing cache:', error);
270
+ }
271
+ },
101
272
  };
102
273
 
103
274
  export default CachedImage;
@@ -0,0 +1,306 @@
1
+ import React, { useMemo, useState, useCallback, useRef, useEffect } from 'react';
2
+ import {
3
+ Text,
4
+ Pressable,
5
+ HStack,
6
+ Box,
7
+ AvatarGroup,
8
+ Avatar,
9
+ AvatarFallbackText,
10
+ AvatarImage,
11
+ AvatarBadge,
12
+ } from '@admin-layout/gluestack-ui-mobile';
13
+ import { format, isToday, isYesterday } from 'date-fns';
14
+ import { useFocusEffect } from '@react-navigation/native';
15
+ import { IChannel, IUserAccount, RoomType } from 'common';
16
+ import { startCase } from 'lodash-es';
17
+ import colors from 'tailwindcss/colors';
18
+
19
+ // Helper function to safely create a Date object
20
+ const safeDate = (dateValue: any): Date | null => {
21
+ if (!dateValue) return null;
22
+
23
+ try {
24
+ const date = new Date(dateValue);
25
+ if (isNaN(date.getTime())) {
26
+ console.warn('Invalid date value detected:', dateValue);
27
+ return null;
28
+ }
29
+ return date;
30
+ } catch (error) {
31
+ console.warn('Error creating date from value:', dateValue, error);
32
+ return null;
33
+ }
34
+ };
35
+
36
+ const createdAtText = (value: string): string => {
37
+ if (!value) return '';
38
+ const date = safeDate(value);
39
+ if (!date) return '';
40
+
41
+ if (isToday(date)) return 'Today';
42
+ if (isYesterday(date)) return 'Yesterday';
43
+ return format(date, 'MMM dd, yyyy');
44
+ };
45
+
46
+ export interface IDialogListChannel extends IChannel {
47
+ users: IUserAccount[];
48
+ }
49
+
50
+ export interface IDialogItemProps {
51
+ currentUser?: any;
52
+ channel?: any;
53
+ onOpen: (id: any, title: any, postParentId?: any) => void;
54
+ }
55
+
56
+ interface LastMessageComponentProps {
57
+ title: string;
58
+ lastMessage: any;
59
+ isServiceChannel?: boolean;
60
+ }
61
+
62
+ // LastMessage component that works for both direct and service channels
63
+ const LastMessageComponent: React.FC<LastMessageComponentProps> = ({
64
+ title,
65
+ lastMessage,
66
+ isServiceChannel = false,
67
+ }) => {
68
+ const count = 30;
69
+ const channelTitle = title?.slice(0, count) + (title?.length > count ? '...' : '') || '';
70
+
71
+ let displayMessage = 'No messages yet';
72
+
73
+ if (lastMessage) {
74
+ const hasFileAttachments = lastMessage.files?.data?.length > 0;
75
+ const isImageMessage =
76
+ lastMessage.message?.includes('<img') ||
77
+ lastMessage.message?.includes('[Image]') ||
78
+ lastMessage.message?.includes('![') ||
79
+ (/\.(jpeg|jpg|gif|png|bmp|webp)/i.test(lastMessage.message || '') &&
80
+ ((lastMessage.message || '').includes('http') || (lastMessage.message || '').includes('/images/')));
81
+
82
+ if (hasFileAttachments) {
83
+ displayMessage = '📎 File attachment';
84
+ } else if (isImageMessage) {
85
+ displayMessage = '[Image]';
86
+ } else if (lastMessage.message && lastMessage.message.trim() !== '') {
87
+ displayMessage = lastMessage.message;
88
+ } else {
89
+ displayMessage = '(Empty message)';
90
+ }
91
+ }
92
+
93
+ const displayDate = lastMessage?.createdAt
94
+ ? createdAtText(lastMessage.createdAt)
95
+ : lastMessage?.updatedAt
96
+ ? createdAtText(lastMessage.updatedAt)
97
+ : '';
98
+
99
+ return (
100
+ <HStack space={'sm'} className="flex-1 justify-between">
101
+ <Box className="flex-[0.8]">
102
+ <Text color={colors.gray[600]} className="text-base text-wrap flex-wrap font-semibold">
103
+ {channelTitle}
104
+ </Text>
105
+ <Text color={colors.gray[600]} numberOfLines={1}>
106
+ {displayMessage}
107
+ </Text>
108
+ </Box>
109
+
110
+ <Box className="flex-[0.2]">
111
+ <Text color={colors.gray[500]}>{displayDate}</Text>
112
+ </Box>
113
+ </HStack>
114
+ );
115
+ };
116
+
117
+ export const DialogItemComponent: React.FC<IDialogItemProps> = function DialogItem({ currentUser, channel, onOpen }) {
118
+ const isMountedRef = useRef(true);
119
+ const [title, setTitle] = useState('');
120
+ const [subscriptionsTimestamp, setSubscriptionsTimestamp] = useState(Date.now());
121
+
122
+ const isServiceChannel = channel?.type === RoomType.Service;
123
+
124
+ // Use channel.lastMessage instead of computing it
125
+ const lastMessage = channel?.lastMessage;
126
+
127
+ // Service channel specific calculations
128
+ const creatorAndMembersId = useMemo(() => {
129
+ if (!isServiceChannel || !channel?.members) return null;
130
+ const membersIds: any =
131
+ channel?.members
132
+ ?.filter((m: any) => m !== null && m?.user?.id !== currentUser?.id)
133
+ ?.map((mu: any) => mu?.user?.id) ?? [];
134
+ const creatorId: any = channel?.creator?.id;
135
+ const mergedIds: any = [].concat(membersIds, creatorId) ?? [];
136
+ return mergedIds?.filter((m: any, pos: any) => mergedIds?.indexOf(m) === pos) ?? [];
137
+ }, [isServiceChannel, channel, currentUser]);
138
+
139
+ const postParentId = useMemo(() => {
140
+ if (!isServiceChannel || !creatorAndMembersId?.length) return null;
141
+ return creatorAndMembersId?.length && creatorAndMembersId?.includes(currentUser?.id)
142
+ ? null
143
+ : lastMessage?.parentId
144
+ ? lastMessage?.parentId
145
+ : lastMessage?.id ?? 0;
146
+ }, [isServiceChannel, creatorAndMembersId, lastMessage, currentUser]);
147
+
148
+ // Channel members computation for direct channels
149
+ const channelMembers = useMemo(() => {
150
+ if (isServiceChannel) return [];
151
+ return (
152
+ channel?.members
153
+ ?.filter((ch: any) => ch?.user?.id !== currentUser?.id && ch?.user?.__typename === 'UserAccount')
154
+ ?.map((m: any) => m?.user) ?? []
155
+ );
156
+ }, [isServiceChannel, currentUser?.id, channel?.members]);
157
+
158
+ // Set title when channel members change (for direct channels)
159
+ useEffect(() => {
160
+ if (!isServiceChannel && channelMembers.length > 0 && isMountedRef.current) {
161
+ const titleString =
162
+ channelMembers
163
+ ?.map((u: any) => `${u?.givenName || ''} ${u?.familyName || ''}`.trim())
164
+ ?.filter(Boolean)
165
+ ?.join(', ') || '';
166
+ setTitle(titleString);
167
+ } else if (isServiceChannel && channel?.title) {
168
+ setTitle(channel.title);
169
+ }
170
+ }, [isServiceChannel, channelMembers, channel?.title]);
171
+
172
+ // Compute display title
173
+ const displayTitle = useMemo(() => {
174
+ const length = 30;
175
+ const titleToUse = isServiceChannel ? channel?.title || '' : title;
176
+ return titleToUse.length > length ? titleToUse.substring(0, length - 3) + '...' : titleToUse;
177
+ }, [isServiceChannel, channel?.title, title]);
178
+
179
+ // Set mounted state on mount/unmount
180
+ useEffect(() => {
181
+ isMountedRef.current = true;
182
+ return () => {
183
+ isMountedRef.current = false;
184
+ };
185
+ }, []);
186
+
187
+ // Focus effect handling
188
+ useFocusEffect(
189
+ useCallback(() => {
190
+ if (!channel?.id) return;
191
+
192
+ console.log(`DialogItem focused for ${isServiceChannel ? 'service' : 'direct'} channel:`, channel?.id);
193
+
194
+ if (isServiceChannel) {
195
+ setSubscriptionsTimestamp(Date.now());
196
+ }
197
+ }, [channel?.id, isServiceChannel]),
198
+ );
199
+
200
+ // Handle press based on channel type
201
+ const handlePress = useCallback(() => {
202
+ if (isServiceChannel) {
203
+ onOpen(channel?.id, displayTitle, postParentId);
204
+ } else {
205
+ onOpen(channel?.id, displayTitle);
206
+ }
207
+ }, [isServiceChannel, channel?.id, displayTitle, postParentId, onOpen]);
208
+
209
+ // Render avatar based on channel type
210
+ const renderAvatar = () => {
211
+ if (isServiceChannel) {
212
+ return (
213
+ <Avatar
214
+ key={'service-channels-key-' + channel?.id}
215
+ size={'sm'}
216
+ className="bg-transparent top-0 right-0 z-[1]"
217
+ >
218
+ <AvatarFallbackText>{startCase(channel?.creator?.username?.charAt(0))}</AvatarFallbackText>
219
+ <AvatarImage
220
+ alt="user image"
221
+ style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
222
+ source={{
223
+ uri: channel?.creator?.picture,
224
+ }}
225
+ />
226
+ </Avatar>
227
+ );
228
+ }
229
+
230
+ return (
231
+ <AvatarGroup>
232
+ {channelMembers &&
233
+ channelMembers?.length > 0 &&
234
+ channelMembers?.slice(0, 1)?.map((ch: any, i: number) => (
235
+ <Avatar
236
+ key={'dialogs-list-' + i}
237
+ size={'sm'}
238
+ className={`bg-transparent top-[${i === 1 ? '4' : '0'}] right-[${
239
+ i === 1 ? '-2' : '0'
240
+ }] z-[${i === 1 ? 5 : 1}]`}
241
+ >
242
+ <AvatarFallbackText>{startCase(ch?.username?.charAt(0))}</AvatarFallbackText>
243
+ {channelMembers?.length > 1 && (
244
+ <AvatarBadge
245
+ style={{
246
+ width: '100%',
247
+ height: '100%',
248
+ backgroundColor: '#e5e7eb',
249
+ borderRadius: 5,
250
+ }}
251
+ className="items-center justify-center bg-gray-200 rounded-md"
252
+ >
253
+ <Text style={{ fontSize: 12, fontWeight: 'bold', color: '#000' }}>
254
+ {channelMembers?.length}
255
+ </Text>
256
+ </AvatarBadge>
257
+ )}
258
+ {channelMembers?.length === 1 && (
259
+ <>
260
+ <AvatarImage
261
+ alt="user image"
262
+ style={{ borderRadius: 6, borderWidth: 2, borderColor: '#fff' }}
263
+ source={{
264
+ uri: ch?.picture,
265
+ }}
266
+ />
267
+ <AvatarBadge style={{ width: 10, height: 10 }} />
268
+ </>
269
+ )}
270
+ </Avatar>
271
+ ))}
272
+ </AvatarGroup>
273
+ );
274
+ };
275
+
276
+ return (
277
+ <Pressable
278
+ onPress={handlePress}
279
+ className="flex-1 border-gray-200 rounded-md dark:border-gray-600 dark:bg-gray-700"
280
+ style={{
281
+ borderBottomWidth: 1,
282
+ borderColor: '#e5e7eb',
283
+ marginVertical: 0,
284
+ paddingHorizontal: isServiceChannel ? 2 : 10,
285
+ }}
286
+ >
287
+ <HStack space={'md'} className="flex-1 w-[100%] py-3 items-center">
288
+ <Box className="flex-[0.1] items-start pl-3">{renderAvatar()}</Box>
289
+ <Box className="flex-1">
290
+ <LastMessageComponent
291
+ key={
292
+ isServiceChannel
293
+ ? `service-channel-${channel?.id}-${subscriptionsTimestamp}`
294
+ : `last-msg-${lastMessage?.id || 'none'}-${channelMembers.length}`
295
+ }
296
+ title={displayTitle}
297
+ lastMessage={lastMessage}
298
+ isServiceChannel={isServiceChannel}
299
+ />
300
+ </Box>
301
+ </HStack>
302
+ </Pressable>
303
+ );
304
+ };
305
+
306
+ export const DialogItem = React.memo(DialogItemComponent);
@@ -15,22 +15,15 @@ export function DialogsHeader({
15
15
  title: string;
16
16
  }) {
17
17
  return (
18
- <HStack px={'$4'} py={'$2'} style={style} alignItems="center" justifyContent="space-between">
19
- <View flex={1}>{onBack ? <AntDesign onPress={onBack} name="left" size={18} /> : null}</View>
20
- <View style={{ flexDirection: 'row', flex: 1, flexGrow: 1 }} flex={1}>
21
- <Text
22
- numberOfLines={1}
23
- flexShrink={1}
24
- textAlign="center"
25
- style={{ fontSize: 20 }}
26
- flexGrow={1}
27
- fontWeight="$semibold"
28
- >
18
+ <HStack className="px-4 py-2 items-center justify-between" style={style}>
19
+ <View style={{ flex: 1 }}>{onBack ? <AntDesign onPress={onBack} name="left" size={18} /> : null}</View>
20
+ <View style={{ flexDirection: 'row', flex: 1, flexGrow: 1 }}>
21
+ <Text numberOfLines={1} style={{ fontSize: 20 }} className="shrink font-semibold text-center grow">
29
22
  {title || ' '}
30
23
  </Text>
31
24
  </View>
32
- <View flex={1} alignItems="flex-end" justifyContent="center">
33
- <Text textAlign="right">{extra}</Text>
25
+ <View style={{ flex: 1, alignItems: 'flex-end', justifyContent: 'center' }}>
26
+ <Text className="text-right">{extra}</Text>
34
27
  </View>
35
28
  </HStack>
36
29
  );