@droppii-org/chat-mobile 0.2.65 → 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 (153) 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 +21 -0
  26. package/lib/module/hooks/conversation/useGetGroupInfo.js.map +1 -0
  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/query-keys.js +2 -1
  30. package/lib/module/hooks/query-keys.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/screens/ChatInfo/ChatInfo.js +18 -2
  34. package/lib/module/screens/ChatInfo/ChatInfo.js.map +1 -1
  35. package/lib/module/screens/ChatInfo/ChatInfoActions.js +83 -55
  36. package/lib/module/screens/ChatInfo/ChatInfoActions.js.map +1 -1
  37. package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js +77 -0
  38. package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js.map +1 -0
  39. package/lib/module/screens/ChatInfo/ChatInfoProfile.js +4 -2
  40. package/lib/module/screens/ChatInfo/ChatInfoProfile.js.map +1 -1
  41. package/lib/module/screens/ChatInfo/GroupDescriptionModal.js +58 -0
  42. package/lib/module/screens/ChatInfo/GroupDescriptionModal.js.map +1 -0
  43. package/lib/module/screens/chat-detail/ChatComposer.js +11 -6
  44. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  45. package/lib/module/screens/chat-detail/ChatDetail.js +7 -0
  46. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  47. package/lib/module/screens/chat-detail/ChatDetailHeader.js +10 -3
  48. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  49. package/lib/module/screens/chat-detail/conversationHeader.utils.js +13 -3
  50. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -1
  51. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +29 -2
  52. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  53. package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
  54. package/lib/module/store/conversation.js +56 -12
  55. package/lib/module/store/conversation.js.map +1 -1
  56. package/lib/module/translation/resources/i18n.js +34 -0
  57. package/lib/module/translation/resources/i18n.js.map +1 -1
  58. package/lib/module/types/events.js +1 -0
  59. package/lib/module/types/events.js.map +1 -1
  60. package/lib/module/utils/conversation.js +3 -0
  61. package/lib/module/utils/conversation.js.map +1 -1
  62. package/lib/module/utils/messageUtils.js +95 -2
  63. package/lib/module/utils/messageUtils.js.map +1 -1
  64. package/lib/typescript/src/components/Avatar/Avatar.d.ts +1 -1
  65. package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -1
  66. package/lib/typescript/src/components/Avatar/Avatar.types.d.ts +5 -1
  67. package/lib/typescript/src/components/Avatar/Avatar.types.d.ts.map +1 -1
  68. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts.map +1 -1
  69. package/lib/typescript/src/components/Avatar/QuadAvatar.d.ts +16 -0
  70. package/lib/typescript/src/components/Avatar/QuadAvatar.d.ts.map +1 -0
  71. package/lib/typescript/src/components/Avatar/TripleAvatar.d.ts +15 -0
  72. package/lib/typescript/src/components/Avatar/TripleAvatar.d.ts.map +1 -0
  73. package/lib/typescript/src/components/Avatar/index.d.ts +2 -0
  74. package/lib/typescript/src/components/Avatar/index.d.ts.map +1 -1
  75. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +2 -2
  76. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  77. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +3 -1
  78. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
  79. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  80. package/lib/typescript/src/components/messages/groupNotificationMessage/index.d.ts +3 -0
  81. package/lib/typescript/src/components/messages/groupNotificationMessage/index.d.ts.map +1 -0
  82. package/lib/typescript/src/context/ChatDetailContext.d.ts +2 -0
  83. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
  84. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  85. package/lib/typescript/src/hooks/conversation/useGetGroupInfo.d.ts +2 -0
  86. package/lib/typescript/src/hooks/conversation/useGetGroupInfo.d.ts.map +1 -0
  87. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  88. package/lib/typescript/src/hooks/query-keys.d.ts +1 -0
  89. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  90. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  91. package/lib/typescript/src/screens/ChatInfo/ChatInfo.d.ts +1 -1
  92. package/lib/typescript/src/screens/ChatInfo/ChatInfo.d.ts.map +1 -1
  93. package/lib/typescript/src/screens/ChatInfo/ChatInfoActions.d.ts +3 -1
  94. package/lib/typescript/src/screens/ChatInfo/ChatInfoActions.d.ts.map +1 -1
  95. package/lib/typescript/src/screens/ChatInfo/ChatInfoDescriptionRow.d.ts +7 -0
  96. package/lib/typescript/src/screens/ChatInfo/ChatInfoDescriptionRow.d.ts.map +1 -0
  97. package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts.map +1 -1
  98. package/lib/typescript/src/screens/ChatInfo/GroupDescriptionModal.d.ts +9 -0
  99. package/lib/typescript/src/screens/ChatInfo/GroupDescriptionModal.d.ts.map +1 -0
  100. package/lib/typescript/src/screens/ChatInfo/types.d.ts +1 -0
  101. package/lib/typescript/src/screens/ChatInfo/types.d.ts.map +1 -1
  102. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  104. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  105. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +1 -1
  106. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -1
  107. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  108. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  109. package/lib/typescript/src/store/conversation.d.ts +11 -0
  110. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  111. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  112. package/lib/typescript/src/types/events.d.ts +6 -1
  113. package/lib/typescript/src/types/events.d.ts.map +1 -1
  114. package/lib/typescript/src/utils/conversation.d.ts +1 -0
  115. package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
  116. package/lib/typescript/src/utils/messageUtils.d.ts +6 -1
  117. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  118. package/package.json +2 -2
  119. package/src/build-ignore.d.ts +1 -0
  120. package/src/components/Avatar/Avatar.tsx +47 -0
  121. package/src/components/Avatar/Avatar.types.ts +5 -1
  122. package/src/components/Avatar/DoubleAvatar.tsx +16 -13
  123. package/src/components/Avatar/QuadAvatar.tsx +161 -0
  124. package/src/components/Avatar/TripleAvatar.tsx +119 -0
  125. package/src/components/Avatar/index.ts +2 -0
  126. package/src/components/ThreadCard/AvatarSection.tsx +58 -2
  127. package/src/components/ThreadCard/NamePrefixIcon.tsx +32 -25
  128. package/src/components/ThreadCard/ThreadCard.tsx +2 -0
  129. package/src/components/messages/groupNotificationMessage/index.tsx +28 -0
  130. package/src/context/ChatContext.tsx +1 -1
  131. package/src/context/ChatDetailContext.tsx +15 -0
  132. package/src/core/useChatListener.ts +175 -23
  133. package/src/hooks/conversation/useGetGroupInfo.ts +20 -0
  134. package/src/hooks/message/useSendMessage.ts +11 -3
  135. package/src/hooks/query-keys.ts +1 -0
  136. package/src/hooks/useConversationList.ts +17 -7
  137. package/src/screens/ChatInfo/ChatInfo.tsx +16 -0
  138. package/src/screens/ChatInfo/ChatInfoActions.tsx +68 -35
  139. package/src/screens/ChatInfo/ChatInfoDescriptionRow.tsx +102 -0
  140. package/src/screens/ChatInfo/ChatInfoProfile.tsx +6 -3
  141. package/src/screens/ChatInfo/GroupDescriptionModal.tsx +67 -0
  142. package/src/screens/ChatInfo/types.ts +1 -0
  143. package/src/screens/chat-detail/ChatComposer.tsx +19 -10
  144. package/src/screens/chat-detail/ChatDetail.tsx +12 -1
  145. package/src/screens/chat-detail/ChatDetailHeader.tsx +19 -3
  146. package/src/screens/chat-detail/conversationHeader.utils.ts +14 -6
  147. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +36 -1
  148. package/src/screens/inbox/MessagesTab.tsx +0 -1
  149. package/src/store/conversation.ts +62 -17
  150. package/src/translation/resources/i18n.ts +34 -0
  151. package/src/types/events.ts +6 -0
  152. package/src/utils/conversation.ts +6 -0
  153. package/src/utils/messageUtils.ts +103 -1
