@droppii-org/chat-mobile 0.2.106 → 0.2.108

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 (187) hide show
  1. package/lib/module/components/ThreadCard/ThreadCard.js +2 -0
  2. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  3. package/lib/module/components/messages/ExpandableMessageText.js +112 -0
  4. package/lib/module/components/messages/ExpandableMessageText.js.map +1 -0
  5. package/lib/module/components/messages/PreviewScrollableText.js +80 -0
  6. package/lib/module/components/messages/PreviewScrollableText.js.map +1 -0
  7. package/lib/module/components/messages/fileMessage/index.js +9 -2
  8. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  9. package/lib/module/components/messages/linkMessage/index.js +21 -12
  10. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  11. package/lib/module/components/messages/quotedMessage/index.js +18 -3
  12. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  13. package/lib/module/components/messages/textMessage/index.js +7 -1
  14. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  15. package/lib/module/components/messages/types.js.map +1 -1
  16. package/lib/module/context/TimelinePressGuardContext.js +14 -0
  17. package/lib/module/context/TimelinePressGuardContext.js.map +1 -0
  18. package/lib/module/hooks/useConversationList.js +4 -0
  19. package/lib/module/hooks/useConversationList.js.map +1 -1
  20. package/lib/module/hooks/useDismissKeyboardFirst.js +19 -0
  21. package/lib/module/hooks/useDismissKeyboardFirst.js.map +1 -0
  22. package/lib/module/hooks/useExpandableText.js +125 -0
  23. package/lib/module/hooks/useExpandableText.js.map +1 -0
  24. package/lib/module/hooks/useGuardedPress.js +19 -0
  25. package/lib/module/hooks/useGuardedPress.js.map +1 -0
  26. package/lib/module/screens/chat-detail/ChatComposer.js +89 -498
  27. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  28. package/lib/module/screens/chat-detail/ChatDetail.js +49 -20
  29. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  30. package/lib/module/screens/chat-detail/ChatListLegend.js +113 -76
  31. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  32. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +7 -0
  33. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
  34. package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js +47 -0
  35. package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js.map +1 -0
  36. package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js +149 -0
  37. package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js.map +1 -0
  38. package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js +104 -0
  39. package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js.map +1 -0
  40. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +0 -17
  41. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
  42. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +16 -2
  43. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
  44. package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js +122 -0
  45. package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js.map +1 -0
  46. package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js +46 -0
  47. package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js.map +1 -0
  48. package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js +75 -0
  49. package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js.map +1 -0
  50. package/lib/module/screens/chat-detail/hooks/useComposerPanels.js +136 -0
  51. package/lib/module/screens/chat-detail/hooks/useComposerPanels.js.map +1 -0
  52. package/lib/module/screens/chat-detail/hooks/useComposerSend.js +74 -0
  53. package/lib/module/screens/chat-detail/hooks/useComposerSend.js.map +1 -0
  54. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +12 -3
  55. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  56. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +11 -2
  57. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
  58. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +79 -6
  59. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
  60. package/lib/module/screens/chat-detail/messageToolbox/useClonePreview.js +30 -0
  61. package/lib/module/screens/chat-detail/messageToolbox/useClonePreview.js.map +1 -0
  62. package/lib/module/screens/chat-detail/types.js +5 -0
  63. package/lib/module/screens/chat-detail/types.js.map +1 -1
  64. package/lib/module/store/expandedMessage.js +28 -0
  65. package/lib/module/store/expandedMessage.js.map +1 -0
  66. package/lib/module/store/message.js +14 -0
  67. package/lib/module/store/message.js.map +1 -1
  68. package/lib/module/utils/composerPanelDismissGesture.js +52 -0
  69. package/lib/module/utils/composerPanelDismissGesture.js.map +1 -0
  70. package/lib/module/utils/expandableText.js +41 -0
  71. package/lib/module/utils/expandableText.js.map +1 -0
  72. package/lib/module/utils/inlineTruncation.js +51 -0
  73. package/lib/module/utils/inlineTruncation.js.map +1 -0
  74. package/lib/module/utils/perf-marker.js +44 -0
  75. package/lib/module/utils/perf-marker.js.map +1 -0
  76. package/lib/module/utils/truncateTextChildren.js +38 -0
  77. package/lib/module/utils/truncateTextChildren.js.map +1 -0
  78. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  79. package/lib/typescript/src/components/messages/ExpandableMessageText.d.ts +25 -0
  80. package/lib/typescript/src/components/messages/ExpandableMessageText.d.ts.map +1 -0
  81. package/lib/typescript/src/components/messages/PreviewScrollableText.d.ts +29 -0
  82. package/lib/typescript/src/components/messages/PreviewScrollableText.d.ts.map +1 -0
  83. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
  85. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  87. package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
  88. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/messages/types.d.ts +18 -0
  90. package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
  91. package/lib/typescript/src/context/TimelinePressGuardContext.d.ts +3 -0
  92. package/lib/typescript/src/context/TimelinePressGuardContext.d.ts.map +1 -0
  93. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  94. package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts +8 -0
  95. package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts.map +1 -0
  96. package/lib/typescript/src/hooks/useExpandableText.d.ts +13 -0
  97. package/lib/typescript/src/hooks/useExpandableText.d.ts.map +1 -0
  98. package/lib/typescript/src/hooks/useGuardedPress.d.ts +9 -0
  99. package/lib/typescript/src/hooks/useGuardedPress.d.ts.map +1 -0
  100. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +7 -3
  101. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  102. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +5 -1
  104. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  105. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
  106. package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts +12 -0
  107. package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts.map +1 -0
  108. package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts +30 -0
  109. package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts.map +1 -0
  110. package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts +19 -0
  111. package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts.map +1 -0
  112. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +0 -1
  113. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
  114. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +7 -0
  115. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
  116. package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts +7 -0
  117. package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts.map +1 -0
  118. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts +17 -0
  119. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts.map +1 -0
  120. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts +12 -0
  121. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts.map +1 -0
  122. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts +25 -0
  123. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts.map +1 -0
  124. package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts +16 -0
  125. package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts.map +1 -0
  126. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +11 -1
  127. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  128. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
  129. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts +14 -1
  130. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
  131. package/lib/typescript/src/screens/chat-detail/messageToolbox/useClonePreview.d.ts +18 -0
  132. package/lib/typescript/src/screens/chat-detail/messageToolbox/useClonePreview.d.ts.map +1 -0
  133. package/lib/typescript/src/screens/chat-detail/types.d.ts +8 -0
  134. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  135. package/lib/typescript/src/store/expandedMessage.d.ts +15 -0
  136. package/lib/typescript/src/store/expandedMessage.d.ts.map +1 -0
  137. package/lib/typescript/src/store/message.d.ts.map +1 -1
  138. package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts +33 -0
  139. package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts.map +1 -0
  140. package/lib/typescript/src/utils/expandableText.d.ts +18 -0
  141. package/lib/typescript/src/utils/expandableText.d.ts.map +1 -0
  142. package/lib/typescript/src/utils/inlineTruncation.d.ts +24 -0
  143. package/lib/typescript/src/utils/inlineTruncation.d.ts.map +1 -0
  144. package/lib/typescript/src/utils/perf-marker.d.ts +8 -0
  145. package/lib/typescript/src/utils/perf-marker.d.ts.map +1 -0
  146. package/lib/typescript/src/utils/truncateTextChildren.d.ts +3 -0
  147. package/lib/typescript/src/utils/truncateTextChildren.d.ts.map +1 -0
  148. package/package.json +2 -1
  149. package/src/components/ThreadCard/ThreadCard.tsx +2 -0
  150. package/src/components/messages/ExpandableMessageText.tsx +145 -0
  151. package/src/components/messages/PreviewScrollableText.tsx +98 -0
  152. package/src/components/messages/fileMessage/index.tsx +17 -3
  153. package/src/components/messages/linkMessage/index.tsx +30 -21
  154. package/src/components/messages/quotedMessage/index.tsx +19 -3
  155. package/src/components/messages/textMessage/index.tsx +11 -2
  156. package/src/components/messages/types.ts +18 -0
  157. package/src/context/TimelinePressGuardContext.tsx +15 -0
  158. package/src/hooks/useConversationList.ts +2 -0
  159. package/src/hooks/useDismissKeyboardFirst.ts +19 -0
  160. package/src/hooks/useExpandableText.ts +143 -0
  161. package/src/hooks/useGuardedPress.ts +20 -0
  162. package/src/screens/chat-detail/ChatComposer.tsx +105 -631
  163. package/src/screens/chat-detail/ChatDetail.tsx +49 -20
  164. package/src/screens/chat-detail/ChatListLegend.tsx +122 -71
  165. package/src/screens/chat-detail/PinnedMessagesSheet.tsx +8 -0
  166. package/src/screens/chat-detail/components/ChatComposerAccessorySlot.tsx +61 -0
  167. package/src/screens/chat-detail/components/ChatComposerInputArea.tsx +198 -0
  168. package/src/screens/chat-detail/components/ChatComposerStatusBanner.tsx +116 -0
  169. package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +0 -29
  170. package/src/screens/chat-detail/hooks/useChatComposerState.ts +25 -2
  171. package/src/screens/chat-detail/hooks/useComposerBotMenu.tsx +140 -0
  172. package/src/screens/chat-detail/hooks/useComposerPanelActions.ts +57 -0
  173. package/src/screens/chat-detail/hooks/useComposerPanelDismiss.ts +111 -0
  174. package/src/screens/chat-detail/hooks/useComposerPanels.ts +197 -0
  175. package/src/screens/chat-detail/hooks/useComposerSend.ts +98 -0
  176. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +24 -2
  177. package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +20 -1
  178. package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +111 -5
  179. package/src/screens/chat-detail/messageToolbox/useClonePreview.ts +34 -0
  180. package/src/screens/chat-detail/types.ts +9 -0
  181. package/src/store/expandedMessage.ts +42 -0
  182. package/src/store/message.ts +14 -0
  183. package/src/utils/composerPanelDismissGesture.ts +72 -0
  184. package/src/utils/expandableText.ts +51 -0
  185. package/src/utils/inlineTruncation.ts +65 -0
  186. package/src/utils/perf-marker.ts +39 -0
  187. package/src/utils/truncateTextChildren.ts +46 -0
