@droppii-org/chat-mobile 0.2.76 → 0.2.78

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 (74) hide show
  1. package/lib/module/components/messages/groupNotificationMessage/index.js +5 -3
  2. package/lib/module/components/messages/groupNotificationMessage/index.js.map +1 -1
  3. package/lib/module/context/ChatDetailContext.js +10 -1
  4. package/lib/module/context/ChatDetailContext.js.map +1 -1
  5. package/lib/module/core/useChatListener.js +8 -5
  6. package/lib/module/core/useChatListener.js.map +1 -1
  7. package/lib/module/hooks/groups/useLeaveGroup.js +93 -0
  8. package/lib/module/hooks/groups/useLeaveGroup.js.map +1 -0
  9. package/lib/module/hooks/message/useSendMessage.js +16 -8
  10. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  11. package/lib/module/hooks/query-keys.js +2 -1
  12. package/lib/module/hooks/query-keys.js.map +1 -1
  13. package/lib/module/hooks/useConversationList.js +6 -9
  14. package/lib/module/hooks/useConversationList.js.map +1 -1
  15. package/lib/module/hooks/useGroupPermission.js +39 -5
  16. package/lib/module/hooks/useGroupPermission.js.map +1 -1
  17. package/lib/module/screens/ChatInfo/ChatInfo.js +15 -5
  18. package/lib/module/screens/ChatInfo/ChatInfo.js.map +1 -1
  19. package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js +2 -1
  20. package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js.map +1 -1
  21. package/lib/module/screens/chat-detail/ChatComposer.js +17 -8
  22. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  23. package/lib/module/screens/chat-detail/ChatDetailHeader.js +4 -8
  24. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  25. package/lib/module/translation/resources/i18n.js +21 -1
  26. package/lib/module/translation/resources/i18n.js.map +1 -1
  27. package/lib/module/types/chat.js.map +1 -1
  28. package/lib/module/utils/legendListMessage.js +5 -1
  29. package/lib/module/utils/legendListMessage.js.map +1 -1
  30. package/lib/module/utils/messageUtils.js +117 -8
  31. package/lib/module/utils/messageUtils.js.map +1 -1
  32. package/lib/module/utils/toastFlashMessage.js +6 -0
  33. package/lib/module/utils/toastFlashMessage.js.map +1 -1
  34. package/lib/typescript/src/components/messages/groupNotificationMessage/index.d.ts.map +1 -1
  35. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
  36. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  37. package/lib/typescript/src/hooks/groups/useLeaveGroup.d.ts +6 -0
  38. package/lib/typescript/src/hooks/groups/useLeaveGroup.d.ts.map +1 -0
  39. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  40. package/lib/typescript/src/hooks/query-keys.d.ts +1 -0
  41. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  42. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  43. package/lib/typescript/src/hooks/useGroupPermission.d.ts +6 -1
  44. package/lib/typescript/src/hooks/useGroupPermission.d.ts.map +1 -1
  45. package/lib/typescript/src/screens/ChatInfo/ChatInfo.d.ts.map +1 -1
  46. package/lib/typescript/src/screens/ChatInfo/ChatInfoDescriptionRow.d.ts +1 -2
  47. package/lib/typescript/src/screens/ChatInfo/ChatInfoDescriptionRow.d.ts.map +1 -1
  48. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  49. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  50. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  51. package/lib/typescript/src/types/chat.d.ts +3 -1
  52. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  53. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
  54. package/lib/typescript/src/utils/messageUtils.d.ts +6 -0
  55. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  56. package/lib/typescript/src/utils/toastFlashMessage.d.ts.map +1 -1
  57. package/package.json +5 -2
  58. package/src/components/messages/groupNotificationMessage/index.tsx +8 -4
  59. package/src/context/ChatDetailContext.tsx +9 -0
  60. package/src/core/useChatListener.ts +15 -4
  61. package/src/hooks/groups/useLeaveGroup.ts +104 -0
  62. package/src/hooks/message/useSendMessage.ts +15 -12
  63. package/src/hooks/query-keys.ts +1 -0
  64. package/src/hooks/useConversationList.ts +8 -17
  65. package/src/hooks/useGroupPermission.ts +46 -5
  66. package/src/screens/ChatInfo/ChatInfo.tsx +26 -8
  67. package/src/screens/ChatInfo/ChatInfoDescriptionRow.tsx +3 -2
  68. package/src/screens/chat-detail/ChatComposer.tsx +21 -25
  69. package/src/screens/chat-detail/ChatDetailHeader.tsx +4 -19
  70. package/src/translation/resources/i18n.ts +29 -1
  71. package/src/types/chat.ts +3 -0
  72. package/src/utils/legendListMessage.ts +5 -1
  73. package/src/utils/messageUtils.ts +127 -7
  74. package/src/utils/toastFlashMessage.ts +8 -2
