@droppii-org/chat-mobile 0.2.66 → 0.2.68

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 (133) 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 +121 -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/usePinMessage.js +4 -2
  28. package/lib/module/hooks/message/usePinMessage.js.map +1 -1
  29. package/lib/module/hooks/message/useSendMessage.js +13 -15
  30. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  31. package/lib/module/hooks/useConversationList.js +9 -4
  32. package/lib/module/hooks/useConversationList.js.map +1 -1
  33. package/lib/module/hooks/useGroupPermission.js +34 -0
  34. package/lib/module/hooks/useGroupPermission.js.map +1 -0
  35. package/lib/module/screens/ChatInfo/ChatInfoProfile.js +4 -2
  36. package/lib/module/screens/ChatInfo/ChatInfoProfile.js.map +1 -1
  37. package/lib/module/screens/chat-detail/ChatComposer.js +16 -18
  38. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  39. package/lib/module/screens/chat-detail/ChatDetail.js +7 -0
  40. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  41. package/lib/module/screens/chat-detail/ChatDetailHeader.js +10 -3
  42. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  43. package/lib/module/screens/chat-detail/conversationHeader.utils.js +13 -3
  44. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -1
  45. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +29 -2
  46. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  47. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +5 -0
  48. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
  49. package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
  50. package/lib/module/store/conversation.js +56 -12
  51. package/lib/module/store/conversation.js.map +1 -1
  52. package/lib/module/translation/resources/i18n.js +32 -0
  53. package/lib/module/translation/resources/i18n.js.map +1 -1
  54. package/lib/module/types/events.js +1 -0
  55. package/lib/module/types/events.js.map +1 -1
  56. package/lib/module/utils/messageUtils.js +95 -2
  57. package/lib/module/utils/messageUtils.js.map +1 -1
  58. package/lib/typescript/src/components/Avatar/Avatar.d.ts +1 -1
  59. package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -1
  60. package/lib/typescript/src/components/Avatar/Avatar.types.d.ts +5 -1
  61. package/lib/typescript/src/components/Avatar/Avatar.types.d.ts.map +1 -1
  62. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts.map +1 -1
  63. package/lib/typescript/src/components/Avatar/QuadAvatar.d.ts +16 -0
  64. package/lib/typescript/src/components/Avatar/QuadAvatar.d.ts.map +1 -0
  65. package/lib/typescript/src/components/Avatar/TripleAvatar.d.ts +15 -0
  66. package/lib/typescript/src/components/Avatar/TripleAvatar.d.ts.map +1 -0
  67. package/lib/typescript/src/components/Avatar/index.d.ts +2 -0
  68. package/lib/typescript/src/components/Avatar/index.d.ts.map +1 -1
  69. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +2 -2
  70. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  71. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +3 -1
  72. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
  73. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  74. package/lib/typescript/src/components/messages/groupNotificationMessage/index.d.ts +3 -0
  75. package/lib/typescript/src/components/messages/groupNotificationMessage/index.d.ts.map +1 -0
  76. package/lib/typescript/src/context/ChatDetailContext.d.ts +2 -0
  77. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
  78. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  79. package/lib/typescript/src/hooks/conversation/useGetGroupInfo.d.ts.map +1 -1
  80. package/lib/typescript/src/hooks/message/usePinMessage.d.ts.map +1 -1
  81. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  82. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  83. package/lib/typescript/src/hooks/useGroupPermission.d.ts +6 -0
  84. package/lib/typescript/src/hooks/useGroupPermission.d.ts.map +1 -0
  85. package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts.map +1 -1
  86. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  87. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  88. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  89. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +1 -1
  90. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -1
  91. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  92. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +1 -1
  93. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
  94. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  95. package/lib/typescript/src/store/conversation.d.ts +11 -0
  96. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  97. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  98. package/lib/typescript/src/types/events.d.ts +6 -1
  99. package/lib/typescript/src/types/events.d.ts.map +1 -1
  100. package/lib/typescript/src/utils/messageUtils.d.ts +6 -1
  101. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  102. package/package.json +2 -2
  103. package/src/build-ignore.d.ts +1 -0
  104. package/src/components/Avatar/Avatar.tsx +47 -0
  105. package/src/components/Avatar/Avatar.types.ts +5 -1
  106. package/src/components/Avatar/DoubleAvatar.tsx +16 -13
  107. package/src/components/Avatar/QuadAvatar.tsx +161 -0
  108. package/src/components/Avatar/TripleAvatar.tsx +119 -0
  109. package/src/components/Avatar/index.ts +2 -0
  110. package/src/components/ThreadCard/AvatarSection.tsx +58 -2
  111. package/src/components/ThreadCard/NamePrefixIcon.tsx +32 -25
  112. package/src/components/ThreadCard/ThreadCard.tsx +2 -0
  113. package/src/components/messages/groupNotificationMessage/index.tsx +28 -0
  114. package/src/context/ChatContext.tsx +1 -1
  115. package/src/context/ChatDetailContext.tsx +15 -0
  116. package/src/core/useChatListener.ts +176 -23
  117. package/src/hooks/conversation/useGetGroupInfo.ts +2 -0
  118. package/src/hooks/message/usePinMessage.ts +8 -2
  119. package/src/hooks/message/useSendMessage.ts +22 -18
  120. package/src/hooks/useConversationList.ts +17 -7
  121. package/src/hooks/useGroupPermission.ts +43 -0
  122. package/src/screens/ChatInfo/ChatInfoProfile.tsx +6 -3
  123. package/src/screens/chat-detail/ChatComposer.tsx +29 -32
  124. package/src/screens/chat-detail/ChatDetail.tsx +12 -1
  125. package/src/screens/chat-detail/ChatDetailHeader.tsx +19 -3
  126. package/src/screens/chat-detail/conversationHeader.utils.ts +14 -6
  127. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +36 -1
  128. package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +8 -1
  129. package/src/screens/inbox/MessagesTab.tsx +0 -1
  130. package/src/store/conversation.ts +62 -17
  131. package/src/translation/resources/i18n.ts +32 -0
  132. package/src/types/events.ts +6 -0
  133. package/src/utils/messageUtils.ts +103 -1
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect } from 'react';
2
- import OpenIMSDK from '@droppii/openim-rn-client-sdk';
2
+ import OpenIMSDK, { GroupPermission } from '@droppii/openim-rn-client-sdk';
3
3
  import type { MessageItem } from '@droppii/openim-rn-client-sdk';
