@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
@@ -18,9 +18,13 @@ import {
18
18
  useConversationStore,
19
19
  useUserStore,
20
20
  } from '../../store';
21
+ import { useExpandedMessageStore } from '../../store/expandedMessage';
22
+ import { clearExpandableTextCache } from '../../hooks/useExpandableText';
21
23
  import ChatDetailHeader from './ChatDetailHeader';
22
24
  import { ChatListLegend } from './ChatListLegend';
23
25
  import { ChatComposer } from './ChatComposer';
26
+ import { useComposerPanelDismiss } from './hooks/useComposerPanelDismiss';
27
+ import { TimelinePressGuardProvider } from '../../context/TimelinePressGuardContext';
24
28
  import { shouldShowAddMember } from './conversationHeader.utils';
25
29
  import type { ChatDetailProps } from './types';
26
30
  import { normalizeUrl } from '../../utils/url';
@@ -81,6 +85,15 @@ const ChatDetail = memo(
81
85
 
82
86
  const conversation = useConversation(conversationId);
83
87
 
88
+ // Keyed on the conversation, not on unmount: this screen is mounted by the
89
+ // consumer's navigator, so whether it remounts per conversation or is
90
+ // reused with a new id is not knowable here. Reopening a conversation must
91
+ // start every message shortened again.
92
+ useEffect(() => {
93
+ useExpandedMessageStore.getState().clearSurface('timeline');
94
+ clearExpandableTextCache();
95
+ }, [conversationId]);
96
+
84
97
  const { data: target } = useGetConversationTarget(conversationId);
85
98
  const isComposeDisabled = !!target && !target.isActive;
86
99
 
@@ -180,6 +193,15 @@ const ChatDetail = memo(
180
193
  [onPressUrl]
181
194
  );
182
195
 
196
+ const {
197
+ panelControllerRef,
198
+ isComposerPanelOpen,
199
+ onComposerPanelOpenChange,
200
+ dismissAreaTouchProps,
201
+ notifyScrollBeginDrag,
202
+ shouldSwallowPress,
203
+ } = useComposerPanelDismiss();
204
+
183
205
  const handleMessageLongPress = useCallback<MessageToolboxShowFn>(
184
206
  (message, rect, groupPosition) => {
185
207
  messageToolboxRef.current?.show(message, rect, groupPosition);
@@ -238,26 +260,31 @@ const ChatDetail = memo(
238
260
  onPress={handlePressPinnedMessageBar}
239
261
  />
240
262
 
241
- <ChatListLegend
242
- messages={messages}
243
- conversationId={conversationId}
244
- customMessageItemFactory={customMessageItemFactory}
245
- isLoading={initLoading}
246
- onLoadMoreOld={getMoreOldMessages}
247
- hasMoreOld={hasMoreOld}
248
- moreOldLoading={moreOldLoading}
249
- onLoadMoreNew={getMoreNewMessages}
250
- hasMoreNew={hasMoreNew}
251
- moreNewLoading={moreNewLoading}
252
- onMediaPress={handleMediaPress}
253
- onPressUrl={handlePressUrl}
254
- onLongPress={handleMessageLongPress}
255
- listRef={listRef}
256
- freeze={freeze}
257
- scrollMessageToEnd={scrollMessageToEnd}
258
- keyboardOffset={insets.bottom}
259
- renderConversationViewHeader={renderConversationViewHeader}
260
- />
263
+ <TimelinePressGuardProvider value={shouldSwallowPress}>
264
+ <ChatListLegend
265
+ messages={messages}
266
+ conversationId={conversationId}
267
+ customMessageItemFactory={customMessageItemFactory}
268
+ isLoading={initLoading}
269
+ onLoadMoreOld={getMoreOldMessages}
270
+ hasMoreOld={hasMoreOld}
271
+ moreOldLoading={moreOldLoading}
272
+ onLoadMoreNew={getMoreNewMessages}
273
+ hasMoreNew={hasMoreNew}
274
+ moreNewLoading={moreNewLoading}
275
+ onMediaPress={handleMediaPress}
276
+ onPressUrl={handlePressUrl}
277
+ onLongPress={handleMessageLongPress}
278
+ listRef={listRef}
279
+ freeze={freeze}
280
+ scrollMessageToEnd={scrollMessageToEnd}
281
+ keyboardOffset={insets.bottom}
282
+ renderConversationViewHeader={renderConversationViewHeader}
283
+ dismissAreaTouchProps={dismissAreaTouchProps}
284
+ isComposerPanelOpen={isComposerPanelOpen}
285
+ onDismissAreaScrollBeginDrag={notifyScrollBeginDrag}
286
+ />
287
+ </TimelinePressGuardProvider>
261
288
 
262
289
  <ChatComposer
263
290
  isEmptyMessage={messages?.length === 0}
@@ -265,6 +292,8 @@ const ChatDetail = memo(
265
292
  isComposeDisabled={isComposeDisabled}
266
293
  onPressJoinGroup={handlePressJoinGroup}
267
294
  isJoiningGroup={isJoiningGroup}
295
+ panelControllerRef={panelControllerRef}
296
+ onPanelOpenChange={onComposerPanelOpenChange}
268
297
  />
269
298
 
270
299
  <MediaViewerModal ref={mediaViewerRef} />
@@ -69,6 +69,11 @@ import ChatImageDimens, {
69
69
  CHAT_IMAGE_VIEW_SOURCE,
70
70
  } from '../../utils/chatImageDimens';
71
71
  import { ChatTypingIndicator } from './ChatTypingIndicator';
72
+ import type { DismissAreaTouchProps } from './hooks/useComposerPanelDismiss';
73
+ import { useGuardedPress } from '../../hooks/useGuardedPress';
74
+ import { useDismissKeyboardFirst } from '../../hooks/useDismissKeyboardFirst';
75
+ import { useTimelinePressGuard } from '../../context/TimelinePressGuardContext';
76
+ import { perfMark } from '../../utils/perf-marker';
72
77
 
73
78
  // Cluster spacing: a wide gap after the message that closes a cluster, a tight
74
79
  // one between messages inside it. These override the flat margin on the row
@@ -135,6 +140,12 @@ interface ChatListLegendInternalProps extends ChatListProps {
135
140
  closeKeyboard: boolean;
136
141
  }) => Promise<void>;
137
142
  keyboardOffset: number;
143
+ // Tap-outside dismiss of the composer panel (useComposerPanelDismiss).
144
+ // Internal — not part of the public ChatListProps.
145
+ dismissAreaTouchProps?: DismissAreaTouchProps;
146
+ // While true the content is non-hittable: taps only dismiss the panel.
147
+ isComposerPanelOpen?: boolean;
148
+ onDismissAreaScrollBeginDrag?: () => void;
138
149
  }
139
150
 
140
151
  export const ChatListLegend = memo(
@@ -147,8 +158,8 @@ export const ChatListLegend = memo(
147
158
  hasMoreOld,
148
159
  onLoadMoreNew,
149
160
  hasMoreNew,
150
- onMediaPress,
151
- onPressUrl,
161
+ onMediaPress: onMediaPressProp,
162
+ onPressUrl: onPressUrlProp,
152
163
  onLongPress,
153
164
  listRef,
154
165
  freeze,
@@ -156,6 +167,9 @@ export const ChatListLegend = memo(
156
167
  keyboardOffset,
157
168
  isLoading,
158
169
  renderConversationViewHeader,
170
+ dismissAreaTouchProps,
171
+ isComposerPanelOpen = false,
172
+ onDismissAreaScrollBeginDrag,
159
173
  }: ChatListLegendInternalProps) => {
160
174
  const currentUserId = useUserStore((s) => s.user?.userID);
161
175
  const windowWidth = useWindowDimensions().width;
@@ -177,7 +191,19 @@ export const ChatListLegend = memo(
177
191
  [messages, isBotConversation, currentUserId]
178
192
  );
179
193
 
180
- const { retryMessage } = useSendMessage();
194
+ // True for presses of a touch that began with the panel open — covers the
195
+ // render gap before `isComposerPanelOpen` reaches the list.
196
+ const shouldSwallowPress = useTimelinePressGuard();
197
+ const { retryMessage: retryMessageRaw } = useSendMessage();
198
+ const retryMessage = useGuardedPress(retryMessageRaw, shouldSwallowPress);
199
+ const onMediaPress = useGuardedPress(
200
+ useDismissKeyboardFirst(onMediaPressProp),
201
+ shouldSwallowPress
202
+ );
203
+ const onPressUrl = useGuardedPress(
204
+ useDismissKeyboardFirst(onPressUrlProp),
205
+ shouldSwallowPress
206
+ );
181
207
 
182
208
  const lastMessageId = useMemo(() => {
183
209
  const last = messages[messages.length - 1];
@@ -267,6 +293,7 @@ export const ChatListLegend = memo(
267
293
 
268
294
  const handleMessageLongPress = useCallback(
269
295
  (messageId: string, message: MessageItem) => {
296
+ if (shouldSwallowPress()) return;
270
297
  const node = rowRefsMap.current.get(messageId);
271
298
  node?.measureInWindow((x, y, width, height) => {
272
299
  if (x == null || y == null || width == null || height == null) return;
@@ -274,23 +301,29 @@ export const ChatListLegend = memo(
274
301
  onLongPress?.(message, { x, y, width, height }, groupPosition);
275
302
  });
276
303
  },
277
- [onLongPress, messageDataMap]
304
+ [onLongPress, messageDataMap, shouldSwallowPress]
278
305
  );
279
306
 
280
307
  const onRunFlow = useCallback(
281
308
  (payload: IRunConversationFlowPayload) => {
309
+ if (shouldSwallowPress()) return;
282
310
  runFlow(payload);
283
311
  },
284
- [runFlow]
312
+ [runFlow, shouldSwallowPress]
285
313
  );
286
314
 
287
315
  const onResetFlow = useCallback(() => {
316
+ if (shouldSwallowPress()) return;
288
317
  resetFlow();
289
- }, [resetFlow]);
318
+ }, [resetFlow, shouldSwallowPress]);
290
319
 
291
- const handleMessagePress = useCallback((messageId: string) => {
292
- setRevealedMessageId((prev) => (prev === messageId ? null : messageId));
293
- }, []);
320
+ const handleMessagePress = useCallback(
321
+ (messageId: string) => {
322
+ if (shouldSwallowPress()) return;
323
+ setRevealedMessageId((prev) => (prev === messageId ? null : messageId));
324
+ },
325
+ [shouldSwallowPress]
326
+ );
294
327
 
295
328
  const handleRenderConversationViewHeader = useCallback(() => {
296
329
  if (!renderConversationViewHeader) return null;
@@ -554,6 +587,7 @@ export const ChatListLegend = memo(
554
587
  // load-more the user never asked for.
555
588
  if (!hasUserScrolledRef.current) return;
556
589
  if (hasMoreOld && !useMessageStore.getState().moreOldLoading) {
590
+ perfMark('history_start_reached');
557
591
  onLoadMoreOld?.(true);
558
592
  }
559
593
  }, [hasMoreOld, onLoadMoreOld]);
@@ -567,7 +601,8 @@ export const ChatListLegend = memo(
567
601
 
568
602
  const handleScrollBeginDrag = useCallback(() => {
569
603
  hasUserScrolledRef.current = true;
570
- }, []);
604
+ onDismissAreaScrollBeginDrag?.();
605
+ }, [onDismissAreaScrollBeginDrag]);
571
606
 
572
607
  const handleScroll = useCallback(
573
608
  (e: any) => {
@@ -649,8 +684,11 @@ export const ChatListLegend = memo(
649
684
  () => [
650
685
  styles.content,
651
686
  { paddingBottom: CONTENT_PADDING_VERTICAL + keyboardInset },
687
+ // Composer panel open: bubbles stop receiving touches (a tap only
688
+ // dismisses the panel) while the scroll view itself still scrolls.
689
+ isComposerPanelOpen && styles.contentNotHittable,
652
690
  ],
653
- [keyboardInset]
691
+ [keyboardInset, isComposerPanelOpen]
654
692
  );
655
693
 
656
694
  const animatedButtonStyle = useAnimatedStyle(() => ({
@@ -866,6 +904,7 @@ export const ChatListLegend = memo(
866
904
  const handleListLoad = useCallback(() => {
867
905
  if (hasCorrectedInitialPaddingRef.current) return;
868
906
  hasCorrectedInitialPaddingRef.current = true;
907
+ perfMark('history_list_loaded');
869
908
 
870
909
  scrollMessageToEnd({ animated: false, closeKeyboard: false });
871
910
  }, [scrollMessageToEnd]);
@@ -942,66 +981,75 @@ export const ChatListLegend = memo(
942
981
  return (
943
982
  <KContainer.View flex background={KColors.white} style={styles.container}>
944
983
  {handleRenderConversationViewHeader()}
945
- <KeyboardAwareLegendList
946
- ref={listRef}
947
- data={messages}
948
- renderItem={renderItem}
949
- // Deliberate lower bound (not a best-guess average) — see the
950
- // comment above `hasCorrectedInitialPaddingRef`. A single-line
951
- // bubble floors out around 38-44px, so this stays safely under it.
952
- estimatedItemSize={36}
953
- getItemType={getItemType}
954
- getFixedItemSize={getFixedItemSize}
955
- keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
956
- recycleItems
957
- extraData={listExtraData}
958
- alignItemsAtEnd
959
- alwaysRender={{
960
- top: 10,
961
- }}
962
- maintainScrollAtEnd
963
- maintainVisibleContentPosition={maintainVisiblePositionConfig}
964
- // See the comment above `maintainVisiblePositionConfig` — this is
965
- // what keeps a history prepend's anchor item mounted through a slow
966
- // fetch instead of the default 250px window losing it.
967
- drawDistance={800}
968
- freeze={freeze}
969
- keyboardOffset={DISABLE_NATIVE_KEYBOARD_INSET}
970
- // iOS only. Left on, React Native recomputes and applies its own
971
- // `contentInset` every time the keyboard moves — measured here as
972
- // {top: 59, bottom: keyboardHeight}, both wrong. The bottom value
973
- // over-insets by the safe area, and the phantom top one is worse:
974
- // iOS rests a scroll view at `-contentInset.top`.
975
- // That resting offset is what put the content 59pt low and hid the
976
- // last message behind the composer the moment a touch let UIKit
977
- // re-clamp. legend-list's own KeyboardAvoidingLegendList disables
978
- // this for the same reason.
979
- automaticallyAdjustContentInsets={false}
980
- // No scroll shift from the library either — the content padding
981
- // above is the single compensator. Measured with it on: a 302px
982
- // keyboard produced a 604px lift, exactly double.
983
- keyboardLiftBehavior="never"
984
- scrollEventThrottle={16}
985
- onScroll={handleScroll}
986
- onScrollBeginDrag={handleScrollBeginDrag}
987
- onLoad={handleListLoad}
988
- contentContainerStyle={contentContainerStyle}
989
- scrollIndicatorInsets={{ right: 1 }}
990
- ListHeaderComponent={
991
- <LegendChatLoadEarlier isLoading={moreOldLoading} />
992
- }
993
- onStartReached={handleStartReached}
994
- // LegendList only re-fires onStartReached after scrolling away from
995
- // the threshold by 1.3x and back. At 0.5 that distance exceeded a
996
- // short conversation's whole scrollable range, so the mount-time
997
- // firing we suppress in handleStartReached "used up" the only
998
- // trigger. 0.3 keeps that reset distance safely under a
999
- // ~1.5-screen conversation; shorter ones can still hit it.
1000
- onStartReachedThreshold={0.2}
1001
- onEndReached={handleEndReached}
1002
- onEndReachedThreshold={0.2}
1003
- initialScrollAtEnd
1004
- />
984
+ <KContainer.View flex {...dismissAreaTouchProps}>
985
+ <KeyboardAwareLegendList
986
+ ref={listRef}
987
+ data={messages}
988
+ renderItem={renderItem}
989
+ // Deliberate lower bound (not a best-guess average) — see the
990
+ // comment above `hasCorrectedInitialPaddingRef`. A single-line
991
+ // bubble floors out around 38-44px, so this stays safely under it.
992
+ estimatedItemSize={36}
993
+ getItemType={getItemType}
994
+ getFixedItemSize={getFixedItemSize}
995
+ keyExtractor={(item) =>
996
+ String(item.clientMsgID || item.serverMsgID)
997
+ }
998
+ recycleItems
999
+ extraData={listExtraData}
1000
+ alignItemsAtEnd
1001
+ alwaysRender={{
1002
+ top: 10,
1003
+ }}
1004
+ maintainScrollAtEnd
1005
+ maintainVisibleContentPosition={maintainVisiblePositionConfig}
1006
+ // See the comment above `maintainVisiblePositionConfig` — this is
1007
+ // what keeps a history prepend's anchor item mounted through a slow
1008
+ // fetch instead of the default 250px window losing it.
1009
+ drawDistance={800}
1010
+ freeze={freeze}
1011
+ keyboardOffset={DISABLE_NATIVE_KEYBOARD_INSET}
1012
+ // iOS only. Left on, React Native recomputes and applies its own
1013
+ // `contentInset` every time the keyboard moves — measured here as
1014
+ // {top: 59, bottom: keyboardHeight}, both wrong. The bottom value
1015
+ // over-insets by the safe area, and the phantom top one is worse:
1016
+ // iOS rests a scroll view at `-contentInset.top`.
1017
+ // That resting offset is what put the content 59pt low and hid the
1018
+ // last message behind the composer the moment a touch let UIKit
1019
+ // re-clamp. legend-list's own KeyboardAvoidingLegendList disables
1020
+ // this for the same reason.
1021
+ automaticallyAdjustContentInsets={false}
1022
+ // No scroll shift from the library either — the content padding
1023
+ // above is the single compensator. Measured with it on: a 302px
1024
+ // keyboard produced a 604px lift, exactly double.
1025
+ keyboardLiftBehavior="never"
1026
+ // With the default 'never' the first tap while the keyboard is up
1027
+ // only dismisses it, so "Xem thêm", links and buttons don't fire.
1028
+ // 'handled': a touchable consumes the tap and the keyboard stays;
1029
+ // a tap on anything else still dismisses it.
1030
+ keyboardShouldPersistTaps="handled"
1031
+ scrollEventThrottle={16}
1032
+ onScroll={handleScroll}
1033
+ onScrollBeginDrag={handleScrollBeginDrag}
1034
+ onLoad={handleListLoad}
1035
+ contentContainerStyle={contentContainerStyle}
1036
+ scrollIndicatorInsets={{ right: 1 }}
1037
+ ListHeaderComponent={
1038
+ <LegendChatLoadEarlier isLoading={moreOldLoading} />
1039
+ }
1040
+ onStartReached={handleStartReached}
1041
+ // LegendList only re-fires onStartReached after scrolling away from
1042
+ // the threshold by 1.3x and back. At 0.5 that distance exceeded a
1043
+ // short conversation's whole scrollable range, so the mount-time
1044
+ // firing we suppress in handleStartReached "used up" the only
1045
+ // trigger. 0.3 keeps that reset distance safely under a
1046
+ // ~1.5-screen conversation; shorter ones can still hit it.
1047
+ onStartReachedThreshold={0.2}
1048
+ onEndReached={handleEndReached}
1049
+ onEndReachedThreshold={0.2}
1050
+ initialScrollAtEnd
1051
+ />
1052
+ </KContainer.View>
1005
1053
  <Animated.View
1006
1054
  style={[styles.scrollToBottomButton, animatedButtonStyle]}
1007
1055
  >
@@ -1033,6 +1081,9 @@ const styles = StyleSheet.create({
1033
1081
  container: {
1034
1082
  position: 'relative',
1035
1083
  },
1084
+ contentNotHittable: {
1085
+ pointerEvents: 'none',
1086
+ },
1036
1087
  header: {
1037
1088
  position: 'absolute',
1038
1089
  top: 0,
@@ -12,6 +12,7 @@ import {
12
12
  import { UIUtils } from '../../utils/UIUtils';
13
13
  import { trans } from '../../translation';
14
14
  import { usePinnedMessageStore } from '../../store/pinnedMessage';
15
+ import { useExpandedMessageStore } from '../../store/expandedMessage';
15
16
  import { LegendChatMessage } from './legend/LegendChatMessage';
16
17
  import { formatMessageTime } from './legend/messageTypes';
17
18
 
@@ -122,6 +123,7 @@ const PinnedMessagesSheetContent = memo(
122
123
  <LegendChatMessage
123
124
  message={message}
124
125
  messageId={messageId}
126
+ surface="pin"
125
127
  isOutgoing={isOutgoing}
126
128
  createdAtTime={createdAtTime}
127
129
  onPress={handlePressMessage}
@@ -198,6 +200,12 @@ export const openPinnedMessagesSheet = ({
198
200
  customMessageItemFactory,
199
201
  onPressMessage,
200
202
  }: OpenPinnedMessagesSheetParams) => {
203
+ // Reset on open, not on close. `UIUtils.bottomSheet` is an adapter the host
204
+ // app registers, so whether dismissing unmounts this content — or keeps it
205
+ // mounted and hidden — is not ours to know. Clearing here satisfies
206
+ // "reopened starts shortened" whatever the host does with the sheet.
207
+ useExpandedMessageStore.getState().clearSurface('pin');
208
+
201
209
  UIUtils.bottomSheet.open({
202
210
  header: {
203
211
  title: { text: trans('chat:pinned_messages_title') },
@@ -0,0 +1,61 @@
1
+ import { useCallback } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import Reanimated from 'react-native-reanimated';
4
+ import { KContainer, KColors } from '@droppii/libs';
5
+
6
+ import { ChatAttachmentPanel } from '../ChatAttachmentPanel';
7
+ import { StickerPicker } from '../../../components/StickerPicker';
8
+ import { useSendMessage } from '../../../hooks/message/useSendMessage';
9
+ import type { StickerItem } from '../../../types/sticker';
10
+ import type { ActivePanelType } from '../hooks/useChatComposerState';
11
+ import type { DChatPannelAction } from '../types';
12
+
13
+ interface ChatComposerAccessorySlotProps {
14
+ visible: boolean;
15
+ accessorySlotStyle: any;
16
+ isAttachmentOpen: boolean;
17
+ activePanelType: ActivePanelType;
18
+ pannelActions: DChatPannelAction[];
19
+ }
20
+
21
+ // The keyboard-area slot below the composer that hosts the attachment
22
+ // panel or the sticker picker; its height is driven by useComposerPanels.
23
+ export const ChatComposerAccessorySlot = ({
24
+ visible,
25
+ accessorySlotStyle,
26
+ isAttachmentOpen,
27
+ activePanelType,
28
+ pannelActions,
29
+ }: ChatComposerAccessorySlotProps) => {
30
+ const { sendMessage } = useSendMessage();
31
+
32
+ const handleStickerSelect = useCallback(
33
+ (sticker: StickerItem) => {
34
+ sendMessage({ sticker });
35
+ },
36
+ [sendMessage]
37
+ );
38
+
39
+ return (
40
+ <KContainer.VisibleView visible={visible}>
41
+ <Reanimated.View
42
+ style={[styles.accessorySlot, accessorySlotStyle]}
43
+ pointerEvents={isAttachmentOpen ? 'auto' : 'none'}
44
+ >
45
+ {activePanelType === 'attachment' && (
46
+ <ChatAttachmentPanel actions={pannelActions} />
47
+ )}
48
+ {activePanelType === 'sticker' && (
49
+ <StickerPicker onSelectSticker={handleStickerSelect} />
50
+ )}
51
+ </Reanimated.View>
52
+ </KContainer.VisibleView>
53
+ );
54
+ };
55
+
56
+ const styles = StyleSheet.create({
57
+ accessorySlot: {
58
+ overflow: 'hidden',
59
+ backgroundColor: KColors.palette.gray.w25,
60
+ },
61
+ });