@@ -0,0 +1,198 @@
1
+ import { useCallback, useMemo, type RefObject } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import { KContainer, KColors, KImage } from '@droppii/libs';
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+ import { PeerType } from '@droppii/openim-rn-client-sdk';
6
+
7
+ import { ChatQuickActions } from '../ChatQuickActions';
8
+ import { ChatLinkPreview } from '../ChatLinkPreview';
9
+ import { ChatCurrentQuotedMessage } from '../ChatCurrentQuotedMessage';
10
+ import { ChatMessageInput } from './ChatMessageInput';
11
+ import { MentionSuggestionList } from './MentionSuggestionList';
12
+ import { AttachmentPreview } from '../../../components/AttachmentPreview';
13
+ import { DEFAULT_QUICK_ACTIONS } from '../constants';
14
+ import { useChatDetailContext } from '../../../context/ChatDetailContext';
15
+ import { useAttachmentMapping } from '../hooks/useAttachmentMapping';
16
+ import type { ActivePanelType } from '../hooks/useChatComposerState';
17
+ import type { useMentionComposer } from '../hooks/useMentionComposer';
18
+ import type { useChatCompose } from '../../../hooks/useChatCompose';
19
+ import type { AttachmentType } from '../../../types/attachment';
20
+
21
+ interface ChatComposerInputAreaProps {
22
+ compose: ReturnType<typeof useChatCompose>;
23
+ mentionComposer: ReturnType<typeof useMentionComposer>;
24
+ activePanelType: ActivePanelType;
25
+ isAttachmentOpen: boolean;
26
+ isInputFocused: boolean;
27
+ isMultilineInput: boolean;
28
+ inputRef: RefObject<any>;
29
+ onToggleAttachment: () => void;
30
+ onToggleSticker: () => void;
31
+ onChangeText: (text?: string) => void;
32
+ onInputFocus: () => void;
33
+ onInputBlur: () => void;
34
+ onInputContentSizeChange: (event: {
35
+ nativeEvent: { contentSize: { height: number } };
36
+ }) => void;
37
+ onOpenBotMenu?: () => void;
38
+ onSend: () => void;
39
+ }
40
+
41
+ // The composer's normal state: quoted message, previews, mention picker and
42
+ // the input row (attachment toggle / input / send).
43
+ export const ChatComposerInputArea = ({
44
+ compose,
45
+ mentionComposer,
46
+ activePanelType,
47
+ isAttachmentOpen,
48
+ isInputFocused,
49
+ isMultilineInput,
50
+ inputRef,
51
+ onToggleAttachment,
52
+ onToggleSticker,
53
+ onChangeText,
54
+ onInputFocus,
55
+ onInputBlur,
56
+ onInputContentSizeChange,
57
+ onOpenBotMenu,
58
+ onSend,
59
+ }: ChatComposerInputAreaProps) => {
60
+ const insets = useSafeAreaInsets();
61
+ const { conversation } = useChatDetailContext();
62
+
63
+ const inputRowStyle = useMemo(
64
+ () => ({ paddingBottom: isAttachmentOpen ? 8 : insets.bottom + 12 }),
65
+ [insets.bottom, isAttachmentOpen]
66
+ );
67
+
68
+ const attachments = useAttachmentMapping({
69
+ selectedImages: compose.selectedImages,
70
+ selectedFiles: compose.selectedFiles,
71
+ });
72
+
73
+ const onRemove = useCallback(
74
+ (type: AttachmentType, data: unknown) => {
75
+ if (type === 'file') return compose.removeFile(data as string);
76
+ return compose.removeImage(data as number);
77
+ },
78
+ [compose]
79
+ );
80
+
81
+ return (
82
+ <KContainer.View background={'#F9F9FA'}>
83
+ <ChatCurrentQuotedMessage />
84
+ <KContainer.VisibleView visible={conversation?.peerType === PeerType.Bot}>
85
+ <ChatQuickActions
86
+ visible
87
+ actions={DEFAULT_QUICK_ACTIONS}
88
+ onActionPress={(action) =>
89
+ console.log('[ChatComposer] quick action:', action.id)
90
+ }
91
+ />
92
+ </KContainer.VisibleView>
93
+ {compose.showLinkPreview && (
94
+ <ChatLinkPreview
95
+ metadata={compose.urlMetadata}
96
+ isLoading={compose.isMetadataLoading}
97
+ onDismiss={compose.dismissPreview}
98
+ />
99
+ )}
100
+
101
+ <AttachmentPreview
102
+ visible={compose.isImagePreviewVisible || compose.isFilePreviewVisible}
103
+ attachments={attachments}
104
+ onRemove={onRemove}
105
+ />
106
+
107
+ <KContainer.View style={styles.inputRowWrapper}>
108
+ <KContainer.VisibleView visible={mentionComposer.isPanelOpen}>
109
+ <KContainer.View style={styles.mentionPanel}>
110
+ <MentionSuggestionList
111
+ suggestions={mentionComposer.suggestions}
112
+ isSearching={mentionComposer.isSearching}
113
+ isLoadingMore={mentionComposer.isLoadingMore}
114
+ hasMore={mentionComposer.hasMore}
115
+ onLoadMore={mentionComposer.loadMore}
116
+ onSelect={mentionComposer.selectSuggestion}
117
+ />
118
+ </KContainer.View>
119
+ </KContainer.VisibleView>
120
+
121
+ <KContainer.View
122
+ row
123
+ alignItems
124
+ paddingH="0.75rem"
125
+ paddingT="0.75rem"
126
+ paddingB="0.75rem"
127
+ style={inputRowStyle}
128
+ >
129
+ <KContainer.Touchable onPress={onToggleAttachment} center br="round">
130
+ <KImage.VectorIcons
131
+ name={
132
+ activePanelType === 'attachment'
133
+ ? 'close-circle-b'
134
+ : 'plus-circle-o'
135
+ }
136
+ size={24}
137
+ color={
138
+ activePanelType === 'attachment'
139
+ ? KColors.palette.primary.w400
140
+ : KColors.palette.gray.w600
141
+ }
142
+ />
143
+ </KContainer.Touchable>
144
+
145
+ <ChatMessageInput
146
+ value={compose.text}
147
+ placeholder="Nhập tin nhắn..."
148
+ onChangeText={onChangeText}
149
+ onFocus={onInputFocus}
150
+ onBlur={onInputBlur}
151
+ onContentSizeChange={onInputContentSizeChange}
152
+ onSelectionChange={mentionComposer.handleSelectionChange}
153
+ mentions={mentionComposer.mentions}
154
+ isInputFocused={isInputFocused}
155
+ isMultilineInput={isMultilineInput}
156
+ inputRef={inputRef}
157
+ onEmojiPress={onToggleSticker}
158
+ onSuggestPress={onOpenBotMenu}
159
+ activePanelType={activePanelType}
160
+ />
161
+
162
+ <KContainer.Touchable
163
+ onPress={compose.isUploading ? undefined : onSend}
164
+ pointerEvents={compose.isUploading ? 'none' : 'auto'}
165
+ width={40}
166
+ height={40}
167
+ center
168
+ br="round"
169
+ background={
170
+ compose.isUploading
171
+ ? KColors.palette.primary.w100
172
+ : KColors.palette.primary.w400
173
+ }
174
+ marginL="0.75rem"
175
+ opacity={compose.isUploading ? 0.25 : 1}
176
+ >
177
+ <KImage.VectorIcons name="send-b" size={20} color={KColors.white} />
178
+ </KContainer.Touchable>
179
+ </KContainer.View>
180
+ </KContainer.View>
181
+ </KContainer.View>
182
+ );
183
+ };
184
+
185
+ const styles = StyleSheet.create({
186
+ inputRowWrapper: {
187
+ position: 'relative',
188
+ },
189
+ // Floats above the input row without going through the accessorySlot
190
+ // (attachment/sticker) render path — that slot intentionally takes over
191
+ // the keyboard area, which the mention picker must not do (finding #5).
192
+ mentionPanel: {
193
+ position: 'absolute',
194
+ left: 0,
195
+ right: 0,
196
+ bottom: '100%',
197
+ },
198
+ });
@@ -0,0 +1,116 @@
1
+ import { KContainer, KColors, KImage, KButton, KLabel } from '@droppii/libs';
2
+
3
+ import { trans } from '../../../translation';
4
+
5
+ export type ComposerStatusVariant =
6
+ | 'sellerInactive'
7
+ | 'botStart'
8
+ | 'notInGroup'
9
+ | 'sendDenied';
10
+
11
+ // Priority order matters — mirrors the composer's original if/else chain.
12
+ export const getComposerStatusVariant = ({
13
+ isDisableCompose,
14
+ isGroupConversation,
15
+ isNewBotConversation,
16
+ isNotInGroup,
17
+ isSendMessageAllowed,
18
+ }: {
19
+ isDisableCompose: boolean;
20
+ isGroupConversation: boolean;
21
+ isNewBotConversation: boolean;
22
+ isNotInGroup: boolean;
23
+ isSendMessageAllowed: boolean;
24
+ }): ComposerStatusVariant | null => {
25
+ if (isDisableCompose && !isGroupConversation) return 'sellerInactive';
26
+ if (isNewBotConversation) return 'botStart';
27
+ if (isNotInGroup) return 'notInGroup';
28
+ if (!isSendMessageAllowed) return 'sendDenied';
29
+ return null;
30
+ };
31
+
32
+ interface ChatComposerStatusBannerProps {
33
+ variant: ComposerStatusVariant;
34
+ isRunFlowPending?: boolean;
35
+ onStartConversationFlow?: () => void;
36
+ isJoiningGroup?: boolean;
37
+ onPressJoinGroup?: () => void;
38
+ sendDeniedReason?: string;
39
+ }
40
+
41
+ const InfoLine = ({ text }: { text?: string }) => (
42
+ <>
43
+ <KContainer.View marginR="0.25rem">
44
+ <KImage.VectorIcons
45
+ name="info-circle-o"
46
+ size={16}
47
+ color={KColors.palette.gray.w600}
48
+ />
49
+ </KContainer.View>
50
+ <KLabel.Text typo="TextXsNormal">{text}</KLabel.Text>
51
+ </>
52
+ );
53
+
54
+ // The composer's non-input states — rendered in place of the input row.
55
+ export const ChatComposerStatusBanner = ({
56
+ variant,
57
+ isRunFlowPending,
58
+ onStartConversationFlow,
59
+ isJoiningGroup = false,
60
+ onPressJoinGroup,
61
+ sendDeniedReason,
62
+ }: ChatComposerStatusBannerProps) => {
63
+ switch (variant) {
64
+ case 'sellerInactive':
65
+ return (
66
+ <KContainer.View row alignItems justifyContent>
67
+ <InfoLine text={trans('chat:compose_disabled_seller_inactive')} />
68
+ </KContainer.View>
69
+ );
70
+ case 'botStart':
71
+ return (
72
+ <KContainer.View padding={'1.5rem'} paddingB={0}>
73
+ <KButton.Solid
74
+ size="md"
75
+ label={trans('start')}
76
+ stretch
77
+ icon={{ vectorName: 'chat-square-o', size: 20 }}
78
+ loading={isRunFlowPending}
79
+ onPress={onStartConversationFlow}
80
+ />
81
+ </KContainer.View>
82
+ );
83
+ case 'notInGroup':
84
+ return (
85
+ <KContainer.View
86
+ paddingH="0.75rem"
87
+ justifyContent="center"
88
+ brTW={1}
89
+ brTC={KColors.border.light}
90
+ >
91
+ <KLabel.Text
92
+ typo="TextSmNormal"
93
+ textAlign="center"
94
+ paddingV={'0.75rem'}
95
+ >
96
+ {trans('group:not_in_group')}
97
+ </KLabel.Text>
98
+ <KButton.Solid
99
+ size="md"
100
+ label={trans('group:join')}
101
+ stretch
102
+ loading={isJoiningGroup}
103
+ onPress={onPressJoinGroup}
104
+ />
105
+ </KContainer.View>
106
+ );
107
+ case 'sendDenied':
108
+ return (
109
+ <KContainer.View row alignItems justifyContent paddingT={'1rem'}>
110
+ <InfoLine text={sendDeniedReason} />
111
+ </KContainer.View>
112
+ );
113
+ default:
114
+ return null;
115
+ }
116
+ };
@@ -1,11 +1,9 @@
1
1
  import { useCallback, useEffect } from 'react';