4
4
  import { useConversationStore } from '../../store/conversation';
5
5
  import { useMessageStore } from '../../store/message';
@@ -7,6 +7,7 @@ import { usePinnedMessageStore } from '../../store/pinnedMessage';
7
7
  import { useUserStore } from '../../store/user';
8
8
  import { UIUtils } from '../../utils/UIUtils';
9
9
  import { trans } from '../../translation';
10
+ import { useGuardedAction } from '../useGroupPermission';
10
11
 
11
12
  const PINNED_MESSAGES_PAGE_SIZE = 5;
12
13
 
@@ -147,9 +148,14 @@ export const usePinMessage = (conversationID?: string) => {
147
148
  [conversationID, removeMessage]
148
149
  );
149
150
 
151
+ const pinMessageWithPermission = useGuardedAction(
152
+ GroupPermission.PinMessage,
153
+ pinMessage
154
+ );
155
+
150
156
  return {
151
157
  pinnedMessages,
152
- pinMessage,
158
+ pinMessage: pinMessageWithPermission,
153
159
  unpinMessage,
154
160
  };
155
161
  };
@@ -33,8 +33,8 @@ import type { StickerItem } from '../../types/sticker';
33
33
  import { useQueryClient } from '@tanstack/react-query';
34
34
  import { stickerQueryKeys } from '../query-keys';
35
35
  import { getMessagePreview } from '../../utils/messageUtils';
36
- import { UIUtils } from '../../utils/UIUtils';
37
36
  import { trans } from '../../translation';
37
+ import { useGuardedAction } from '../useGroupPermission';
38
38
 
