@droppii-org/chat-mobile 0.2.66 → 0.2.67

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 (119) hide show
  1. package/lib/module/components/Avatar/Avatar.js +39 -2
  2. package/lib/module/components/Avatar/Avatar.js.map +1 -1
  3. package/lib/module/components/Avatar/DoubleAvatar.js +18 -18
  4. package/lib/module/components/Avatar/DoubleAvatar.js.map +1 -1
  5. package/lib/module/components/Avatar/QuadAvatar.js +131 -0
  6. package/lib/module/components/Avatar/QuadAvatar.js.map +1 -0
  7. package/lib/module/components/Avatar/TripleAvatar.js +97 -0
  8. package/lib/module/components/Avatar/TripleAvatar.js.map +1 -0
  9. package/lib/module/components/Avatar/index.js +2 -0
  10. package/lib/module/components/Avatar/index.js.map +1 -1
  11. package/lib/module/components/ThreadCard/AvatarSection.js +41 -0
  12. package/lib/module/components/ThreadCard/AvatarSection.js.map +1 -1
  13. package/lib/module/components/ThreadCard/NamePrefixIcon.js +5 -4
  14. package/lib/module/components/ThreadCard/NamePrefixIcon.js.map +1 -1
  15. package/lib/module/components/ThreadCard/ThreadCard.js +3 -1
  16. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  17. package/lib/module/components/messages/groupNotificationMessage/index.js +25 -0
  18. package/lib/module/components/messages/groupNotificationMessage/index.js.map +1 -0
  19. package/lib/module/context/ChatContext.js +1 -1
  20. package/lib/module/context/ChatContext.js.map +1 -1
  21. package/lib/module/context/ChatDetailContext.js +9 -0
  22. package/lib/module/context/ChatDetailContext.js.map +1 -1
  23. package/lib/module/core/useChatListener.js +120 -21
  24. package/lib/module/core/useChatListener.js.map +1 -1
  25. package/lib/module/hooks/conversation/useGetGroupInfo.js +3 -1
  26. package/lib/module/hooks/conversation/useGetGroupInfo.js.map +1 -1
  27. package/lib/module/hooks/message/useSendMessage.js +6 -3
  28. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  29. package/lib/module/hooks/useConversationList.js +9 -4
  30. package/lib/module/hooks/useConversationList.js.map +1 -1
  31. package/lib/module/screens/ChatInfo/ChatInfoProfile.js +4 -2
  32. package/lib/module/screens/ChatInfo/ChatInfoProfile.js.map +1 -1
  33. package/lib/module/screens/chat-detail/ChatComposer.js +11 -6
  34. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  35. package/lib/module/screens/chat-detail/ChatDetail.js +7 -0
  36. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  37. package/lib/module/screens/chat-detail/ChatDetailHeader.js +10 -3
  38. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  39. package/lib/module/screens/chat-detail/conversationHeader.utils.js +13 -3
  40. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -1
  41. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +29 -2
  42. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  43. package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
  44. package/lib/module/store/conversation.js +56 -12
  45. package/lib/module/store/conversation.js.map +1 -1
  46. package/lib/module/translation/resources/i18n.js +31 -0
  47. package/lib/module/translation/resources/i18n.js.map +1 -1
  48. package/lib/module/types/events.js +1 -0
  49. package/lib/module/types/events.js.map +1 -1
  50. package/lib/module/utils/messageUtils.js +95 -2
  51. package/lib/module/utils/messageUtils.js.map +1 -1
  52. package/lib/typescript/src/components/Avatar/Avatar.d.ts +1 -1
  53. package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -1
  54. package/lib/typescript/src/components/Avatar/Avatar.types.d.ts +5 -1
  55. package/lib/typescript/src/components/Avatar/Avatar.types.d.ts.map +1 -1
  56. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts.map +1 -1
  57. package/lib/typescript/src/components/Avatar/QuadAvatar.d.ts +16 -0
  58. package/lib/typescript/src/components/Avatar/QuadAvatar.d.ts.map +1 -0
  59. package/lib/typescript/src/components/Avatar/TripleAvatar.d.ts +15 -0
  60. package/lib/typescript/src/components/Avatar/TripleAvatar.d.ts.map +1 -0
  61. package/lib/typescript/src/components/Avatar/index.d.ts +2 -0
  62. package/lib/typescript/src/components/Avatar/index.d.ts.map +1 -1
  63. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +2 -2
  64. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  65. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +3 -1
  66. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
  67. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  68. package/lib/typescript/src/components/messages/groupNotificationMessage/index.d.ts +3 -0
  69. package/lib/typescript/src/components/messages/groupNotificationMessage/index.d.ts.map +1 -0
  70. package/lib/typescript/src/context/ChatDetailContext.d.ts +2 -0
  71. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
  72. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  73. package/lib/typescript/src/hooks/conversation/useGetGroupInfo.d.ts.map +1 -1
  74. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  75. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  76. package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts.map +1 -1
  77. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  78. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  79. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  80. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +1 -1
  81. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -1
  82. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  83. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  84. package/lib/typescript/src/store/conversation.d.ts +11 -0
  85. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  86. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  87. package/lib/typescript/src/types/events.d.ts +6 -1
  88. package/lib/typescript/src/types/events.d.ts.map +1 -1
  89. package/lib/typescript/src/utils/messageUtils.d.ts +6 -1
  90. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  91. package/package.json +2 -2
  92. package/src/build-ignore.d.ts +1 -0
  93. package/src/components/Avatar/Avatar.tsx +47 -0
  94. package/src/components/Avatar/Avatar.types.ts +5 -1
  95. package/src/components/Avatar/DoubleAvatar.tsx +16 -13
  96. package/src/components/Avatar/QuadAvatar.tsx +161 -0
  97. package/src/components/Avatar/TripleAvatar.tsx +119 -0
  98. package/src/components/Avatar/index.ts +2 -0
  99. package/src/components/ThreadCard/AvatarSection.tsx +58 -2
  100. package/src/components/ThreadCard/NamePrefixIcon.tsx +32 -25
  101. package/src/components/ThreadCard/ThreadCard.tsx +2 -0
  102. package/src/components/messages/groupNotificationMessage/index.tsx +28 -0
  103. package/src/context/ChatContext.tsx +1 -1
  104. package/src/context/ChatDetailContext.tsx +15 -0
  105. package/src/core/useChatListener.ts +175 -23
  106. package/src/hooks/conversation/useGetGroupInfo.ts +2 -0
  107. package/src/hooks/message/useSendMessage.ts +11 -3
  108. package/src/hooks/useConversationList.ts +17 -7
  109. package/src/screens/ChatInfo/ChatInfoProfile.tsx +6 -3
  110. package/src/screens/chat-detail/ChatComposer.tsx +19 -10
  111. package/src/screens/chat-detail/ChatDetail.tsx +12 -1
  112. package/src/screens/chat-detail/ChatDetailHeader.tsx +19 -3
  113. package/src/screens/chat-detail/conversationHeader.utils.ts +14 -6
  114. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +36 -1
  115. package/src/screens/inbox/MessagesTab.tsx +0 -1
  116. package/src/store/conversation.ts +62 -17
  117. package/src/translation/resources/i18n.ts +31 -0
  118. package/src/types/events.ts +6 -0
  119. package/src/utils/messageUtils.ts +103 -1