@@ -0,0 +1,104 @@
1
+ import OpenIMSDK, {
2
+ GroupMemberRole,
3
+ GroupVisibility,
4
+ } from '@droppii/openim-rn-client-sdk';
5
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
6
+ import { conversationQueryKeys, groupQueryKeys } from '../query-keys';
7
+ import { UIUtils } from '../../utils/UIUtils';
8
+ import { trans } from '../../translation';
9
+
10
+ const showOwnerBlockedModal = () => {
11
+ UIUtils.alert.open({
12
+ title: trans('chat:leave_group_blocked_title'),
13
+ message: trans('chat:leave_group_blocked_desc'),
14
+ primaryIndex: 0,
15
+ buttonType: 'horizontal-button',
16
+ buttons: [
17
+ { label: trans('chat:leave_group_blocked_button'), kind: 'primary' },
18
+ ],
19
+ });
20
+ };
21
+
22
+ const confirmLeaveGroup = (visibility?: GroupVisibility) =>
23
+ new Promise<void>((resolve, reject) => {
24
+ console.log('confirmLeaveGroup', visibility);
25
+ UIUtils.alert.open({
26
+ title: trans('chat:leave_group_confirm_title'),
27
+ message: trans(
28
+ visibility === GroupVisibility.Public
29
+ ? 'chat:leave_group_confirm_desc_public'
30
+ : 'chat:leave_group_confirm_desc_private'
31
+ ),
32
+ primaryIndex: 1,
33
+ buttonType: 'horizontal-button',
34
+ buttons: [
35
+ {
36
+ label: trans('chat:leave_group_confirm_secondary'),
37
+ kind: 'light',
38
+ onPress: reject,
39
+ },
40
+ {
41
+ label: trans('chat:leave_group_confirm_primary'),
42
+ kind: 'danger',
43
+ onPress: resolve,
44
+ },
45
+ ],
46
+ });
47
+ });
48
+
49
+ export const useLeaveGroup = () => {
50
+ const queryClient = useQueryClient();
51
+
52
+ const mutation = useMutation({
53
+ mutationKey: groupQueryKeys.leave(),
54
+ mutationFn: (groupID: string) => OpenIMSDK.quitGroup(groupID),
55
+ onSuccess: (_data, groupID) => {
56
+ queryClient.invalidateQueries({
57
+ queryKey: conversationQueryKeys.joinedGroup(groupID),
58
+ });
59
+ },
60
+ onError: () => {
61
+ UIUtils.toast.open({
62
+ title: trans('chat:generic_error'),
63
+ theme: 'error',
64
+ });
65
+ },
66
+ });
67
+
68
+ const leaveGroup = async (groupID: string, visibility?: GroupVisibility) => {
69
+ try {
70
+ // The owner must transfer ownership before leaving — block instead of
71
+ // showing the confirm modal.
72
+ const selfInfo = await OpenIMSDK.getSelfUserInfo(groupID);
73
+ if (selfInfo?.roleLevel === GroupMemberRole.Owner) {
74
+ showOwnerBlockedModal();
75
+ return false;
76
+ }
77
+ try {
78
+ await confirmLeaveGroup(visibility);
79
+ } catch {
80
+ return false;
81
+ }
82
+
83
+ // Revalidate right before submitting — role/membership may have
84
+ // changed while the confirm modal was open.
85
+ const latestSelfInfo = await OpenIMSDK.getSelfUserInfo(groupID);
86
+ if (latestSelfInfo?.roleLevel === GroupMemberRole.Owner) {
87
+ showOwnerBlockedModal();
88
+ return false;
89
+ }
90
+
91
+ await mutation.mutateAsync(groupID);
92
+ return true;
93
+ } catch (error) {
94
+ console.error('leaveGroup failed', error);
95
+ UIUtils.toast.open({
96
+ title: trans('chat:generic_error'),
97
+ theme: 'error',
98
+ });
99
+ return false;
100
+ }
101
+ };
102
+
103
+ return { leaveGroup, isLeavingGroup: mutation.isPending };
104
+ };
@@ -9,7 +9,6 @@ import type {
9
9
  SendMsgParams,
10
10
  } from '@droppii/openim-rn-client-sdk';
