@droppii-org/chat-mobile 0.2.107 → 0.2.109

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