@@ -2,12 +2,15 @@ import OpenIMSDK, {
2
2
  MessageReceiveOptType,
3
3
  MessageType,
4
4
  OpenIMEvent,
5
+ SessionType,
5
6
  type ConversationItem,
7
+ type GroupItem,
6
8
  type GroupMemberItem,
7
9
  type MessageItem,
8
10
  type MessagePinned,
9
11
  type RevokedInfo,
10
12
  } from '@droppii/openim-rn-client-sdk';
13
+ import { useQueryClient } from '@tanstack/react-query';
11
14
  import { useCallback, useEffect } from 'react';
12
15
  import { useConversationStore } from '../store/conversation';
13
16
  import { ToastFlashMessage } from '../utils/toastFlashMessage';
@@ -18,22 +21,31 @@ import isEmpty from 'lodash/isEmpty';
18
21
  import { markMessageRevoked, useMessageStore } from '../store/message';
19
22
  import { syncPinnedMessagesOnEvent } from '../hooks/message/usePinMessage';
20
23
  import { belongsToConversation } from '../utils/message';
24
+ import { getConversationID } from '../utils/conversation';
21
25
  import { DeviceEventEmitter } from 'react-native';
22
26
  import { ChatEvent } from '../types/events';
27
+ import { conversationQueryKeys } from '../hooks/query-keys';
23
28
 