39
39
  const buildSendParams = (
40
40
  message: MessageItem,
@@ -52,8 +52,7 @@ export const useSendMessage = () => {
52
52
  const applicationType = useChatContext().applicationType;
53
53
  const pushNewMessage = useMessageStore((state) => state.pushNewMessage);
54
54
  const updateOneMessage = useMessageStore((state) => state.updateOneMessage);
55
- const { quotedMessage, setQuotedMessage, groupPermissions } =
56
- useChatDetailContext();
55
+ const { quotedMessage, setQuotedMessage } = useChatDetailContext();
57
56
  const queryClient = useQueryClient();
58
57
 
59
58
  const dispatchSend = useCallback(
@@ -107,16 +106,6 @@ export const useSendMessage = () => {
107
106
  const recvID = currentConversation?.userID || targetUserId;
108
107
  const groupID = currentConversation?.groupID || targetGroupId;
109
108
  if (!recvID && !groupID) return;
110
- if (
111
- currentConversation?.conversationType === SessionType.Group &&
112
- !groupPermissions?.includes(GroupPermission.SendMessage)
113
- ) {
114
- UIUtils.toast.open({
115
- title: trans('chat:no_send_permission'),
116
- theme: 'info',
117
- });
118
- return;
119
- }
120
109
 
121
110
  const messageList: MessageItem[] = [];
122
111
 
@@ -192,7 +181,6 @@ export const useSendMessage = () => {
192
181
  quotedMessage,
193
182
  setQuotedMessage,
194
183
  queryClient,
195
- groupPermissions,
196
184
  ]
197
185
  );
198
186
 
@@ -257,8 +245,16 @@ export const useSendMessage = () => {
257
245
  markMessageRevoked(message.clientMsgID);
258
246
  }, []);
259
247
 
260
- return {
248
+ const sendMessageWithPermission = useGuardedAction(
249
+ GroupPermission.SendMessage,
261
250
  sendMessage,
251
+ {
252
+ deniedMessage: trans('chat:no_send_permission'),
253
+ }
254
+ );
255
+
256
+ return {
257
+ sendMessage: sendMessageWithPermission,
262
258
  retryMessage,
263
259
  revokeMessage,
264
260
  };
@@ -268,14 +264,22 @@ export const generateOfflinePushInfo = (message: MessageItem) => {
268
264
  const conversationData = useConversationStore
269
265
  ?.getState?.()
270
266
  ?.getCurrentConversation();
271
- const operatorUserID = useUserStore.getState().user?.userID || '';
272
- const title = conversationData?.showName || 'Droppii';
267
+ const operator = useUserStore.getState().user;
268
+ const operatorUserID = operator?.userID || '';
269
+ const isSingleChat =
270
+ conversationData?.conversationType === SessionType.Single;
271
+ const title = isSingleChat
272
+ ? operator?.nickname || 'Droppii'
273
+ : conversationData?.showName || 'Droppii';
274
+ const icon = isSingleChat
275
+ ? operator?.faceURL || ''
276
+ : conversationData?.faceURL || '';
273
277
  const desc = getMessagePreview(message);
274
278
  return {
275
279
  title,
276
280
  desc,
277
281
  ex: JSON.stringify({
278
- icon: conversationData?.faceURL || '',
282
+ icon,
279
283
  conversationId: conversationData?.conversationID || '',
280
284
  userId: operatorUserID,
281
285
  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
 
@@ -0,0 +1,43 @@
1
+ import { PeerType, type GroupPermission } from '@droppii/openim-rn-client-sdk';
2
+ import { useChatDetailContext } from '../context';
3
+ import { useCallback, useMemo } from 'react';
4
+ import { trans } from '../translation';
5
+ import { UIUtils } from '../utils/UIUtils';
6
+
7
+ export const useHasGroupPermission = (
8
+ permission: GroupPermission | GroupPermission[],
9
+ mode: 'every' | 'some' = 'every'
10
+ ): boolean => {
11
+ const { conversation, groupPermissions } = useChatDetailContext();
12
+ const isNotGroup = conversation?.peerType !== PeerType.Group;
13
+ const required = Array.isArray(permission) ? permission : [permission];
14
+
15
+ return useMemo(() => {
16
+ if (isNotGroup) return true;
17
+ return mode === 'every'
18
+ ? required.every((p) => groupPermissions?.includes(p))
19
+ : required.some((p) => groupPermissions?.includes(p));
20
+ // eslint-disable-next-line react-hooks/exhaustive-deps
21
+ }, [groupPermissions, isNotGroup, mode]);
22
+ };
23
+
24
+ export const useGuardedAction = <T extends (...args: any[]) => any>(
25
+ permission: GroupPermission,
26
+ action: T,
27
+ options?: { deniedMessage?: string }
28
+ ): T => {
29
+ const allowed = useHasGroupPermission(permission);
30
+ return useCallback(
31
+ (...args: Parameters<T>) => {
32
+ if (!allowed) {
33
+ UIUtils.toast.open({
34
+ title: options?.deniedMessage ?? trans('chat:no_permission'),
35
+ theme: 'info',
36
+ });
37
+ return;
38
+ }
39
+ return action(...args);
40
+ },
41
+ [allowed, action, options?.deniedMessage]
42
+ ) as T;
43
+ };
@@ -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,8 +63,8 @@ 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';
67
- import { useConversationStore } from '../../store';
66
+ import { GroupPermission, PeerType } from '@droppii/openim-rn-client-sdk';
67
+ import { useGuardedAction } from '../../hooks/useGroupPermission';
68
68
 
69
69
  interface ChatComposerInternalProps {
70
70
  isEmptyMessage?: boolean;
@@ -88,16 +88,17 @@ export const ChatComposer = memo(
88
88
 
89
89
  const {
90
90
  isBotConversation,
91
+ conversation,
91
92
  conversationFlow,
92
93
  conversationFlowMainMenu,
93
94
  flowState,
94
95
  } = useChatDetailContext();
96
+ const isGroupConversation = conversation?.peerType === PeerType.Group;
95
97
  const { mutate: runFlow, isPending } = useRunFlow();
96
98
  const { mutate: resetFlow } = useResetFlow();
97
99
  const { mutateAsync: closeSupportSession } = useCloseSupportSession();
98
100
  const { sendMessage } = useSendMessage();
99
101
  const compose = useChatCompose();
100
- const groupPermissions = useChatDetailContext().groupPermissions;
101
102
 
102
103
  const {
103
104
  activePanelType,
@@ -131,27 +132,20 @@ export const ChatComposer = memo(
131
132
  onAttachmentOpenChange: setIsAttachmentOpen,
132
133
  });
133
134
 
134
- const handleSend = useCallback(async () => {
135
- const currentConversation = useConversationStore
136
- .getState()
137
- .getCurrentConversation();
138
- if (
139
- currentConversation?.conversationType === SessionType.Group &&
140
- !groupPermissions?.includes(GroupPermission.SendMessage)
141
- ) {
142
- UIUtils.toast.open({
143
- title: trans('chat:no_send_permission'),
144
- theme: 'info',
145
- });
146
- return;
147
- }
148
- if (compose.isUploading || !compose.canSend) return;
149
- try {
150
- await compose.send();
151
- } catch (error) {
152
- if (__DEV__) console.error('[ChatComposer] Failed to send:', error);
135
+ const handleSend = useGuardedAction(
136
+ GroupPermission.SendMessage,
137
+ useCallback(async () => {
138
+ if (compose.isUploading || !compose.canSend) return;
139
+ try {
140
+ await compose.send();
141
+ } catch (error) {
142
+ if (__DEV__) console.error('[ChatComposer] Failed to send:', error);
143
+ }
144
+ }, [compose]),
145
+ {
146
+ deniedMessage: trans('chat:no_send_permission'),
153
147
  }
154
- }, [compose, groupPermissions]);
148
+ );
155
149
 
156
150
  const handleInputFocus = useCallback(() => {
157
151
  setIsInputFocused(true);
@@ -409,7 +403,7 @@ export const ChatComposer = memo(
409
403
  conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
410
404
  isEmptyMessage;
411
405
 
412
- if (isDisableCompose) {
406
+ if (isDisableCompose && !isGroupConversation) {
413
407
  content = (
414
408
  <KContainer.View row alignItems justifyContent>
415
409
  <KContainer.View marginR="0.25rem">
@@ -441,14 +435,17 @@ export const ChatComposer = memo(
441
435
  content = (
442
436
  <KContainer.View background={'#F9F9FA'}>
443
437
  <ChatCurrentQuotedMessage />
444
- <ChatQuickActions
445
- visible
446
- actions={DEFAULT_QUICK_ACTIONS}
447
- onActionPress={(action) =>
448
- console.log('[ChatComposer] quick action:', action.id)
449
- }
450
- />
451
-
438
+ <KContainer.VisibleView
439
+ visible={conversation?.peerType === PeerType.Bot}
440
+ >
441
+ <ChatQuickActions
442
+ visible
443
+ actions={DEFAULT_QUICK_ACTIONS}
444
+ onActionPress={(action) =>
445
+ console.log('[ChatComposer] quick action:', action.id)
446
+ }
447
+ />
448
+ </KContainer.VisibleView>
452
449
  {compose.showLinkPreview && (
453
450
  <ChatLinkPreview
454
451
  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
@@ -20,7 +20,11 @@ import { MessageToolboxWrapper } from './MessageToolboxWrapper';
20
20
  import type { ReactNode } from 'react';
21
21
  import { useSendMessage } from '../../../hooks/message/useSendMessage';
22
22
  import { useChatDetailContext } from '../../../context';
23
- import type { MessageItem } from '@droppii/openim-rn-client-sdk';
23
+ import {
24
+ GroupPermission,
25
+ type MessageItem,
26
+ } from '@droppii/openim-rn-client-sdk';
27
+ import { useHasGroupPermission } from '../../../hooks/useGroupPermission';
24
28
 
25
29
  interface MessageToolboxOverlayProps {
26
30
  currentUserId?: string;
@@ -51,6 +55,7 @@ export const MessageToolboxOverlay = forwardRef<
51
55
  [message, currentUserId]
52
56
  );
53
57
  const { retryMessage, revokeMessage } = useSendMessage();
58
+ const isPinAllowed = useHasGroupPermission(GroupPermission.PinMessage);
54
59
 
55
60
  const hide = useCallback(() => {
56
61
  setVisible(false);
@@ -173,6 +178,8 @@ export const MessageToolboxOverlay = forwardRef<
173
178
  toolbox={
174
179
  <MessageToolboxMenu
175
180
  {...toolboxItems}
181
+ showPinMessage={toolboxItems.showPinMessage && isPinAllowed}
182
+ showUnpinMessage={toolboxItems.showUnpinMessage && isPinAllowed}
176
183
  onCopyMessage={handleCopyMessage}
177
184
  onCopyMessageLink={handleCopyMessageLink}
178
185
  onPinMessage={handlePinMessage}
@@ -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
  }