2
- import { Keyboard } from 'react-native';
3
2
  import {
4
3
  useAnimatedStyle,
5
4
  useSharedValue,
6
5
  withTiming,
7
6
  } from 'react-native-reanimated';
8
- import { useKeyboardState } from 'react-native-keyboard-controller';
9
7
 
10
8
  const ACCESSORY_ANIMATION_MS = 250;
11
9
 
@@ -17,7 +15,6 @@ interface UseChatComposerAnimationProps {
17
15
  interface UseChatComposerAnimationReturn {
18
16
  attachmentHeightSV: any;
19
17
  accessorySlotStyle: any;
20
- toggleAttachmentVisibility: () => void;
21
18
  openAttachment: () => void;
22
19
  openWithHeight: (height: number) => void;
23
20
  closeAttachment: (speedUp?: boolean, immediate?: boolean) => void;
@@ -30,8 +27,6 @@ export const useChatComposerAnimation = ({
30
27
  const panelHeightSV = useSharedValue(panelHeight);
31
28
  const attachmentHeightSV = useSharedValue(0);
32
29
 
33
- const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
34
-
35
30
  useEffect(() => {
36
31
  panelHeightSV.value = panelHeight;
37
32
  }, [panelHeight, panelHeightSV]);
@@ -71,33 +66,9 @@ export const useChatComposerAnimation = ({
71
66
  [attachmentHeightSV, onAttachmentOpenChange]
72
67
  );
73
68
 
74
- const toggleAttachmentVisibility = useCallback(() => {
75
- // Use animated value as source of truth, not stale state
76
- const isOpen = attachmentHeightSV.value > 0;
77
-
78
- if (isOpen) {
79
- closeAttachment();
80
- return;
81
- }
82
-
83
- if (isKeyboardVisible) {
84
- Keyboard.dismiss();
85
- onAttachmentOpenChange(true);
86
- }
87
-
88
- openAttachment();
89
- }, [
90
- closeAttachment,
91
- isKeyboardVisible,
92
- onAttachmentOpenChange,
93
- openAttachment,
94
- attachmentHeightSV,
95
- ]);
96
-
97
69
  return {
98
70
  attachmentHeightSV,
99
71
  accessorySlotStyle,
100
- toggleAttachmentVisibility,
101
72
  openAttachment,
102
73
  openWithHeight,
103
74
  closeAttachment,
@@ -1,8 +1,15 @@
1
- import { useState, useMemo, useRef } from 'react';
1
+ import { useState, useMemo, useRef, useCallback, useEffect } from 'react';
2
+ import { Platform } from 'react-native';
3
+
4
+ const IS_IOS = Platform.OS === 'ios';
5
+ const INPUT_LINE_HEIGHT = 22.4;
6
+ const WRAPPED_HEIGHT_THRESHOLD = IS_IOS
7
+ ? INPUT_LINE_HEIGHT + 4
8
+ : INPUT_LINE_HEIGHT + 12;
2
9
 
3
10
  // 'mention' is used only for mutual-exclusion (closing attachment/sticker
4
11
  // when the mention picker opens) — the mention picker itself renders as its
5
- // own absolutely-positioned sibling in ChatComposer, not through this
12
+ // own absolutely-positioned sibling in ChatComposerInputArea, not through this
6
13
  // state's render slot, since it must float above the input while the
7
14
  // keyboard stays open (attachment/sticker intentionally take over the
8
15
  // keyboard area, which the mention picker must not do).
@@ -18,6 +25,9 @@ interface UseChatComposerStateReturn {
18
25
  isInputWrapped: boolean;
19
26
  setIsInputWrapped: (wrapped: boolean) => void;
20
27
  isMultilineInput: boolean;
28
+ handleInputContentSizeChange: (event: {
29
+ nativeEvent: { contentSize: { height: number } };
30
+ }) => void;
21
31
  inputRef: React.RefObject<any>;
22
32
  }
23
33
 
@@ -38,6 +48,18 @@ export const useChatComposerState = (
38
48
  return value.includes('\n') || isInputWrapped;
39
49
  }, [isInputWrapped, value]);
40
50
 
51
+ const handleInputContentSizeChange = useCallback(
52
+ (event: { nativeEvent: { contentSize: { height: number } } }) => {
53
+ const { height } = event.nativeEvent.contentSize;
54
+ setIsInputWrapped(height > WRAPPED_HEIGHT_THRESHOLD);
55
+ },
56
+ []
57
+ );
58
+
59
+ useEffect(() => {
60
+ if (!value) setIsInputWrapped(false);
61
+ }, [value]);
62
+
41
63
  return {
42
64
  activePanelType,
43
65
  setActivePanelType,
@@ -48,6 +70,7 @@ export const useChatComposerState = (
48
70
  isInputWrapped,
49
71
  setIsInputWrapped,
50
72
  isMultilineInput,
73
+ handleInputContentSizeChange,
51
74
  inputRef,
52
75
  };
53
76
  };
@@ -0,0 +1,140 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { KContainer, KColors, KLabel } from '@droppii/libs';
3
+
4
+ import { trans } from '../../../translation';
5
+ import { UIUtils } from '../../../utils/UIUtils';
6
+ import {
7
+ useCloseSupportSession,
8
+ useResetFlow,
9
+ useRunFlow,
10
+ } from '../../../hooks/flows/useFlow';
11
+ import type { InputButtonItem } from '../../../types/flows';
12
+ import { DEFAULT_BUTTON_ID, isContextButton } from '../../../utils/flows';
13
+ import { DFlowState } from '../../../types/chat';
14
+ import { useChatDetailContext } from '../../../context/ChatDetailContext';
15
+ import { openBotMenuSheet } from '../BotMenuSheet';
16
+
17
+ export const useComposerBotMenu = () => {
18
+ const {
19
+ isBotConversation,
20
+ conversationFlow,
21
+ conversationFlowMainMenu,
22
+ flowState,
23
+ } = useChatDetailContext();
24
+ const { mutate: runFlow, isPending } = useRunFlow();
25
+ const { mutate: resetFlow } = useResetFlow();
26
+ const { mutateAsync: closeSupportSession } = useCloseSupportSession();
27
+
28
+ const onStartConversationFlow = useCallback(() => {
29
+ runFlow({
30
+ groupId: conversationFlow?.flowCurrentStep?.id || '',
31
+ actionId: null,
32
+ childId: null,
33
+ plainText: trans('start'),
34
+ });
35
+ }, [runFlow, conversationFlow]);
36
+
37
+ const botMenuOptions = useMemo(() => {
38
+ const apiOptions =
39
+ conversationFlowMainMenu?.filter(
40
+ (item) => !isContextButton(item.label)
41
+ ) || [];
42
+
43
+ const hasDefaultButton = apiOptions.some(
44
+ (item) => item.id === DEFAULT_BUTTON_ID
45
+ );
46
+
47
+ if (hasDefaultButton) return apiOptions;
48
+
49
+ return [...apiOptions, { id: DEFAULT_BUTTON_ID, label: trans('restart') }];
50
+ }, [conversationFlowMainMenu]);
51
+
52
+ const hasBotMenuOptions = isBotConversation && botMenuOptions.length > 0;
53
+
54
+ const runBotMenuOption = useCallback(
55
+ (item: InputButtonItem) => {
56
+ if (item.id === DEFAULT_BUTTON_ID) {
57
+ resetFlow();
58
+ return;
59
+ }
60
+ runFlow({
61
+ actionId: item.id,
62
+ groupId: '',
63
+ childId: '',
64
+ plainText: item.label,
65
+ });
66
+ },
67
+ [runFlow, resetFlow]
68
+ );
69
+
70
+ const handleSelectBotMenuOption = useCallback(
71
+ (item: InputButtonItem) => {
72
+ if (flowState === DFlowState.CS) {
73
+ UIUtils.popup.open({
74
+ header: {
75
+ title: { text: trans('chat:end_support_session_title') },
76
+ alignment: 'center',
77
+ },
78
+ body: {
79
+ renderContent: () => (
80
+ <KContainer.View padding={'1rem'}>
81
+ <KLabel.RichText
82
+ typo="TextMdNormal"
83
+ textAlign="center"
84
+ color={KColors.gray.dark}
85
+ richTextOptions={{
86
+ i18nKey: trans('chat:end_support_session_desc'),
87
+ components: {
88
+ bold: (
89
+ <KLabel.Text
90
+ typo="TextMdBold"
91
+ color={KColors.gray.dark}
92
+ />
93
+ ),
94
+ },
95
+ }}
96
+ />
97
+ </KContainer.View>
98
+ ),
99
+ },
100
+ actions: {
101
+ buttons: [
102
+ {
103
+ label: trans('chat:end_support_session_secondary'),
104
+ kind: 'light',
105
+ onPress: () => {},
106
+ },
107
+ {
108
+ label: trans('chat:end_support_session_primary'),
109
+ kind: 'danger',
110
+ onPress: async () => {
111
+ await closeSupportSession();
112
+ runBotMenuOption(item);
113
+ },
114
+ },
115
+ ],
116
+ type: 'horizontal-button',
117
+ },
118
+ });
119
+ return;
120
+ }
121
+ runBotMenuOption(item);
122
+ },
123
+ [flowState, runBotMenuOption, closeSupportSession]
124
+ );
125
+
126
+ const handleOpenBotMenu = useCallback(() => {
127
+ if (!hasBotMenuOptions) return;
128
+ openBotMenuSheet({
129
+ options: botMenuOptions,
130
+ onSelect: handleSelectBotMenuOption,
131
+ });
132
+ }, [hasBotMenuOptions, botMenuOptions, handleSelectBotMenuOption]);
133
+
134
+ return {
135
+ hasBotMenuOptions,
136
+ handleOpenBotMenu,
137
+ onStartConversationFlow,
138
+ isRunFlowPending: isPending,
139
+ };
140
+ };
@@ -0,0 +1,57 @@
1
+ import { useMemo, useRef } from 'react';
2
+ import { debounce } from 'lodash';
3
+
4
+ import { DEFAULT_PANNEL_ACTIONS } from '../constants';
5
+ import { DChatPannelActionItemType, type DChatPannelAction } from '../types';
6
+
7
+ interface UseComposerPanelActionsProps {
8
+ pannelActions?: DChatPannelAction[];
9
+ addImages?: () => void;
10
+ addFiles?: () => void;
11
+ }
12
+
13
+ // Resolves the attachment panel's actions, wiring Gallery/Documents to
14
+ // debounced pickers unless the consumer supplied its own onPress.
15
+ export const useComposerPanelActions = ({
16
+ pannelActions,
17
+ addImages,
18
+ addFiles,
19
+ }: UseComposerPanelActionsProps) => {
20
+ const addImagesRef = useRef(addImages);
21
+ addImagesRef.current = addImages;
22
+ const debouncedImagePickerRef = useRef(
23
+ debounce(() => addImagesRef.current?.(), 300, {
24
+ leading: true,
25
+ trailing: false,
26
+ })
27
+ );
28
+
29
+ const addFilesRef = useRef(addFiles);
30
+ addFilesRef.current = addFiles;
31
+ const debouncedFilePickerRef = useRef(
32
+ debounce(() => addFilesRef.current?.(), 300, {
33
+ leading: true,
34
+ trailing: false,
35
+ })
36
+ );
37
+
38
+ return useMemo(() => {
39
+ return (pannelActions || DEFAULT_PANNEL_ACTIONS)?.map((p) => {
40
+ let onPress;
41
+ switch (p.id) {
42
+ case DChatPannelActionItemType.Gallery:
43
+ onPress = debouncedImagePickerRef.current;
44
+ break;
45
+ case DChatPannelActionItemType.Documents:
46
+ onPress = debouncedFilePickerRef.current;
47
+ break;
48
+ default:
49
+ break;
50
+ }
51
+ return {
52
+ ...p,
53
+ onPress: p.onPress || onPress,
54
+ };
55
+ });
56
+ }, [pannelActions]);
57
+ };