24
29
  export const useChatListener = () => {
25
30
  const updateOneMessage = useMessageStore((state) => state.updateOneMessage);
26
31
  const currentUserId = useUserStore((s) => s.user?.userID);
27
32
  const onPressFlashMessage = useChatContext()?.onPressFlashMessage;
28
33
  const isShowFLashMessage = useChatContext()?.isShowFLashMessage;
34
+ const queryClient = useQueryClient();
29
35
 
30
36
  const onSendMessageProgress = useCallback(
31
- ({ progress, message }: { progress: number; message: MessageItem }) => {
32
- console.log('[Listener] SendMessageProgress', {
33
- clientMsgID: message.clientMsgID,
34
- status: message.status,
35
- progress,
36
- });
37
+ ({
38
+ progress: _progress,
39
+ message,
40
+ }: {
41
+ progress: number;
42
+ message: MessageItem;
43
+ }) => {
44
+ // console.log('[Listener] SendMessageProgress', {
45
+ // clientMsgID: message.clientMsgID,
46
+ // status: message.status,
47
+ // progress,
48
+ // });
37
49
  updateOneMessage(message);
38
50
  },
39
51
  [updateOneMessage]
@@ -41,6 +53,7 @@ export const useChatListener = () => {
41
53
 
42
54
  const onConversationChanged = useCallback(
43
55
  (conversations: ConversationItem[]) => {
56
+ // console.log('[Listener] OnConversationChanged', conversations);
44
57
  const oldMap = useConversationStore.getState().map;
45
58
  const visibleConversations: ConversationItem[] = [];
46
59
 
@@ -87,26 +100,156 @@ export const useChatListener = () => {
87
100
  );
88
101
 
89
102
  const onNewConversation = useCallback((conversations: ConversationItem[]) => {
90
- console.log('[Listener] OnNewConversation', conversations);
103
+ // console.log(
104
+ // '[Listener] OnNewConversation',
105
+ // conversations.map((c) => ({
106
+ // conversationID: c.conversationID,
107
+ // groupID: c.groupID,
108
+ // isNotInGroup: c.isNotInGroup,
109
+ // isGuardedRemoved: c.groupID
110
+ // ? useConversationStore.getState().removedGroupIds.has(c.groupID)
111
+ // : false,
112
+ // }))
113
+ // );
91
114
  useConversationStore.getState().updateConversations(conversations);
92
115
  }, []);
93
116
 
94
- const onGroupMemberAdded = useCallback((member: GroupMemberItem) => {
95
- console.log('[Listener] OnGroupMemberAdded', {
96
- userID: member.userID,
97
- groupID: member.groupID,
98
- });
99
- }, []);
117
+ const onGroupInfoChanged = useCallback(
118
+ (group: GroupItem) => {
119
+ // console.log('[Listener] OnGroupInfoChanged', {
120
+ // groupID: group.groupID,
121
+ // memberCount: group.memberCount,
122
+ // });
123
+ queryClient.setQueryData(
124
+ conversationQueryKeys.groupInfo(group.groupID),
125
+ group
126
+ );
127
+ },
128
+ [queryClient]
129
+ );
100
130
 
101
- const onGroupMemberDeleted = useCallback((member: GroupMemberItem) => {
102
- console.log('[Listener] OnGroupMemberDeleted', {
103
- userID: member.userID,
104
- groupID: member.groupID,
105
- });
106
- }, []);
131
+ const onGroupMemberAdded = useCallback(
132
+ (member: GroupMemberItem) => {
133
+ // console.log('[Listener] OnGroupMemberAdded', {
134
+ // userID: member.userID,
135
+ // groupID: member.groupID,
136
+ // isCurrentUser: member.userID === currentUserId,
137
+ // });
138
+ queryClient.invalidateQueries({
139
+ queryKey: conversationQueryKeys.groupInfo(member.groupID),
140
+ });
141
+
142
+ // The current user (re)joined this group — lift the removal guard so
143
+ // a future OnConversationChanged/OnNewConversation for it isn't dropped.
144
+ if (member.userID === currentUserId) {
145
+ useConversationStore
146
+ .getState()
147
+ .restoreGroupConversation(member.groupID);
148
+ }
149
+ },
150
+ [queryClient, currentUserId]
151
+ );
152
+
153
+ const removeGroupConversation = useCallback(
154
+ (
155
+ groupID: string,
156
+ groupName: string | undefined,
157
+ reason: 'left' | 'dismissed' | 'kicked'
158
+ ) => {
159
+ // console.log('[Listener] removeGroupConversation', { groupID, reason });
160
+
161
+ // TEMPORARILY DISABLED: OnConversationChanged/OnNewConversation can
162
+ // still race this removal and resurrect the conversation before the
163
+ // removedGroupIds guard is proven reliable. Flip this back to `true`
164
+ // once that race is confirmed fixed.
165
+ const REMOVE_FLOW_ENABLED: boolean = false;
166
+ if (!REMOVE_FLOW_ENABLED) return;
167
+
168
+ const conversationID = getConversationID(SessionType.Group, groupID);
169
+ const conversation = useConversationStore.getState().map[conversationID];
170
+ const isCurrentConversation =
171
+ useConversationStore.getState().currentConversationId ===
172
+ conversationID;
173
+
174
+ // Marks groupID as removed so any late-arriving conversation-sync event
175
+ // for it (see onConversationChanged/onNewConversation) is dropped
176
+ // instead of silently resurrecting the conversation we just removed.
177
+ useConversationStore.getState().removeGroupConversation(groupID);
178
+
179
+ queryClient.removeQueries({
180
+ queryKey: conversationQueryKeys.groupInfo(groupID),
181
+ });
182
+
183
+ ToastFlashMessage.show({
184
+ key: `group-removed-${groupID}`,
185
+ title: groupName || conversation?.showName || 'Nhóm',
186
+ body:
187
+ reason === 'dismissed'
188
+ ? 'Nhóm đã bị giải tán'
189
+ : reason === 'kicked'
190
+ ? 'Bạn đã bị mời khỏi nhóm'
191
+ : 'Bạn không còn trong nhóm này',
192
+ });
193
+
194
+ if (isCurrentConversation) {
195
+ DeviceEventEmitter.emit(ChatEvent.GroupRemoved, {
196
+ groupID,
197
+ reason,
198
+ });
199
+ }
200
+ },
201
+ [queryClient]
202
+ );
203
+
204
+ const onGroupMemberDeleted = useCallback(
205
+ (member: GroupMemberItem) => {
206
+ // console.log('[Listener] OnGroupMemberDeleted', {
207
+ // userID: member.userID,
208
+ // groupID: member.groupID,
209
+ // isCurrentUser: member.userID === currentUserId,
210
+ // });
211
+ queryClient.invalidateQueries({
212
+ queryKey: conversationQueryKeys.groupInfo(member.groupID),
213
+ });
214
+
215
+ // The removed member is the current user themselves — they were kicked,
216
+ // so their conversation for this group must be dropped like a leave/dismiss.
217
+ if (member.userID === currentUserId) {
218
+ removeGroupConversation(member.groupID, undefined, 'kicked');
219
+ }
220
+ },
221
+ [queryClient, currentUserId, removeGroupConversation]
222
+ );
223
+
224
+ const onJoinedGroupDeleted = useCallback(
225
+ (group: GroupItem) => {
226
+ // console.log('[Listener] OnJoinedGroupDeleted', {
227
+ // groupID: group.groupID,
228
+ // groupName: group.groupName,
229
+ // });
230
+ removeGroupConversation(group.groupID, group.groupName, 'left');
231
+ },
232
+ [removeGroupConversation]
233
+ );
234
+
235
+ const onGroupDismissed = useCallback(
236
+ (group: GroupItem) => {
237
+ // console.log('[Listener] OnGroupDismissed', {
238
+ // groupID: group.groupID,
239
+ // groupName: group.groupName,
240
+ // });
241
+ removeGroupConversation(group.groupID, group.groupName, 'dismissed');
242
+ },
243
+ [removeGroupConversation]
244
+ );
107
245
 
108
246
  const OnGroupMemberInfoChanged = useCallback(
109
247
  (member: GroupMemberItem) => {
248
+ // console.log('[Listener] OnGroupMemberInfoChanged', {
249
+ // userID: member.userID,
250
+ // groupID: member.groupID,
251
+ // permissions: member.permissions,
252
+ // });
110
253
  const conversation = useConversationStore
111
254
  .getState()
112
255
  .getCurrentConversation();
@@ -148,7 +291,7 @@ export const useChatListener = () => {
148
291
  );
149
292
 
150
293
  const onRecvNewMessages = useCallback((messages: MessageItem[]) => {
151
- console.log('[Listener] OnRecvNewMessages', messages);
294
+ // console.log('[Listener] OnRecvNewMessages', messages);
152
295
  const conversationId =
153
296
  useConversationStore.getState().currentConversationId;
154
297
  const targetUserId = useConversationStore.getState().currentUserId;
@@ -164,8 +307,8 @@ export const useChatListener = () => {
164
307
  relevant.forEach((msg) => useMessageStore.getState().pushNewMessage(msg));
165
308
  }, []);
166
309
 
167
- const onRecvOfflineNewMessages = useCallback((messages: MessageItem[]) => {
168
- console.log('[Listener] onRecvOfflineNewMessages', messages);
310
+ const onRecvOfflineNewMessages = useCallback((_messages: MessageItem[]) => {
311
+ // console.log('[Listener] onRecvOfflineNewMessages', _messages);
169
312
  }, []);
170
313
 
171
314
  useEffect(() => {
@@ -174,12 +317,15 @@ export const useChatListener = () => {
174
317
  OpenIMSDK.on(OpenIMEvent.OnNewConversation, onNewConversation);
175
318
 
176
319
  // Group members
320
+ OpenIMSDK.on(OpenIMEvent.OnGroupInfoChanged, onGroupInfoChanged);
177
321
  OpenIMSDK.on(OpenIMEvent.OnGroupMemberAdded, onGroupMemberAdded);
178
322
  OpenIMSDK.on(OpenIMEvent.OnGroupMemberDeleted, onGroupMemberDeleted);
179
323
  OpenIMSDK.on(
180
324
  OpenIMEvent.OnGroupMemberInfoChanged,
181
325
  OnGroupMemberInfoChanged
182
326
  );
327
+ OpenIMSDK.on(OpenIMEvent.OnJoinedGroupDeleted, onJoinedGroupDeleted);
328
+ OpenIMSDK.on(OpenIMEvent.OnGroupDismissed, onGroupDismissed);
183
329
 
184
330
  // Message status
185
331
  OpenIMSDK.on(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
@@ -197,12 +343,15 @@ export const useChatListener = () => {
197
343
  OpenIMSDK.off(OpenIMEvent.OnNewConversation, onNewConversation);
198
344
 
199
345
  // Group members
346
+ OpenIMSDK.off(OpenIMEvent.OnGroupInfoChanged, onGroupInfoChanged);
200
347
  OpenIMSDK.off(OpenIMEvent.OnGroupMemberAdded, onGroupMemberAdded);
201
348
  OpenIMSDK.off(OpenIMEvent.OnGroupMemberDeleted, onGroupMemberDeleted);
202
- OpenIMSDK.on(
349
+ OpenIMSDK.off(
203
350
  OpenIMEvent.OnGroupMemberInfoChanged,
204
351
  OnGroupMemberInfoChanged
205
352
  );
353
+ OpenIMSDK.off(OpenIMEvent.OnJoinedGroupDeleted, onJoinedGroupDeleted);
354
+ OpenIMSDK.off(OpenIMEvent.OnGroupDismissed, onGroupDismissed);
206
355
 
207
356
  // Message status
208
357
  OpenIMSDK.off(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
@@ -216,6 +365,7 @@ export const useChatListener = () => {
216
365
  };
217
366
  }, [
218
367
  onConversationChanged,
368
+ onGroupInfoChanged,
219
369
  onGroupMemberAdded,
220
370
  onGroupMemberDeleted,
221
371
  onMessagePinned,
@@ -225,5 +375,7 @@ export const useChatListener = () => {
225
375
  onRecvNewMessages,
226
376
  onRecvOfflineNewMessages,
227
377
  OnGroupMemberInfoChanged,
378
+ onJoinedGroupDeleted,
379
+ onGroupDismissed,
228
380
  ]);
229
381
  };
@@ -15,4 +15,6 @@ export const useGetGroupInfo = (groupId = '') =>
15
15
  }
16
16
  },
17
17
  enabled: !!groupId,
18
+ staleTime: 0,
19
+ refetchOnMount: 'always',
18
20
  });
@@ -268,14 +268,22 @@ export const generateOfflinePushInfo = (message: MessageItem) => {
268
268
  const conversationData = useConversationStore
269
269
  ?.getState?.()
270
270
  ?.getCurrentConversation();
271
- const operatorUserID = useUserStore.getState().user?.userID || '';
272
- const title = conversationData?.showName || 'Droppii';
271
+ const operator = useUserStore.getState().user;
272
+ const operatorUserID = operator?.userID || '';
273
+ const isSingleChat =
274
+ conversationData?.conversationType === SessionType.Single;
275
+ const title = isSingleChat
276
+ ? operator?.nickname || 'Droppii'
277
+ : conversationData?.showName || 'Droppii';
278
+ const icon = isSingleChat
279
+ ? operator?.faceURL || ''
280
+ : conversationData?.faceURL || '';
273
281
  const desc = getMessagePreview(message);
274
282
  return {
275
283
  title,
276
284
  desc,
277
285
  ex: JSON.stringify({
278
- icon: conversationData?.faceURL || '',
286
+ icon,
279
287
  conversationId: conversationData?.conversationID || '',
280
288
  userId: operatorUserID,
281
289
  groupId: conversationData?.groupID || '',
@@ -25,14 +25,24 @@ export function useConversationList({
25
25
  enabled: enabled !== false,
26
26
  queryFn: async () => {
27
27
  const offset = (page - 1) * 500;
28
- const conversations = await OpenIMSDK.getConversationListSplitApp({
29
- offset,
30
- count: 500,
31
- applicationType,
32
- });
28
+ const [conversations, joinedGroups] = await Promise.all([
29
+ OpenIMSDK.getConversationListSplitApp({
30
+ offset,
31
+ count: 500,
32
+ applicationType,
33
+ }),
34
+ OpenIMSDK.getJoinedGroupList(),
35
+ ]);
33
36
 
34
- useConversationStore.getState().updateConversations(conversations);
35
- return conversations;
37
+ const joinedGroupIds = new Set(joinedGroups.map((g) => g.groupID));
38
+ const resolved = conversations.map((conversation) =>
39
+ conversation.groupID && !joinedGroupIds.has(conversation.groupID)
40
+ ? { ...conversation, isNotInGroup: true }
41
+ : conversation
42
+ );
43
+
44
+ useConversationStore.getState().updateConversations(resolved);
45
+ return resolved;
36
46
  },
37
47
  });
38
48
 
@@ -93,12 +93,15 @@ const ChatInfoProfile = memo(
93
93
  fullName={showName}
94
94
  badge={badge}
95
95
  />
96
- <KContainer.View row alignItems marginT="0.5rem">
97
- <NamePrefixIcon peerType={conversation?.peerType} />
96
+ <KContainer.View row alignItems marginT="0.5rem" paddingH={'0.75rem'}>
97
+ <NamePrefixIcon
98
+ peerType={conversation?.peerType}
99
+ marginR="0.5rem"
100
+ />
98
101
  <KLabel.Text
99
102
  typo="TextLgBold"
100
103
  color={KColors.black}
101
- numberOfLines={1}
104
+ numberOfLines={2}
102
105
  >
103
106
  {showName}
104
107
  </KLabel.Text>
@@ -63,7 +63,11 @@ import { ChatCurrentQuotedMessage } from './ChatCurrentQuotedMessage';
63
63
  import { useSendMessage } from '../../hooks/message/useSendMessage';
64
64
  import { openBotMenuSheet } from './BotMenuSheet';
65
65
  import { UIUtils } from '../../utils/UIUtils';
66
- import { GroupPermission, SessionType } from '@droppii/openim-rn-client-sdk';
66
+ import {
67
+ GroupPermission,
68
+ PeerType,
69
+ SessionType,
70
+ } from '@droppii/openim-rn-client-sdk';
67
71
  import { useConversationStore } from '../../store';
68
72
 
69
73
  interface ChatComposerInternalProps {
@@ -88,10 +92,12 @@ export const ChatComposer = memo(
88
92
 
89
93
  const {
90
94
  isBotConversation,
95
+ conversation,
91
96
  conversationFlow,
92
97
  conversationFlowMainMenu,
93
98
  flowState,
94
99
  } = useChatDetailContext();
100
+ const isGroupConversation = conversation?.peerType === PeerType.Group;
95
101
  const { mutate: runFlow, isPending } = useRunFlow();
96
102
  const { mutate: resetFlow } = useResetFlow();
97
103
  const { mutateAsync: closeSupportSession } = useCloseSupportSession();
@@ -409,7 +415,7 @@ export const ChatComposer = memo(
409
415
  conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
410
416
  isEmptyMessage;
411
417
 
412
- if (isDisableCompose) {
418
+ if (isDisableCompose && !isGroupConversation) {
413
419
  content = (
414
420
  <KContainer.View row alignItems justifyContent>
415
421
  <KContainer.View marginR="0.25rem">
@@ -441,14 +447,17 @@ export const ChatComposer = memo(
441
447
  content = (
442
448
  <KContainer.View background={'#F9F9FA'}>
443
449
  <ChatCurrentQuotedMessage />
444
- <ChatQuickActions
445
- visible
446
- actions={DEFAULT_QUICK_ACTIONS}
447
- onActionPress={(action) =>
448
- console.log('[ChatComposer] quick action:', action.id)
449
- }
450
- />
451
-
450
+ <KContainer.VisibleView
451
+ visible={conversation?.peerType === PeerType.Bot}
452
+ >
453
+ <ChatQuickActions
454
+ visible
455
+ actions={DEFAULT_QUICK_ACTIONS}
456
+ onActionPress={(action) =>
457
+ console.log('[ChatComposer] quick action:', action.id)
458
+ }
459
+ />
460
+ </KContainer.VisibleView>
452
461
  {compose.showLinkPreview && (
453
462
  <ChatLinkPreview
454
463
  metadata={compose.urlMetadata}
@@ -32,7 +32,7 @@ import {
32
32
  } from '../../hooks/conversation/useGetConversationDetail';
33
33
  import type { MessageItem } from '@droppii/openim-rn-client-sdk';
34
34
  import { useMessages } from '../../hooks/message/useMessages';
35
- import { ChatEvent } from '../../types/events';
35
+ import { ChatEvent, type GroupRemovedPayload } from '../../types/events';
36
36
  import { useSetIsShowFLashMessage } from '../../context/global';
37
37
 
38
38
  const ChatDetail = memo(
@@ -112,6 +112,17 @@ const ChatDetail = memo(
112
112
  };
113
113
  }, [conversationId, targetUserId, targetGroupId, setShowFLashMessage]);
114
114
 
115
+ useEffect(() => {
116
+ const sub = DeviceEventEmitter.addListener(
117
+ ChatEvent.GroupRemoved,
118
+ (payload: GroupRemovedPayload) => {
119
+ if (payload.groupID !== targetGroupId) return;
120
+ (onBack ?? handleBackDefault)();
121
+ }
122
+ );
123
+ return () => sub.remove();
124
+ }, [targetGroupId, onBack, handleBackDefault]);
125
+
115
126
  const resolvedShowAddMember =
116
127
  showAddMember ?? shouldShowAddMember(conversation);
117
128
 
@@ -12,6 +12,8 @@ import type { ChatDetailHeaderProps } from './types';
12
12
  import { useConversation } from '../../store';
13
13
  import { getConversationSubtitle } from './conversationHeader.utils';
14
14
  import { useGetUsersInfo } from '../../hooks/users/useGetUsersInfo';
15
+ import { useGetGroupInfo } from '../../hooks/conversation/useGetGroupInfo';
16
+ import { isGroupConversation } from '../../utils/conversation';
15
17
 
16
18
  const ChatDetailHeader = memo(
17
19
  ({
@@ -28,7 +30,10 @@ const ChatDetailHeader = memo(
28
30
  const { data: usersInfo } = useGetUsersInfo(
29
31
  targetUserId ? [targetUserId] : []
30
32
  );
31
-
33
+ const isGroup = isGroupConversation(conversation);
34
+ const { data: groupInfo } = useGetGroupInfo(
35
+ isGroup ? conversation?.groupID : undefined
36
+ );
32
37
  const { faceURL, showName } = useMemo(() => {
33
38
  const userInfo = usersInfo?.[0];
34
39
  const resolvedFaceURL = conversation?.faceURL || userInfo?.faceURL;
@@ -103,6 +108,7 @@ const ChatDetailHeader = memo(
103
108
  avatar={faceURL || null}
104
109
  fullName={showName || ''}
105
110
  peerType={conversation?.peerType}
111
+ members={conversation?.members}
106
112
  />
107
113
 
108
114
  <KContainer.View flex marginL="0.5rem">
@@ -123,14 +129,24 @@ const ChatDetailHeader = memo(
123
129
  color={KColors.palette.gray.w500}
124
130
  numberOfLines={1}
125
131
  >
126
- {getConversationSubtitle(conversation)}
132
+ {getConversationSubtitle(
133
+ conversation,
134
+ groupInfo?.memberCount
135
+ )}
127
136
  </KLabel.Text>
128
137
  )}
129
138
  </KContainer.View>
130
139
  </KContainer.Touchable>
131
140
  </KContainer.View>
132
141
  ),
133
- [onBack, onPressAvatar, conversation, faceURL, showName]
142
+ [
143
+ onBack,
144
+ onPressAvatar,
145
+ conversation,
146
+ faceURL,
147
+ showName,
148
+ groupInfo?.memberCount,
149
+ ]
134
150
  );
135
151
 
136
152
  return (
@@ -18,18 +18,26 @@ export const getConversationAvatarUri = (
18
18
  };
19
19
 
20
20
  export const getConversationSubtitle = (
21
- conversation?: ConversationItem
21
+ conversation?: ConversationItem,
22
+ memberCount?: number
22
23
  ): string | undefined => {
23
24
  if (!conversation) {
24
25
  return undefined;
25
26
  }
26
27
 
27
28
  if (conversation.peerType === PeerType.Group) {
28
- const unreadLabel =
29
- (conversation.unreadCount ?? 0) > 0
30
- ? `${conversation.unreadCount} tin nhắn mới`
31
- : 'Không có tin mới';
32
- return `20 thành viên • ${unreadLabel}`;
29
+ if (typeof memberCount !== 'number') {
30
+ return 'Nhấn để xem thông tin';
31
+ }
32
+
33
+ // const unreadLabel =
34
+ // (conversation.unreadCount ?? 0) > 0
35
+ // ? `${conversation.unreadCount} tin nhắn mới`
36
+ // : 'Không có tin mới';
37
+ // if (memberCount == null) {
38
+ // return unreadLabel;
39
+ // }
40
+ return `${memberCount} thành viên`;
33
41
  }
34
42
 
35
43
  if (conversation.peerType === PeerType.Customer) {
@@ -1,4 +1,4 @@
1
- import { KContainer, KImage, KColors } from '@droppii/libs';
1
+ import { KContainer, KImage, KColors, KSpacingValue } from '@droppii/libs';
2
2
  import { StyleSheet } from 'react-native';
3
3
  import { memo, useCallback, useMemo } from 'react';
4
4
  import type { ComponentType, PropsWithChildren, ReactNode } from 'react';
@@ -14,11 +14,13 @@ import { FileMessage } from '../../../components/messages/fileMessage';
14
14
  import { LinkMessage } from '../../../components/messages/linkMessage';
15
15
  import { MergedMessage } from '../../../components/messages/mergedMessage';
16
16
  import { RevokedMessage } from '../../../components/messages/revokedMessage';
17
+ import { GroupNotificationMessage } from '../../../components/messages/groupNotificationMessage';
17
18
  import { MessageStatusIndicator } from '../../../components/messages/MessageStatusIndicator';
18
19
  import type { BaseLegendMessageProps } from '../../../components/messages/types';
19
20
  import { messageStyles } from './messageTypes';
20
21
  import QuotedMessage from '../../../components/messages/quotedMessage';
21
22
  import { StickerMessage } from '../../../components/messages/stickerMessage';
23
+ import { GROUP_NOTIFICATION_TEXT } from '../../../utils/messageUtils';
22
24
 
23
25
  type MessageProps = BaseLegendMessageProps & {
24
26
  onPressUrl?: (url: string) => void;
@@ -38,6 +40,16 @@ interface LegendChatMessageProps {
38
40
  isHighlighted?: boolean;
39
41
  }
40
42
 
43
+ // Notification-style messages (group created/renamed/member changes/etc.)
44
+ // are system banners, not user-authored text — they render centered,
45
+ // full-width, without a bubble, avatar/status chrome, or left/right
46
+ // alignment (see SYSTEM_MESSAGE_TYPES below). Driven by the same lookup
47
+ // messageUtils uses to build their preview/thread text, so adding a new
48
+ // notification content type there is enough to cover both places.
49
+ const GROUP_NOTIFICATION_TYPES = Object.keys(GROUP_NOTIFICATION_TEXT).map(
50
+ Number
51
+ ) as MessageType[];
52
+
41
53
  const messageComponentMap: Partial<
42
54
  Record<MessageType, ComponentType<MessageProps>>
43
55
  > = {
@@ -51,6 +63,9 @@ const messageComponentMap: Partial<
51
63
  [MessageType.MergeMessage]: MergedMessage,
52
64
  [MessageType.RevokeMessage]: RevokedMessage,
53
65
  [MessageType.StickerMessage]: StickerMessage,
66
+ ...Object.fromEntries(
67
+ GROUP_NOTIFICATION_TYPES.map((type) => [type, GroupNotificationMessage])
68
+ ),
54
69
  };
55
70
 
56
71
  const MEDIA_TYPES: number[] = [
@@ -59,6 +74,8 @@ const MEDIA_TYPES: number[] = [
59
74
  MessageType.MergeMessage,
60
75
  ];
61
76
 
77
+ const SYSTEM_MESSAGE_TYPES: number[] = GROUP_NOTIFICATION_TYPES;
78
+
62
79
  export const LegendChatMessage = memo(
63
80
  ({
64
81
  message,
@@ -86,6 +103,20 @@ export const LegendChatMessage = memo(
86
103
  return customNode;
87
104
  }
88
105
 
106
+ if (SYSTEM_MESSAGE_TYPES.includes(message?.contentType)) {
107
+ const SystemMessageComponent =
108
+ messageComponentMap[message.contentType] ?? TextMessage;
109
+ return (
110
+ <KContainer.View center style={styles.systemMessageWrapper}>
111
+ <SystemMessageComponent
112
+ message={message}
113
+ isOutgoing={isOutgoing}
114
+ createdAtTime={createdAtTime}
115
+ />
116
+ </KContainer.View>
117
+ );
118
+ }
119
+
89
120
  const MessageComponent: ComponentType<MessageProps> =
90
121
  messageComponentMap[message?.contentType] ?? TextMessage;
91
122
 
@@ -134,6 +165,10 @@ const styles = StyleSheet.create({
134
165
  content: {
135
166
  position: 'relative',
136
167
  },
168
+ systemMessageWrapper: {
169
+ width: '100%',
170
+ marginVertical: KSpacingValue['0.25rem'],
171
+ },
137
172
  pinBadge: {
138
173
  position: 'absolute',
139
174
  // Capped at the inter-row gap (messageRowLeft/Right's marginBottom) so the
@@ -49,7 +49,6 @@ export const MessagesTab = memo(({ onPressThread }: MessagesTabProps) => {
49
49
  </KLabel.Text>
50
50
  </KContainer.View>
51
51
  );
52
-
53
52
  if (isLoading && data.length === 0) {
54
53
  return LoadingComponent;
55
54
  }