11
11
  import {
12
- GroupPermission,
13
12
  MessageStatus,
14
13
  MessageType,
15
14
  SessionType,
@@ -34,8 +33,8 @@ import type { StickerItem } from '../../types/sticker';
34
33
  import { useQueryClient } from '@tanstack/react-query';
35
34
  import { stickerQueryKeys } from '../query-keys';
36
35
  import { getMessagePreview } from '../../utils/messageUtils';
37
- import { trans } from '../../translation';
38
- import { useGuardedAction } from '../useGroupPermission';
36
+ import { useCanSendMessage } from '../useGroupPermission';
37
+ import { UIUtils } from '../../utils/UIUtils';
39
38
 
40
39
  const buildSendParams = (
41
40
  message: MessageItem,
@@ -61,6 +60,8 @@ export const useSendMessage = () => {
61
60
  const updateOneMessage = useMessageStore((state) => state.updateOneMessage);
62
61
  const { quotedMessage, setQuotedMessage } = useChatDetailContext();
63
62
  const queryClient = useQueryClient();
63
+ const { allowed: isSendMessageAllowed, reason: sendDeniedReason } =
64
+ useCanSendMessage();
64
65
 
65
66
  const dispatchSend = useCallback(
66
67
  async (
@@ -104,6 +105,14 @@ export const useSendMessage = () => {
104
105
  customMessage?: CustomMessagePayload;
105
106
  sticker?: StickerItem;
106
107
  }) => {
108
+ if (!isSendMessageAllowed) {
109
+ UIUtils.toast.open({
110
+ title: sendDeniedReason,
111
+ theme: 'info',
112
+ });
113
+ return;
114
+ }
115
+
107
116
  const currentConversation = useConversationStore
108
117
  .getState()
109
118
  .getCurrentConversation();
@@ -188,6 +197,8 @@ export const useSendMessage = () => {
188
197
  quotedMessage,
189
198
  setQuotedMessage,
190
199
  queryClient,
200
+ isSendMessageAllowed,
201
+ sendDeniedReason,
191
202
  ]
192
203
  );
193
204
 
@@ -252,16 +263,8 @@ export const useSendMessage = () => {
252
263
  markMessageRevoked(message.clientMsgID);
253
264
  }, []);
254
265
 
255
- const sendMessageWithPermission = useGuardedAction(
256
- GroupPermission.SendMessage,
257
- sendMessage,
258
- {
259
- deniedMessage: trans('chat:no_send_permission'),
260
- }
261
- );
262
-
263
266
  return {
264
- sendMessage: sendMessageWithPermission,
267
+ sendMessage,
265
268
  retryMessage,
266
269
  revokeMessage,
267
270
  };
@@ -68,4 +68,5 @@ export const groupQueryKeys = {
68
68
  publicSearch: (keyword: string) =>
69
69
  [...groupQueryKeys.public(), keyword] as const,
70
70
  join: () => [...groupQueryKeys.all, 'join'] as const,
71
+ leave: () => [...groupQueryKeys.all, 'leave'] as const,
71
72
  };
@@ -23,26 +23,17 @@ export function useConversationList({
23
23
  const query = useQuery({
24
24
  queryKey: [...conversationQueryKeys.list(applicationType), page],
25
25
  enabled: enabled !== false,
26
+ staleTime: 1000 * 60 * 1, // 5 minutes
26
27
  queryFn: async () => {
27
28
  const offset = (page - 1) * 500;
28
- const [conversations, joinedGroups] = await Promise.all([
29
- OpenIMSDK.getConversationListSplitApp({
30
- offset,
31
- count: 500,
32
- applicationType,
33
- }),
34
- OpenIMSDK.getJoinedGroupList(),
35
- ]);
29
+ const conversations = await OpenIMSDK.getConversationListSplitApp({
30
+ offset,
31
+ count: 500,
32
+ applicationType,
33
+ });
36
34
 
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;
35
+ useConversationStore.getState().updateConversations(conversations);
36
+ return conversations;
46
37
  },
47
38
  });
48
39
 
@@ -1,4 +1,8 @@
1
- import { PeerType, type GroupPermission } from '@droppii/openim-rn-client-sdk';
1
+ import {
2
+ GroupMemberRole,
3
+ GroupPermission,
4
+ GroupStatus,
5
+ } from '@droppii/openim-rn-client-sdk';
2
6
  import { useChatDetailContext } from '../context';
3
7
  import { useCallback, useMemo } from 'react';
4
8
  import { trans } from '../translation';
@@ -8,18 +12,17 @@ export const useHasGroupPermission = (
8
12
  permission: GroupPermission | GroupPermission[],
9
13
  mode: 'every' | 'some' = 'every'
10
14
  ): boolean => {
11
- const { conversation, userInfo } = useChatDetailContext();
12
- const isNotGroup = conversation?.peerType !== PeerType.Group;
15
+ const { isGroupConversation, userInfo } = useChatDetailContext();
13
16
  const required = Array.isArray(permission) ? permission : [permission];
14
17
  const granted = userInfo?.permissions || [];
15
18
 
16
19
  return useMemo(() => {
17
- if (isNotGroup) return true;
20
+ if (!isGroupConversation) return true;
18
21
  return mode === 'every'
19
22
  ? required.every((p) => granted?.includes(p))
20
23
  : required.some((p) => granted?.includes(p));
21
24
  // eslint-disable-next-line react-hooks/exhaustive-deps
22
- }, [granted, isNotGroup, mode]);
25
+ }, [isGroupConversation, mode]);
23
26
  };
24
27
 
25
28
  export const useGuardedAction = <T extends (...args: any[]) => any>(
@@ -42,3 +45,41 @@ export const useGuardedAction = <T extends (...args: any[]) => any>(
42
45
  [allowed, action, options?.deniedMessage]
43
46
  ) as T;
44
47
  };
48
+
49
+ export const useHasRoleLevel = (minRoleLevel: GroupMemberRole): boolean => {
50
+ const { isGroupConversation, userInfo } = useChatDetailContext();
51
+
52
+ return useMemo(() => {
53
+ if (!isGroupConversation) return true;
54
+ return (userInfo?.roleLevel ?? GroupMemberRole.Normal) >= minRoleLevel;
55
+ }, [isGroupConversation, userInfo?.roleLevel, minRoleLevel]);
56
+ };
57
+
58
+ export const useCanSendMessage = (): {
59
+ allowed: boolean;
60
+ reason?: string;
61
+ } => {
62
+ const { userInfo, groupInfo, isGroupConversation } = useChatDetailContext();
63
+
64
+ const hasRolePermission = useMemo(
65
+ () => userInfo?.permissions?.includes(GroupPermission.SendMessage) ?? false,
66
+ [userInfo?.permissions]
67
+ );
68
+ const isOwnerOrAdmin = useHasRoleLevel(GroupMemberRole.Admin);
69
+
70
+ if (!isGroupConversation) return { allowed: true };
71
+
72
+ if (!hasRolePermission)
73
+ return {
74
+ allowed: false,
75
+ reason: trans('group:cannot_send_message'),
76
+ };
77
+
78
+ if (groupInfo?.status === GroupStatus.Muted && !isOwnerOrAdmin) {
79
+ return {
80
+ allowed: false,
81
+ reason: trans('group:messaging_muted_by_owner'),
82
+ };
83
+ }
84
+ return { allowed: true };
85
+ };
@@ -1,8 +1,9 @@
1
- import { memo } from 'react';
1
+ import { memo, useCallback } from 'react';
2
2
  import { KContainer, KNavigation } from '@droppii/libs';
3
3
  import { useNavigation } from '@react-navigation/native';
4
4
  import { useGetConversationTarget } from '../../hooks/conversation/useGetConversationDetail';
5
5
  import { useGetGroupInfo } from '../../hooks/conversation/useGetGroupInfo';
6
+ import { useLeaveGroup } from '../../hooks/groups/useLeaveGroup';
6
7
  import { useConversation } from '../../store';
7
8
  import ChatInfoProfile from './ChatInfoProfile';
8
9
  import ChatInfoMediaRow from './ChatInfoMediaRow';
@@ -10,8 +11,8 @@ import ChatInfoDescriptionRow from './ChatInfoDescriptionRow';
10
11
  import ChatInfoActions from './ChatInfoActions';
11
12
  import type { ChatInfoProps } from './types';
12
13
  import { trans } from '../../translation';
13
- import { isGroupConversation } from '../../utils/conversation';
14
14
  import { withChatDetailProvider } from '../../context';
15
+ import { useChatDetailContext } from '../../context';
15
16
 
16
17
  const ChatInfo = memo(
17
18
  ({
@@ -30,10 +31,30 @@ const ChatInfo = memo(
30
31
  const conversation = useConversation(conversationId);
31
32
  const { data: target } = useGetConversationTarget(conversationId);
32
33
  const isTargetActive = target ? target.isActive : true;
33
- const isGroup = isGroupConversation(conversation);
34
+ const isGroup = useChatDetailContext().isGroupConversation;
35
+
34
36
  const { data: groupInfo } = useGetGroupInfo(
35
37
  isGroup ? conversation?.groupID : undefined
36
38
  );
39
+ const { leaveGroup } = useLeaveGroup();
40
+
41
+ const handleLeaveGroup = useCallback(async () => {
42
+ if (!conversation?.groupID) return;
43
+
44
+ const didLeave = await leaveGroup(
45
+ conversation.groupID,
46
+ groupInfo?.visibility
47
+ );
48
+ if (didLeave) {
49
+ (onPressLeaveGroup ?? navigation.goBack)();
50
+ }
51
+ }, [
52
+ conversation?.groupID,
53
+ groupInfo?.visibility,
54
+ leaveGroup,
55
+ navigation.goBack,
56
+ onPressLeaveGroup,
57
+ ]);
37
58
 
38
59
  return (
39
60
  <KContainer.Page flex edges={['bottom']} background="#EBEDF2">
@@ -56,10 +77,7 @@ const ChatInfo = memo(
56
77
  />
57
78
  <ChatInfoMediaRow onPressMedia={onPressMedia} />
58
79
  <KContainer.VisibleView visible={isGroup}>
59
- <ChatInfoDescriptionRow
60
- introduction={groupInfo?.introduction}
61
- groupName={conversation?.showName}
62
- />
80
+ <ChatInfoDescriptionRow groupName={conversation?.showName} />
63
81
  </KContainer.VisibleView>
64
82
  <ChatInfoActions
65
83
  conversation={conversation}
@@ -68,7 +86,7 @@ const ChatInfo = memo(
68
86
  onPressOrders={onPressOrders}
69
87
  onPressCreateGroup={onPressCreateGroup}
70
88
  onPressAddToGroup={onPressAddToGroup}
71
- onPressLeaveGroup={onPressLeaveGroup}
89
+ onPressLeaveGroup={handleLeaveGroup}
72
90
  onDismissGroup={onDismissGroup}
73
91
  />
74
92
  </KContainer.ScrollView>
@@ -3,9 +3,9 @@ import { StyleSheet, type TextLayoutEvent } from 'react-native';
3
3
  import { KContainer, KLabel, KColors } from '@droppii/libs';
4
4
  import { trans } from '../../translation';
5
5
  import GroupDescriptionModal from './GroupDescriptionModal';
6
+ import { useChatDetailContext } from '../../context';
6
7
 
7
8
  interface ChatInfoDescriptionRowProps {
8
- introduction?: string;
9
9
  groupName?: string;
10
10
  }
11
11
 
@@ -13,7 +13,8 @@ const EXTRA_TEXT_LENGTH = 14; // Length of "...View More" text
13
13
  const MAX_LINES = 2;
14
14
 
15
15
  const ChatInfoDescriptionRow = memo(
16
- ({ introduction, groupName }: ChatInfoDescriptionRowProps) => {
16
+ ({ groupName }: ChatInfoDescriptionRowProps) => {
17
+ const introduction = useChatDetailContext().groupInfo?.introduction;
17
18
  const [modalVisible, setModalVisible] = useState(false);
18
19
  const handlePressDescription = useCallback(() => setModalVisible(true), []);
19
20
  const handleCloseDescription = useCallback(
@@ -63,11 +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, PeerType } from '@droppii/openim-rn-client-sdk';
67
- import {
68
- useGuardedAction,
69
- useHasGroupPermission,
70
- } from '../../hooks/useGroupPermission';
66
+ import { PeerType } from '@droppii/openim-rn-client-sdk';
67
+ import { useCanSendMessage } from '../../hooks/useGroupPermission';
71
68
  import { useIsJoinedGroup } from '../../hooks/useIsJoinedGroup';
72
69
 
73
70
  interface ChatComposerInternalProps {
@@ -107,9 +104,8 @@ export const ChatComposer = memo(
107
104
  const { mutateAsync: closeSupportSession } = useCloseSupportSession();
108
105
  const { sendMessage } = useSendMessage();
109
106
  const compose = useChatCompose();
110
- const isSendMessageAllowed = useHasGroupPermission(
111
- GroupPermission.SendMessage
112
- );
107
+ const { allowed: isSendMessageAllowed, reason: sendDeniedReason } =
108
+ useCanSendMessage();
113
109
 
114
110
  const { data: isJoinedGroup } = useIsJoinedGroup(conversation?.groupID);
115
111
 
@@ -149,20 +145,22 @@ export const ChatComposer = memo(
149
145
  onAttachmentOpenChange: setIsAttachmentOpen,
150
146
  });
151
147
 
152
- const handleSend = useGuardedAction(
153
- GroupPermission.SendMessage,
154
- useCallback(async () => {
155
- if (compose.isUploading || !compose.canSend) return;
156
- try {
157
- await compose.send();
158
- } catch (error) {
159
- if (__DEV__) console.error('[ChatComposer] Failed to send:', error);
160
- }
161
- }, [compose]),
162
- {
163
- deniedMessage: trans('chat:no_send_permission'),
148
+ const handleSend = useCallback(async () => {
149
+ if (!isSendMessageAllowed) {
150
+ UIUtils.toast.open({
151
+ title: sendDeniedReason,
152
+ theme: 'info',
153
+ });
154
+ return;
164
155
  }
165
- );
156
+
157
+ if (compose.isUploading || !compose.canSend) return;
158
+ try {
159
+ await compose.send();
160
+ } catch (error) {
161
+ if (__DEV__) console.error('[ChatComposer] Failed to send:', error);
162
+ }
163
+ }, [compose, isSendMessageAllowed, sendDeniedReason]);
166
164
 
167
165
  const handleInputFocus = useCallback(() => {
168
166
  setIsInputFocused(true);
@@ -474,7 +472,7 @@ export const ChatComposer = memo(
474
472
  );
475
473
  } else if (!isSendMessageAllowed) {
476
474
  content = (
477
- <KContainer.View row alignItems justifyContent>
475
+ <KContainer.View row alignItems justifyContent paddingT={'1rem'}>
478
476
  <KContainer.View marginR="0.25rem">
479
477
  <KImage.VectorIcons
480
478
  name="info-circle-o"
@@ -482,9 +480,7 @@ export const ChatComposer = memo(
482
480
  color={KColors.palette.gray.w600}
483
481
  />
484
482
  </KContainer.View>
485
- <KLabel.Text typo="TextXsNormal">
486
- {trans('group:cannot_send_message')}
487
- </KLabel.Text>
483
+ <KLabel.Text typo="TextXsNormal">{sendDeniedReason}</KLabel.Text>
488
484
  </KContainer.View>
489
485
  );
490
486
  } else {
@@ -11,9 +11,8 @@ import { NamePrefixIcon } from '../../components/ThreadCard/NamePrefixIcon';
11
11
  import type { ChatDetailHeaderProps } from './types';
12
12
  import { getConversationSubtitle } from './conversationHeader.utils';
13
13
  import { useGetUsersInfo } from '../../hooks/users/useGetUsersInfo';
14
- import { useGetGroupInfo } from '../../hooks/conversation/useGetGroupInfo';
15
- import { isGroupConversation } from '../../utils/conversation';
16
14
  import { useGetConversationDetail } from '../../hooks/conversation/useGetConversationDetail';
15
+ import { useChatDetailContext } from '../../context';
17
16
 
18
17
  const ChatDetailHeader = memo(
19
18
  ({
@@ -34,11 +33,7 @@ const ChatDetailHeader = memo(
34
33
  const { data: usersInfo } = useGetUsersInfo(
35
34
  targetUserId ? [targetUserId] : []
36
35
  );
37
-
38
- const isGroup = isGroupConversation(conversation);
39
- const { data: groupInfo } = useGetGroupInfo(
40
- isGroup ? conversation?.groupID : undefined
41
- );
36
+ const memberCount = useChatDetailContext().groupInfo?.memberCount;
42
37
 
43
38
  const { faceURL, showName } = useMemo(() => {
44
39
  const userInfo = usersInfo?.[0];
@@ -135,24 +130,14 @@ const ChatDetailHeader = memo(
135
130
  color={KColors.palette.gray.w500}
136
131
  numberOfLines={1}
137
132
  >
138
- {getConversationSubtitle(
139
- conversation,
140
- groupInfo?.memberCount
141
- )}
133
+ {getConversationSubtitle(conversation, memberCount)}
142
134
  </KLabel.Text>
143
135
  )}
144
136
  </KContainer.View>
145
137
  </KContainer.Touchable>
146
138
  </KContainer.View>
147
139
  ),
148
- [
149
- onBack,
150
- onPressAvatar,
151
- conversation,
152
- faceURL,
153
- showName,
154
- groupInfo?.memberCount,
155
- ]
140
+ [onBack, onPressAvatar, conversation, faceURL, showName, memberCount]
156
141
  );
157
142
 
158
143
  return (
@@ -75,16 +75,31 @@ const i18nKey: Record<string, string> = {
75
75
  'chat:group_created_by': '{{sender}} đã tạo nhóm',
76
76
  'msg_type_group_name_updated': 'Đã đổi tên nhóm',
77
77
  'chat:group_name_updated_by': '{{sender}} đã đổi tên nhóm',
78
+ 'chat:group_name_updated_by_named':
79
+ '{{sender}} đã đổi tên nhóm thành {{newGroupName}}',
78
80
  'msg_type_group_info_updated': 'Đã cập nhật thông tin nhóm',
79
81
  'chat:group_info_updated_by': '{{sender}} đã cập nhật thông tin nhóm',
80
82
  'msg_type_member_quit': 'Đã rời khỏi nhóm',
81
- 'chat:member_quit_by': '{{sender}} đã rời khỏi nhóm',
83
+ 'chat:member_quit_by': '{{sender}} đã rời nhóm',
82
84
  'msg_type_group_owner_transferred': 'Đã chuyển quyền trưởng nhóm',
83
85
  'chat:group_owner_transferred_by': '{{sender}} đã chuyển quyền trưởng nhóm',
86
+ 'chat:group_owner_transferred_by_named':
87
+ '{{sender}} đã chuyển quyền trưởng nhóm cho {{memberName}}',
84
88
  'msg_type_member_kicked': 'Đã xoá thành viên khỏi nhóm',
85
89
  'chat:member_kicked_by': '{{sender}} đã xoá thành viên khỏi nhóm',
90
+ 'chat:member_kicked_by_named': '{{sender}} đã xoá {{names}} khỏi nhóm',
86
91
  'msg_type_member_invited': 'Đã thêm thành viên vào nhóm',
87
92
  'chat:member_invited_by': '{{sender}} đã thêm thành viên vào nhóm',
93
+ 'chat:member_invited_by_named': '{{sender}} đã thêm {{names}} vào nhóm',
94
+ 'msg_type_group_member_set_admin': 'Đã chỉ định quản trị viên mới',
95
+ 'chat:group_member_set_admin_by':
96
+ '{{sender}} đã đặt {{memberName}} làm quản trị viên',
97
+ 'chat:group_member_set_admin_by_generic':
98
+ '{{sender}} đã đặt một thành viên làm quản trị viên',
99
+ 'msg_type_message_pinned': 'Đã ghim một tin nhắn',
100
+ 'chat:group_message_pinned_by': '{{sender}} đã ghim một tin nhắn',
101
+ 'msg_type_message_unpinned': 'Đã bỏ ghim một tin nhắn',
102
+ 'chat:group_message_unpinned_by': '{{sender}} đã bỏ ghim một tin nhắn',
88
103
  'msg_type_member_enter': 'Đã tham gia nhóm',
89
104
  'chat:member_enter_by': '{{sender}} đã tham gia nhóm',
90
105
  'msg_type_group_dismissed': 'Nhóm đã bị giải tán',
@@ -124,6 +139,17 @@ const i18nKey: Record<string, string> = {
124
139
  'chat:info_unpin_message': 'Bỏ ghim trò chuyện',
125
140
  'chat:info_delete_history': 'Xoá lịch sử trò chuyện',
126
141
  'chat:info_leave_group': 'Rời nhóm',
142
+ 'chat:leave_group_confirm_title': 'Rời nhóm?',
143
+ 'chat:leave_group_confirm_desc_private':
144
+ 'Sau khi rời nhóm, bạn sẽ không thể xem tin nhắn từ cuộc trò chuyện này nữa. Bạn có chắc chắn muốn rời khỏi nhóm?',
145
+ 'chat:leave_group_confirm_desc_public':
146
+ 'Sau khi rời nhóm, bạn sẽ không thể nhắn tin hay nhận thông báo từ cuộc trò chuyện này nữa. Bạn có chắc chắn muốn rời khỏi nhóm?',
147
+ 'chat:leave_group_confirm_primary': 'Rời nhóm',
148
+ 'chat:leave_group_confirm_secondary': 'Ở lại nhóm',
149
+ 'chat:leave_group_blocked_title': 'Không thể rời nhóm',
150
+ 'chat:leave_group_blocked_desc':
151
+ 'Bạn không thể rời nhóm khi đang là chủ nhóm, vui lòng chuyển quyền chủ nhóm cho thành viên khác và thử lại!',
152
+ 'chat:leave_group_blocked_button': 'Đã hiểu',
127
153
  'chat:dismiss_group': 'Giải tán nhóm',
128
154
  'chat:info_view_more': 'Xem thêm',
129
155
  'chat:group_description_title': 'Mô tả nhóm',
@@ -172,6 +198,8 @@ const i18nKey: Record<string, string> = {
172
198
  'group:join': 'Tham gia',
173
199
  'group:not_in_group': 'Bạn cần tham gia nhóm để gửi tin nhắn',
174
200
  'group:cannot_send_message': 'Bạn không thể gửi tin nhắn vào nhóm này',
201
+ 'group:messaging_muted_by_owner':
202
+ 'Chỉ chủ nhóm và quản trị viên được phép gửi tin nhắn',
175
203
  'group:joined_group': 'Đã tham gia nhóm {{groupName}}',
176
204
  'group:public_group': 'Nhóm công khai',
177
205
  'group:private_group': 'Nhóm riêng tư',
package/src/types/chat.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  MessageType,
3
3
  type ConversationItem,
4
+ type GroupItem,
4
5
  type MessageItem,
5
6
  type SelfUserInfo,
6
7
  } from '@droppii/openim-rn-client-sdk';
@@ -206,6 +207,7 @@ export interface MessageStore {
206
207
  export interface ChatDetailContextType {
207
208
  conversationFlow?: IConversationFlow;
208
209
  isBotConversation: boolean;
210
+ isGroupConversation: boolean;
209
211
  setConverationFlowStep: (
210
212
  step?: IConversationFlowStep,
211
213
  isEnd?: boolean,
@@ -218,6 +220,7 @@ export interface ChatDetailContextType {
218
220
  setQuotedMessage: (item?: MessageItem) => void;
219
221
  conversationFlowMainMenu?: InputButtonItem[];
220
222
  userInfo?: SelfUserInfo;
223
+ groupInfo?: GroupItem | null;
221
224
  }
222
225
 
223
226
  export interface ChatDetailProviderProps extends PropsWithChildren {}
@@ -28,8 +28,12 @@ export const getMessageText = (message: MessageItem): string => {
28
28
  case MessageType.FaceMessage:
29
29
  return '[Sticker]';
30
30
  default:
31
+ // Never surface the raw notification payload as message text — it
32
+ // contains other users' userID/nickname/faceURL/permissions and would
33
+ // render verbatim as a normal chat bubble for any content type that
34
+ // isn't (yet) mapped in GROUP_NOTIFICATION_TEXT.
31
35
  if (message.notificationElem?.detail) {
32
- return message.notificationElem.detail;
36
+ return '';
33
37
  }
34
38
  return message.content || '';
35
39
  }