@@ -13,18 +13,22 @@ import { trans } from '../../translation';
13
13
  interface ChatInfoActionsProps {
14
14
  conversation?: ConversationItem;
15
15
  isTargetActive: boolean;
16
+ isGroup?: boolean;
16
17
  onPressOrders?: () => void;
17
18
  onPressCreateGroup?: () => void;
18
19
  onPressAddToGroup?: () => void;
20
+ onPressLeaveGroup?: () => void;
19
21
  }
20
22
 
21
23
  const ChatInfoActions = memo(
22
24
  ({
23
25
  conversation,
24
26
  isTargetActive,
27
+ isGroup,
25
28
  onPressOrders,
26
29
  onPressCreateGroup,
27
30
  onPressAddToGroup,
31
+ onPressLeaveGroup,
28
32
  }: ChatInfoActionsProps) => {
29
33
  const { pinConversation, unpinConversation } = usePinConversation();
30
34
  const isPinned = Boolean(conversation?.isPinned);
@@ -41,8 +45,10 @@ const ChatInfoActions = memo(
41
45
  const handleDeleteHistory = useCallback(() => {}, []);
42
46
 
43
47
  const primaryData = useMemo(() => {
44
- const items: KListItemBaseItemProps[] = [
45
- {
48
+ const items: KListItemBaseItemProps[] = [];
49
+
50
+ if (!isGroup) {
51
+ items.push({
46
52
  leftNode: { icon: { vectorName: 'receipt-o' } },
47
53
  onPress: onPressOrders,
48
54
  paddingH: '0.75rem',
@@ -52,34 +58,34 @@ const ChatInfoActions = memo(
52
58
  {trans('chat:info_orders', { name: showName })}
53
59
  </KLabel.Text>
54
60
  ),
55
- },
56
- ];
61
+ });
57
62
 
58
- if (isTargetActive) {
59
- items.push(
60
- {
61
- leftNode: { icon: { vectorName: 'user-three-o' } },
62
- onPress: onPressCreateGroup,
63
- paddingH: '0.75rem',
64
- paddingV: '0.75rem',
65
- customLabel: (
66
- <KLabel.Text typo="TextMdMedium" numberOfLines={1}>
67
- {trans('chat:info_create_group', { name: showName })}
68
- </KLabel.Text>
69
- ),
70
- },
71
- {
72
- leftNode: { icon: { vectorName: 'user-add-o' } },
73
- onPress: onPressAddToGroup,
74
- paddingH: '0.75rem',
75
- paddingV: '0.75rem',
76
- customLabel: (
77
- <KLabel.Text typo="TextMdMedium" numberOfLines={1}>
78
- {trans('chat:info_add_to_group')}
79
- </KLabel.Text>
80
- ),
81
- }
82
- );
63
+ if (isTargetActive) {
64
+ items.push(
65
+ {
66
+ leftNode: { icon: { vectorName: 'user-three-o' } },
67
+ onPress: onPressCreateGroup,
68
+ paddingH: '0.75rem',
69
+ paddingV: '0.75rem',
70
+ customLabel: (
71
+ <KLabel.Text typo="TextMdMedium" numberOfLines={1}>
72
+ {trans('chat:info_create_group', { name: showName })}
73
+ </KLabel.Text>
74
+ ),
75
+ },
76
+ {
77
+ leftNode: { icon: { vectorName: 'user-add-o' } },
78
+ onPress: onPressAddToGroup,
79
+ paddingH: '0.75rem',
80
+ paddingV: '0.75rem',
81
+ customLabel: (
82
+ <KLabel.Text typo="TextMdMedium" numberOfLines={1}>
83
+ {trans('chat:info_add_to_group')}
84
+ </KLabel.Text>
85
+ ),
86
+ }
87
+ );
88
+ }
83
89
  }
84
90
 
85
91
  items.push({
@@ -102,6 +108,7 @@ const ChatInfoActions = memo(
102
108
  return items;
103
109
  }, [
104
110
  showName,
111
+ isGroup,
105
112
  isTargetActive,
106
113
  isPinned,
107
114
  onPressOrders,
@@ -110,8 +117,35 @@ const ChatInfoActions = memo(
110
117
  handlePinPress,
111
118
  ]);
112
119
 
113
- const deleteData: KListItemBaseItemProps[] = useMemo(
114
- () => [
120
+ const dangerData: KListItemBaseItemProps[] = useMemo(() => {
121
+ if (isGroup) {
122
+ return [
123
+ {
124
+ leftNode: {
125
+ icon: {
126
+ vectorName: 'logout-o',
127
+ tintColor: KColors.danger.normal,
128
+ },
129
+ },
130
+ tintColor: KColors.danger.normal,
131
+ onPress: onPressLeaveGroup,
132
+ showChevron: false,
133
+ paddingH: '0.75rem',
134
+ paddingV: '0.75rem',
135
+ customLabel: (
136
+ <KLabel.Text
137
+ typo="TextMdMedium"
138
+ color={KColors.danger.normal}
139
+ numberOfLines={1}
140
+ >
141
+ {trans('chat:info_leave_group')}
142
+ </KLabel.Text>
143
+ ),
144
+ },
145
+ ];
146
+ }
147
+
148
+ return [
115
149
  {
116
150
  label: trans('chat:info_delete_history'),
117
151
  leftNode: {
@@ -135,9 +169,8 @@ const ChatInfoActions = memo(
135
169
  </KLabel.Text>
136
170
  ),
137
171
  },
138
- ],
139
- [handleDeleteHistory]
140
- );
172
+ ];
173
+ }, [isGroup, onPressLeaveGroup, handleDeleteHistory]);
141
174
 
142
175
  return (
143
176
  <>
@@ -158,7 +191,7 @@ const ChatInfoActions = memo(
158
191
  marginT="0.75rem"
159
192
  hiddenOverflow
160
193
  >
161
- <KListItem.Base data={deleteData} />
194
+ <KListItem.Base data={dangerData} />
162
195
  </KContainer.View>
163
196
  </>
164
197
  );
@@ -0,0 +1,102 @@
1
+ import { memo, useCallback, useState } from 'react';
2
+ import { StyleSheet, type TextLayoutEvent } from 'react-native';
3
+ import { KContainer, KLabel, KColors } from '@droppii/libs';
4
+ import { trans } from '../../translation';
5
+ import GroupDescriptionModal from './GroupDescriptionModal';
6
+
7
+ interface ChatInfoDescriptionRowProps {
8
+ introduction?: string;
9
+ groupName?: string;
10
+ }
11
+
12
+ const EXTRA_TEXT_LENGTH = 14; // Length of "...View More" text
13
+ const MAX_LINES = 2;
14
+
15
+ const ChatInfoDescriptionRow = memo(
16
+ ({ introduction, groupName }: ChatInfoDescriptionRowProps) => {
17
+ const [modalVisible, setModalVisible] = useState(false);
18
+ const handlePressDescription = useCallback(() => setModalVisible(true), []);
19
+ const handleCloseDescription = useCallback(
20
+ () => setModalVisible(false),
21
+ []
22
+ );
23
+
24
+ const [isTruncated, setIsTruncated] = useState(false);
25
+ const [displayedText, setDisplayedText] = useState(introduction);
26
+
27
+ const handleTextLayout = useCallback((event: TextLayoutEvent) => {
28
+ const isTextTruncated = event.nativeEvent.lines.length > MAX_LINES;
29
+
30
+ setIsTruncated(isTextTruncated);
31
+
32
+ if (isTextTruncated) {
33
+ setDisplayedText(
34
+ event.nativeEvent.lines
35
+ .slice(0, MAX_LINES)
36
+ .map((line) => line.text)
37
+ .join(' ')
38
+ .slice(0, -EXTRA_TEXT_LENGTH) + '...'
39
+ );
40
+ }
41
+ }, []);
42
+
43
+ const textProps = {
44
+ color: KColors.gray.dark,
45
+ typo: 'TextMdMedium' as const,
46
+ paddingH: '0.75rem' as const,
47
+ };
48
+
49
+ if (!introduction) {
50
+ return null;
51
+ }
52
+
53
+ return (
54
+ <>
55
+ <KContainer.View
56
+ background={KColors.white}
57
+ br="3x"
58
+ marginH="0.75rem"
59
+ marginT="0.75rem"
60
+ paddingV="0.75rem"
61
+ >
62
+ <KLabel.Text
63
+ {...textProps}
64
+ style={styles.hiddenText}
65
+ numberOfLines={MAX_LINES + 1}
66
+ onTextLayout={handleTextLayout}
67
+ >
68
+ {introduction}
69
+ </KLabel.Text>
70
+ <KLabel.Text {...textProps} numberOfLines={2}>
71
+ {displayedText}
72
+ <KContainer.VisibleView visible={isTruncated}>
73
+ <KLabel.Text
74
+ {...textProps}
75
+ color={KColors.primary.normal}
76
+ onPress={handlePressDescription}
77
+ >
78
+ {trans('chat:info_view_more')}
79
+ </KLabel.Text>
80
+ </KContainer.VisibleView>
81
+ </KLabel.Text>
82
+ </KContainer.View>
83
+ <GroupDescriptionModal
84
+ visible={modalVisible}
85
+ groupName={groupName}
86
+ introduction={introduction}
87
+ onClose={handleCloseDescription}
88
+ />
89
+ </>
90
+ );
91
+ }
92
+ );
93
+
94
+ export default ChatInfoDescriptionRow;
95
+
96
+ const styles = StyleSheet.create({
97
+ hiddenText: {
98
+ position: 'absolute',
99
+ opacity: 0,
100
+ zIndex: -1,
101
+ },
102
+ });
@@ -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>
@@ -0,0 +1,67 @@
1
+ import { memo } from 'react';
2
+ import { Modal, StyleSheet } from 'react-native';
3
+ import { KContainer, KNavigation, KLabel, KColors } from '@droppii/libs';
4
+ import { trans } from '../../translation';
5
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
6
+
7
+ interface GroupDescriptionModalProps {
8
+ visible: boolean;
9
+ groupName?: string;
10
+ introduction?: string;
11
+ onClose: () => void;
12
+ }
13
+
14
+ const GroupDescriptionModal = memo(
15
+ ({
16
+ visible,
17
+ groupName,
18
+ introduction,
19
+ onClose,
20
+ }: GroupDescriptionModalProps) => (
21
+ <Modal
22
+ visible={visible}
23
+ transparent={false}
24
+ statusBarTranslucent
25
+ style={styles.modal}
26
+ presentationStyle="fullScreen"
27
+ animationType="slide"
28
+ onRequestClose={onClose}
29
+ >
30
+ <SafeAreaProvider>
31
+ <KContainer.Page flex edges={['bottom']} background={KColors.white}>
32
+ <KNavigation.Header
33
+ theme="light"
34
+ alignment="left"
35
+ size="small"
36
+ leftButton={{
37
+ id: 'GroupDescriptionModalCloseButton',
38
+ icon: { vectorName: 'close-o' },
39
+ onPress: onClose,
40
+ }}
41
+ title={trans('chat:group_description_title')}
42
+ subTitle={groupName}
43
+ shadow
44
+ />
45
+ <KLabel.Text
46
+ paddingH="1rem"
47
+ paddingT="0.75rem"
48
+ typo="TextMdNormal"
49
+ color={KColors.gray.dark}
50
+ >
51
+ {introduction}
52
+ </KLabel.Text>
53
+ </KContainer.Page>
54
+ </SafeAreaProvider>
55
+ </Modal>
56
+ )
57
+ );
58
+
59
+ GroupDescriptionModal.displayName = 'GroupDescriptionModal';
60
+
61
+ export default GroupDescriptionModal;
62
+
63
+ const styles = StyleSheet.create({
64
+ modal: {
65
+ flex: 1,
66
+ },
67
+ });
@@ -8,4 +8,5 @@ export interface ChatInfoProps {
8
8
  onPressOrders?: () => void;
9
9
  onPressCreateGroup?: () => void;
10
10
  onPressAddToGroup?: () => void;
11
+ onPressLeaveGroup?: () => void;
11
12
  }
@@ -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
  }