@messenger-box/platform-mobile 10.0.3-alpha.47 → 10.0.3-alpha.48
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/queries/inboxQueries.js +2 -13
- package/lib/queries/inboxQueries.js.map +1 -1
- package/lib/screens/inbox/components/DialogItem.js +169 -0
- package/lib/screens/inbox/components/DialogItem.js.map +1 -0
- package/lib/screens/inbox/containers/Dialogs.js +10 -20
- package/lib/screens/inbox/containers/Dialogs.js.map +1 -1
- package/package.json +4 -4
- package/src/screens/inbox/components/DialogItem.tsx +306 -0
- package/src/screens/inbox/components/DialogsListItem.tsx +140 -305
- package/src/screens/inbox/components/ServiceDialogsListItem.tsx +171 -264
- package/src/screens/inbox/containers/Dialogs.tsx +104 -45
- package/lib/screens/inbox/components/DialogsListItem.js +0 -355
- package/lib/screens/inbox/components/DialogsListItem.js.map +0 -1
- package/lib/screens/inbox/components/ServiceDialogsListItem.js +0 -402
- package/lib/screens/inbox/components/ServiceDialogsListItem.js.map +0 -1
|
@@ -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);
|