@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
@@ -1,79 +1,32 @@
1
- import {
2
- memo,
3
- useCallback,
4
- useEffect,
5
- useMemo,
6
- useRef,
7
- type ReactNode,
8
- type RefObject,
9
- } from 'react';
10
- import {
11
- Keyboard,
12
- Platform,
13
- StyleSheet,
14
- type LayoutChangeEvent,
15
- type View,
16
- } from 'react-native';
17
- import { debounce } from 'lodash';
18
- import Reanimated from 'react-native-reanimated';
1
+ import { memo, useCallback, useMemo, type RefObject } from 'react';
2
+ import type { LayoutChangeEvent, View } from 'react-native';
19
3
  import {
20
4
  KeyboardStickyView,
21
5
  useKeyboardState,
22
6
  } from 'react-native-keyboard-controller';
23
7
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
24
- import { KContainer, KColors, KImage, KButton, KLabel } from '@droppii/libs';
25
-
26
- const IS_IOS = Platform.OS === 'ios';
27
- const INPUT_LINE_HEIGHT = 22.4;
28
- const WRAPPED_HEIGHT_THRESHOLD = IS_IOS
29
- ? INPUT_LINE_HEIGHT + 4
30
- : INPUT_LINE_HEIGHT + 12;
31
-
32
- import { ChatQuickActions } from './ChatQuickActions';
33
- import { ChatAttachmentPanel } from './ChatAttachmentPanel';
34
- import { ChatLinkPreview } from './ChatLinkPreview';
35
- import { ChatMessageInput } from './components/ChatMessageInput';
36
- import { AttachmentPreview } from '../../components/AttachmentPreview';
37
- import { StickerPicker } from '../../components/StickerPicker';
38
- import type { StickerItem } from '../../types/sticker';
8
+ import { PeerType } from '@droppii/openim-rn-client-sdk';
39
9
 
40
- import {
41
- DEFAULT_QUICK_ACTIONS,
42
- getAttachmentPanelHeight,
43
- DEFAULT_PANNEL_ACTIONS,
44
- } from './constants';
45
- import { STICKER_PICKER_HEIGHT } from '../../components/StickerPicker/constants';
46
- import { DChatPannelActionItemType, type DChatPannelAction } from './types';
10
+ import type { ComposerPanelController, DChatPannelAction } from './types';
47
11
  import { useChatComposerState } from './hooks/useChatComposerState';
48
- import { useChatComposerAnimation } from './hooks/useChatComposerAnimation';
49
- import { useChatDetailContext } from '../../context/ChatDetailContext';
50
- import { trans } from '../../translation';
12
+ import { useComposerPanels } from './hooks/useComposerPanels';
13
+ import { useComposerSend } from './hooks/useComposerSend';
14
+ import { useComposerBotMenu } from './hooks/useComposerBotMenu';
15
+ import { useComposerPanelActions } from './hooks/useComposerPanelActions';
16
+ import { useMentionComposer } from './hooks/useMentionComposer';
51
17
  import {
52
- useCloseSupportSession,
53
- useResetFlow,
54
- useRunFlow,
55
- } from '../../hooks/flows/useFlow';
56
- import { ReactFlowsNodeTypes, type InputButtonItem } from '../../types/flows';
57
- import { DEFAULT_BUTTON_ID, isContextButton } from '../../utils/flows';
58
- import { DFlowState } from '../../types/chat';
59
- import { useAttachmentMapping } from './hooks/useAttachmentMapping';
60
- import type { AttachmentType } from '../../types/attachment';
18
+ ChatComposerStatusBanner,
19
+ getComposerStatusVariant,
20
+ } from './components/ChatComposerStatusBanner';
21
+ import { ChatComposerInputArea } from './components/ChatComposerInputArea';
22
+ import { ChatComposerAccessorySlot } from './components/ChatComposerAccessorySlot';
23
+ import { useChatDetailContext } from '../../context/ChatDetailContext';
24
+ import { ReactFlowsNodeTypes } from '../../types/flows';
61
25
  import { useChatCompose } from '../../hooks/useChatCompose';
62
26
  import { useInputStateBroadcast } from '../../hooks/useInputStateBroadcast';
63
- import { ChatCurrentQuotedMessage } from './ChatCurrentQuotedMessage';
64
- import { useSendMessage } from '../../hooks/message/useSendMessage';
65
- import { openBotMenuSheet } from './BotMenuSheet';
66
- import { UIUtils } from '../../utils/UIUtils';
67
- import { PeerType } from '@droppii/openim-rn-client-sdk';
68
27
  import { useCanSendMessage } from '../../hooks/useGroupPermission';
69
28
  import { useIsJoinedGroup } from '../../hooks/useIsJoinedGroup';
70
- import { useMentionComposer } from './hooks/useMentionComposer';
71
- import { MentionSuggestionList } from './components/MentionSuggestionList';
72
29
  import { createOpenIMMentionSearchSource } from '../../services/mentionSearch';
73
- import {
74
- serializeMentionsForSend,
75
- MAX_AT_USER_COUNT,
76
- } from '../../utils/mentionSerializer';
77
30
 
78
31
  interface ChatComposerInternalProps {
79
32
  isEmptyMessage?: boolean;
@@ -83,6 +36,10 @@ interface ChatComposerInternalProps {
83
36
  onComposerLayout?: (event: LayoutChangeEvent) => void;
84
37
  onPressJoinGroup?: () => void;
85
38
  isJoiningGroup?: boolean;
39
+ /** @internal Filled with the panel controller for tap-outside dismiss. */
40
+ panelControllerRef?: RefObject<ComposerPanelController | null>;
41
+ /** @internal Reports whether the attachment/sticker panel is open. */
42
+ onPanelOpenChange?: (open: boolean) => void;
86
43
  }
87
44
 
88
45
  export const ChatComposer = memo(
@@ -94,23 +51,16 @@ export const ChatComposer = memo(
94
51
  onComposerLayout,
95
52
  onPressJoinGroup,
96
53
  isJoiningGroup = false,
54
+ panelControllerRef,
55
+ onPanelOpenChange,
97
56
  }: ChatComposerInternalProps) => {
98
57
  const insets = useSafeAreaInsets();
99
58
  const isDisableCompose = isComposeDisabled;
100
59
  const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
101
60
 
102
- const {
103
- isBotConversation,
104
- conversation,
105
- conversationFlow,
106
- conversationFlowMainMenu,
107
- flowState,
108
- } = useChatDetailContext();
61
+ const { isBotConversation, conversation, conversationFlow } =
62
+ useChatDetailContext();
109
63
  const isGroupConversation = conversation?.peerType === PeerType.Group;
110
- const { mutate: runFlow, isPending } = useRunFlow();
111
- const { mutate: resetFlow } = useResetFlow();
112
- const { mutateAsync: closeSupportSession } = useCloseSupportSession();
113
- const { sendMessage } = useSendMessage();
114
64
  const compose = useChatCompose();
115
65
  const { notifyTyping, notifyStoppedTyping } = useInputStateBroadcast(
116
66
  conversation?.conversationID
@@ -131,8 +81,8 @@ export const ChatComposer = memo(
131
81
  setIsAttachmentOpen,
132
82
  isInputFocused,
133
83
  setIsInputFocused,
134
- setIsInputWrapped,
135
84
  isMultilineInput,
85
+ handleInputContentSizeChange,
136
86
  inputRef,
137
87
  } = useChatComposerState(compose.text);
138
88
 
@@ -158,176 +108,72 @@ export const ChatComposer = memo(
158
108
  inputRef,
159
109
  });
160
110
 
161
- const attachmentPanelHeight = useMemo(
162
- () => getAttachmentPanelHeight(DEFAULT_PANNEL_ACTIONS.length),
163
- []
164
- );
111
+ const isNewBotConversation =
112
+ isBotConversation &&
113
+ conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
114
+ isEmptyMessage;
165
115
 
166
- const panelHeight =
167
- activePanelType === 'sticker'
168
- ? STICKER_PICKER_HEIGHT
169
- : attachmentPanelHeight;
116
+ const statusVariant = getComposerStatusVariant({
117
+ isDisableCompose,
118
+ isGroupConversation,
119
+ isNewBotConversation,
120
+ isNotInGroup,
121
+ isSendMessageAllowed,
122
+ });
170
123
 
171
124
  const {
172
125
  accessorySlotStyle,
173
- toggleAttachmentVisibility,
174
- openWithHeight,
175
- closeAttachment,
176
- } = useChatComposerAnimation({
177
- panelHeight,
178
- onAttachmentOpenChange: setIsAttachmentOpen,
126
+ closeActivePanel,
127
+ yieldingCloseMode,
128
+ handleToggleAttachment,
129
+ handleToggleSticker,
130
+ } = useComposerPanels({
131
+ activePanelType,
132
+ setActivePanelType,
133
+ isAttachmentOpen,
134
+ setIsAttachmentOpen,
135
+ isMentionPanelOpen: mentionComposer.isPanelOpen,
136
+ closeMentionPanel: mentionComposer.closeMentionPanel,
137
+ isKeyboardVisible,
138
+ isSlotVisible:
139
+ !isDisableCompose && !isNewBotConversation && statusVariant === null,
140
+ panelControllerRef,
141
+ onPanelOpenChange,
179
142
  });
180
143
 
181
- // Mutual exclusion (finding #5 in the mention plan): the mention picker
182
- // never renders through the attachment/sticker panel slot, but opening
183
- // one must still close the other — starting a mention query while the
184
- // attachment/sticker panel is open closes that panel.
185
- useEffect(() => {
186
- if (mentionComposer.isPanelOpen && isAttachmentOpen) {
187
- const immediate = activePanelType === 'sticker';
188
- closeAttachment(!immediate, immediate);
189
- setActivePanelType(null);
190
- }
191
- // eslint-disable-next-line react-hooks/exhaustive-deps
192
- }, [mentionComposer.isPanelOpen]);
193
-
194
- const handleSend = useCallback(async () => {
195
- if (!isSendMessageAllowed) {
196
- UIUtils.toast.open({
197
- title: sendDeniedReason,
198
- theme: 'info',
199
- });
200
- return;
201
- }
202
-
203
- if (compose.isUploading || !compose.canSend) return;
204
- const mentionsToSend = mentionComposer.mentions;
205
-
206
- // OpenIM's Go core hard-rejects an AtTextMessage tagging more than
207
- // MAX_AT_USER_COUNT users, so serializeMentionsForSend caps
208
- // atUserIDList to the first MAX_AT_USER_COUNT (mentionSerializer.ts's
209
- // doc comment) — the message still sends, with every mention still
210
- // highlighted in the text, just only the first MAX_AT_USER_COUNT
211
- // actually get natively tagged/notified. This is a heads-up, not a
212
- // block, so no `return` here.
213
- if (mentionsToSend.length > 0) {
214
- const { truncated } = serializeMentionsForSend(mentionsToSend);
215
- if (truncated) {
216
- UIUtils.toast.open({
217
- title: trans('group:tag_limit_exceeded', {
218
- max: String(MAX_AT_USER_COUNT),
219
- }),
220
- theme: 'info',
221
- });
222
- }
223
- }
224
-
225
- // Clear mention state synchronously, in the same tick as
226
- // compose.send() (which clears `text`) — otherwise there's a real
227
- // transient render where `value` is already '' but `mentions` still
228
- // holds the just-sent entries, and the composer can visibly fail to
229
- // clear (ChatMessageInput stays in children-controlled mode with a
230
- // "ghost" mention anchored past the end of an empty string). See
231
- // useMentionComposer.clear()'s doc comment.
232
- mentionComposer.clear();
233
- // Stop before awaiting the send: the peer should lose the indicator as
234
- // the message goes out, not once it round-trips.
235
- notifyStoppedTyping();
236
- try {
237
- await compose.send(mentionsToSend);
238
- } catch (error) {
239
- if (__DEV__) console.error('[ChatComposer] Failed to send:', error);
240
- } finally {
241
- // Belt-and-suspenders: force the native view to actually clear,
242
- // regardless of whether the value/children mode-switch (triggered
243
- // by mentions going from non-empty to empty) synced correctly on
244
- // its own — the underlying nested-Text-in-TextInput technique is
245
- // known to have platform quirks around this kind of transition.
246
- inputRef.current?.clear();
247
- }
248
- // Depend on mentionComposer.mentions/.clear specifically (both
249
- // stable-per-value / [] deps), not the whole mentionComposer object —
250
- // same reasoning as handleToggleAttachment above.
251
- // eslint-disable-next-line react-hooks/exhaustive-deps
252
- }, [
144
+ const handleSend = useComposerSend({
253
145
  compose,
254
146
  isSendMessageAllowed,
255
147
  sendDeniedReason,
256
- mentionComposer.mentions,
257
- mentionComposer.clear,
148
+ mentions: mentionComposer.mentions,
149
+ clearMentions: mentionComposer.clear,
258
150
  notifyStoppedTyping,
259
151
  inputRef,
260
- ]);
152
+ });
153
+
154
+ const {
155
+ hasBotMenuOptions,
156
+ handleOpenBotMenu,
157
+ onStartConversationFlow,
158
+ isRunFlowPending,
159
+ } = useComposerBotMenu();
160
+
161
+ const resolvedPannelActons = useComposerPanelActions({
162
+ pannelActions,
163
+ addImages: compose.addImages,
164
+ addFiles: compose.addFiles,
165
+ });
261
166
 
262
167
  const handleInputFocus = useCallback(() => {
263
168
  setIsInputFocused(true);
264
169
  if (isAttachmentOpen) {
265
- // Use immediate close (no animation) when sticker panel is open to avoid
266
- // layout conflict between the panel closing and keyboard appearing.
267
- // Attachment panel is small enough that animation is fine (speedUp=true).
268
- const immediate = activePanelType === 'sticker';
269
- closeAttachment(!immediate, immediate);
270
- setActivePanelType(null);
170
+ closeActivePanel(yieldingCloseMode);
271
171
  }
272
172
  }, [
273
- closeAttachment,
173
+ closeActivePanel,
274
174
  isAttachmentOpen,
275
- activePanelType,
175
+ yieldingCloseMode,
276
176
  setIsInputFocused,
277
- setActivePanelType,
278
- ]);
279
-
280
- const handleToggleAttachment = useCallback(() => {
281
- mentionComposer.closeMentionPanel();
282
- if (activePanelType === 'sticker') {
283
- // switch from sticker to attachment
284
- setActivePanelType('attachment');
285
- openWithHeight(attachmentPanelHeight);
286
- return;
287
- }
288
- setActivePanelType(isAttachmentOpen ? null : 'attachment');
289
- toggleAttachmentVisibility();
290
- // Deliberately depend on `mentionComposer.closeMentionPanel` (stable —
291
- // [] deps in useMentionComposer), NOT the whole `mentionComposer`
292
- // object, which is a fresh literal every render (useMentionComposer
293
- // doesn't memoize its return value). Depending on the whole object
294
- // recreated this callback — and therefore every prop built from it,
295
- // e.g. ChatMessageInput's onEmojiPress — on every single render,
296
- // which contributed to a real "Maximum update depth exceeded" loop
297
- // found in testing. The lint rule can't prove member-access stability
298
- // on its own, hence the disable.
299
- // eslint-disable-next-line react-hooks/exhaustive-deps
300
- }, [
301
- activePanelType,
302
- isAttachmentOpen,
303
- attachmentPanelHeight,
304
- openWithHeight,
305
- toggleAttachmentVisibility,
306
- setActivePanelType,
307
- mentionComposer.closeMentionPanel,
308
- ]);
309
-
310
- const handleToggleSticker = useCallback(() => {
311
- mentionComposer.closeMentionPanel();
312
- if (activePanelType === 'sticker') {
313
- closeAttachment();
314
- setActivePanelType(null);
315
- return;
316
- }
317
- if (isKeyboardVisible) {
318
- Keyboard.dismiss();
319
- }
320
- setActivePanelType('sticker');
321
- openWithHeight(STICKER_PICKER_HEIGHT);
322
- // Same reasoning as handleToggleAttachment above — see that comment.
323
- // eslint-disable-next-line react-hooks/exhaustive-deps
324
- }, [
325
- activePanelType,
326
- isKeyboardVisible,
327
- mentionComposer.closeMentionPanel,
328
- closeAttachment,
329
- openWithHeight,
330
- setActivePanelType,
331
177
  ]);
332
178
 
333
179
  const handleInputBlur = useCallback(() => {
@@ -346,380 +192,35 @@ export const ChatComposer = memo(
346
192
  [notifyTyping, mentionComposer]
347
193
  );
348
194
 
349
- const handleInputContentSizeChange = useCallback(
350
- (event: { nativeEvent: { contentSize: { height: number } } }) => {
351
- const { height } = event.nativeEvent.contentSize;
352
- setIsInputWrapped(height > WRAPPED_HEIGHT_THRESHOLD);
353
- },
354
- [setIsInputWrapped]
355
- );
356
-
357
- const onStartConversationFlow = useCallback(() => {
358
- runFlow({
359
- groupId: conversationFlow?.flowCurrentStep?.id || '',
360
- actionId: null,
361
- childId: null,
362
- plainText: trans('start'),
363
- });
364
- }, [runFlow, conversationFlow]);
365
-
366
- const botMenuOptions = useMemo(() => {
367
- const apiOptions =
368
- conversationFlowMainMenu?.filter(
369
- (item) => !isContextButton(item.label)
370
- ) || [];
371
-
372
- const hasDefaultButton = apiOptions.some(
373
- (item) => item.id === DEFAULT_BUTTON_ID
374
- );
375
-
376
- if (hasDefaultButton) return apiOptions;
377
-
378
- return [
379
- ...apiOptions,
380
- { id: DEFAULT_BUTTON_ID, label: trans('restart') },
381
- ];
382
- }, [conversationFlowMainMenu]);
383
-
384
- const hasBotMenuOptions = isBotConversation && botMenuOptions.length > 0;
385
-
386
- const runBotMenuOption = useCallback(
387
- (item: InputButtonItem) => {
388
- if (item.id === DEFAULT_BUTTON_ID) {
389
- resetFlow();
390
- return;
391
- }
392
- runFlow({
393
- actionId: item.id,
394
- groupId: '',
395
- childId: '',
396
- plainText: item.label,
397
- });
398
- },
399
- [runFlow, resetFlow]
400
- );
401
-
402
- const handleSelectBotMenuOption = useCallback(
403
- (item: InputButtonItem) => {
404
- if (flowState === DFlowState.CS) {
405
- UIUtils.popup.open({
406
- header: {
407
- title: { text: trans('chat:end_support_session_title') },
408
- alignment: 'center',
409
- },
410
- body: {
411
- renderContent: () => (
412
- <KContainer.View padding={'1rem'}>
413
- <KLabel.RichText
414
- typo="TextMdNormal"
415
- textAlign="center"
416
- color={KColors.gray.dark}
417
- richTextOptions={{
418
- i18nKey: trans('chat:end_support_session_desc'),
419
- components: {
420
- bold: (
421
- <KLabel.Text
422
- typo="TextMdBold"
423
- color={KColors.gray.dark}
424
- />
425
- ),
426
- },
427
- }}
428
- />
429
- </KContainer.View>
430
- ),
431
- },
432
- actions: {
433
- buttons: [
434
- {
435
- label: trans('chat:end_support_session_secondary'),
436
- kind: 'light',
437
- onPress: () => {},
438
- },
439
- {
440
- label: trans('chat:end_support_session_primary'),
441
- kind: 'danger',
442
- onPress: async () => {
443
- await closeSupportSession();
444
- runBotMenuOption(item);
445
- },
446
- },
447
- ],
448
- type: 'horizontal-button',
449
- },
450
- });
451
- return;
452
- }
453
- runBotMenuOption(item);
454
- },
455
- [flowState, runBotMenuOption, closeSupportSession]
456
- );
457
-
458
- const handleOpenBotMenu = useCallback(() => {
459
- if (!hasBotMenuOptions) return;
460
- openBotMenuSheet({
461
- options: botMenuOptions,
462
- onSelect: handleSelectBotMenuOption,
463
- });
464
- }, [hasBotMenuOptions, botMenuOptions, handleSelectBotMenuOption]);
465
-
466
- useEffect(() => {
467
- if (!compose.text) setIsInputWrapped(false);
468
- }, [compose.text, setIsInputWrapped]);
469
-
470
- const addImagesRef = useRef(compose.addImages);
471
- addImagesRef.current = compose.addImages;
472
- const debouncedImagePickerRef = useRef(
473
- debounce(() => addImagesRef.current?.(), 300, {
474
- leading: true,
475
- trailing: false,
476
- })
477
- );
478
-
479
- const addFilesRef = useRef(compose.addFiles);
480
- addFilesRef.current = compose.addFiles;
481
- const debouncedFilePickerRef = useRef(
482
- debounce(() => addFilesRef.current?.(), 300, {
483
- leading: true,
484
- trailing: false,
485
- })
195
+ const content = statusVariant ? (
196
+ <ChatComposerStatusBanner
197
+ variant={statusVariant}
198
+ isRunFlowPending={isRunFlowPending}
199
+ onStartConversationFlow={onStartConversationFlow}
200
+ isJoiningGroup={isJoiningGroup}
201
+ onPressJoinGroup={onPressJoinGroup}
202
+ sendDeniedReason={sendDeniedReason}
203
+ />
204
+ ) : (
205
+ <ChatComposerInputArea
206
+ compose={compose}
207
+ mentionComposer={mentionComposer}
208
+ activePanelType={activePanelType}
209
+ isAttachmentOpen={isAttachmentOpen}
210
+ isInputFocused={isInputFocused}
211
+ isMultilineInput={isMultilineInput}
212
+ inputRef={inputRef}
213
+ onToggleAttachment={handleToggleAttachment}
214
+ onToggleSticker={handleToggleSticker}
215
+ onChangeText={handleChangeText}
216
+ onInputFocus={handleInputFocus}
217
+ onInputBlur={handleInputBlur}
218
+ onInputContentSizeChange={handleInputContentSizeChange}
219
+ onOpenBotMenu={hasBotMenuOptions ? handleOpenBotMenu : undefined}
220
+ onSend={handleSend}
221
+ />
486
222
  );
487
223
 
488
- const resolvedPannelActons = useMemo(() => {
489
- return (pannelActions || DEFAULT_PANNEL_ACTIONS)?.map((p) => {
490
- let onPress;
491
- switch (p.id) {
492
- case DChatPannelActionItemType.Gallery:
493
- onPress = debouncedImagePickerRef.current;
494
- break;
495
- case DChatPannelActionItemType.Documents:
496
- onPress = debouncedFilePickerRef.current;
497
- break;
498
- default:
499
- break;
500
- }
501
- return {
502
- ...p,
503
- onPress: p.onPress || onPress,
504
- };
505
- });
506
- }, [pannelActions]);
507
-
508
- const inputRowStyle = useMemo(
509
- () => ({ paddingBottom: isAttachmentOpen ? 8 : insets.bottom + 12 }),
510
- [insets.bottom, isAttachmentOpen]
511
- );
512
-
513
- const handleStickerSelect = useCallback(
514
- (sticker: StickerItem) => {
515
- sendMessage({ sticker });
516
- },
517
- [sendMessage]
518
- );
519
-
520
- const attachments = useAttachmentMapping({
521
- selectedImages: compose.selectedImages,
522
- selectedFiles: compose.selectedFiles,
523
- });
524
-
525
- const onRemove = useCallback(
526
- (type: AttachmentType, data: unknown) => {
527
- if (type === 'file') return compose.removeFile(data as string);
528
- return compose.removeImage(data as number);
529
- },
530
- [compose]
531
- );
532
-
533
- let content: ReactNode;
534
- const isNewBotConversation =
535
- isBotConversation &&
536
- conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
537
- isEmptyMessage;
538
-
539
- if (isDisableCompose && !isGroupConversation) {
540
- content = (
541
- <KContainer.View row alignItems justifyContent>
542
- <KContainer.View marginR="0.25rem">
543
- <KImage.VectorIcons
544
- name="info-circle-o"
545
- size={16}
546
- color={KColors.palette.gray.w600}
547
- />
548
- </KContainer.View>
549
- <KLabel.Text typo="TextXsNormal">
550
- {trans('chat:compose_disabled_seller_inactive')}
551
- </KLabel.Text>
552
- </KContainer.View>
553
- );
554
- } else if (isNewBotConversation) {
555
- content = (
556
- <KContainer.View padding={'1.5rem'} paddingB={0}>
557
- <KButton.Solid
558
- size="md"
559
- label={trans('start')}
560
- stretch
561
- icon={{ vectorName: 'chat-square-o', size: 20 }}
562
- loading={isPending}
563
- onPress={onStartConversationFlow}
564
- />
565
- </KContainer.View>
566
- );
567
- } else if (isNotInGroup) {
568
- content = (
569
- <KContainer.View
570
- paddingH="0.75rem"
571
- justifyContent="center"
572
- brTW={1}
573
- brTC={KColors.border.light}
574
- >
575
- <KLabel.Text
576
- typo="TextSmNormal"
577
- textAlign="center"
578
- paddingV={'0.75rem'}
579
- >
580
- {trans('group:not_in_group')}
581
- </KLabel.Text>
582
- <KButton.Solid
583
- size="md"
584
- label={trans('group:join')}
585
- stretch
586
- loading={isJoiningGroup}
587
- onPress={onPressJoinGroup}
588
- />
589
- </KContainer.View>
590
- );
591
- } else if (!isSendMessageAllowed) {
592
- content = (
593
- <KContainer.View row alignItems justifyContent paddingT={'1rem'}>
594
- <KContainer.View marginR="0.25rem">
595
- <KImage.VectorIcons
596
- name="info-circle-o"
597
- size={16}
598
- color={KColors.palette.gray.w600}
599
- />
600
- </KContainer.View>
601
- <KLabel.Text typo="TextXsNormal">{sendDeniedReason}</KLabel.Text>
602
- </KContainer.View>
603
- );
604
- } else {
605
- content = (
606
- <KContainer.View background={'#F9F9FA'}>
607
- <ChatCurrentQuotedMessage />
608
- <KContainer.VisibleView
609
- visible={conversation?.peerType === PeerType.Bot}
610
- >
611
- <ChatQuickActions
612
- visible
613
- actions={DEFAULT_QUICK_ACTIONS}
614
- onActionPress={(action) =>
615
- console.log('[ChatComposer] quick action:', action.id)
616
- }
617
- />
618
- </KContainer.VisibleView>
619
- {compose.showLinkPreview && (
620
- <ChatLinkPreview
621
- metadata={compose.urlMetadata}
622
- isLoading={compose.isMetadataLoading}
623
- onDismiss={compose.dismissPreview}
624
- />
625
- )}
626
-
627
- <AttachmentPreview
628
- visible={
629
- compose.isImagePreviewVisible || compose.isFilePreviewVisible
630
- }
631
- attachments={attachments}
632
- onRemove={onRemove}
633
- />
634
-
635
- <KContainer.View style={styles.inputRowWrapper}>
636
- <KContainer.VisibleView visible={mentionComposer.isPanelOpen}>
637
- <KContainer.View style={styles.mentionPanel}>
638
- <MentionSuggestionList
639
- suggestions={mentionComposer.suggestions}
640
- isSearching={mentionComposer.isSearching}
641
- isLoadingMore={mentionComposer.isLoadingMore}
642
- hasMore={mentionComposer.hasMore}
643
- onLoadMore={mentionComposer.loadMore}
644
- onSelect={mentionComposer.selectSuggestion}
645
- />
646
- </KContainer.View>
647
- </KContainer.VisibleView>
648
-
649
- <KContainer.View
650
- row
651
- alignItems
652
- paddingH="0.75rem"
653
- paddingT="0.75rem"
654
- paddingB="0.75rem"
655
- style={inputRowStyle}
656
- >
657
- <KContainer.Touchable
658
- onPress={handleToggleAttachment}
659
- center
660
- br="round"
661
- >
662
- <KImage.VectorIcons
663
- name={
664
- activePanelType === 'attachment'
665
- ? 'close-circle-b'
666
- : 'plus-circle-o'
667
- }
668
- size={24}
669
- color={
670
- activePanelType === 'attachment'
671
- ? KColors.palette.primary.w400
672
- : KColors.palette.gray.w600
673
- }
674
- />
675
- </KContainer.Touchable>
676
-
677
- <ChatMessageInput
678
- value={compose.text}
679
- placeholder="Nhập tin nhắn..."
680
- onChangeText={handleChangeText}
681
- onFocus={handleInputFocus}
682
- onBlur={handleInputBlur}
683
- onContentSizeChange={handleInputContentSizeChange}
684
- onSelectionChange={mentionComposer.handleSelectionChange}
685
- mentions={mentionComposer.mentions}
686
- isInputFocused={isInputFocused}
687
- isMultilineInput={isMultilineInput}
688
- inputRef={inputRef}
689
- onEmojiPress={handleToggleSticker}
690
- onSuggestPress={
691
- hasBotMenuOptions ? handleOpenBotMenu : undefined
692
- }
693
- activePanelType={activePanelType}
694
- />
695
-
696
- <KContainer.Touchable
697
- onPress={compose.isUploading ? undefined : handleSend}
698
- pointerEvents={compose.isUploading ? 'none' : 'auto'}
699
- width={40}
700
- height={40}
701
- center
702
- br="round"
703
- background={
704
- compose.isUploading
705
- ? KColors.palette.primary.w100
706
- : KColors.palette.primary.w400
707
- }
708
- marginL="0.75rem"
709
- opacity={compose.isUploading ? 0.25 : 1}
710
- >
711
- <KImage.VectorIcons
712
- name="send-b"
713
- size={20}
714
- color={KColors.white}
715
- />
716
- </KContainer.Touchable>
717
- </KContainer.View>
718
- </KContainer.View>
719
- </KContainer.View>
720
- );
721
- }
722
-
723
224
  return (
724
225
  <>
725
226
  <KeyboardStickyView
@@ -729,43 +230,16 @@ export const ChatComposer = memo(
729
230
  >
730
231
  {content}
731
232
  </KeyboardStickyView>
732
- <KContainer.VisibleView
233
+ <ChatComposerAccessorySlot
733
234
  visible={!isDisableCompose && !isNewBotConversation}
734
- >
735
- <Reanimated.View
736
- style={[styles.accessorySlot, accessorySlotStyle]}
737
- pointerEvents={isAttachmentOpen ? 'auto' : 'none'}
738
- >
739
- {activePanelType === 'attachment' && (
740
- <ChatAttachmentPanel actions={resolvedPannelActons} />
741
- )}
742
- {activePanelType === 'sticker' && (
743
- <StickerPicker onSelectSticker={handleStickerSelect} />
744
- )}
745
- </Reanimated.View>
746
- </KContainer.VisibleView>
235
+ accessorySlotStyle={accessorySlotStyle}
236
+ isAttachmentOpen={isAttachmentOpen}
237
+ activePanelType={activePanelType}
238
+ pannelActions={resolvedPannelActons}
239
+ />
747
240
  </>
748
241
  );
749
242
  }
750
243
  );
751
244
 
752
245
  ChatComposer.displayName = 'ChatComposer';
753
-
754
- const styles = StyleSheet.create({
755
- accessorySlot: {
756
- overflow: 'hidden',
757
- backgroundColor: KColors.palette.gray.w25,
758
- },
759
- inputRowWrapper: {
760
- position: 'relative',
761
- },
762
- // Floats above the input row without going through the accessorySlot
763
- // (attachment/sticker) render path — that slot intentionally takes over
764
- // the keyboard area, which the mention picker must not do (finding #5).
765
- mentionPanel: {
766
- position: 'absolute',
767
- left: 0,
768
- right: 0,
769
- bottom: '100%',
770
- },
771
- });