@droppii-org/chat-mobile 0.2.106 → 0.2.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/lib/module/components/ThreadCard/ThreadCard.js +2 -0
  2. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  3. package/lib/module/components/messages/ExpandableMessageText.js +112 -0
  4. package/lib/module/components/messages/ExpandableMessageText.js.map +1 -0
  5. package/lib/module/components/messages/PreviewScrollableText.js +80 -0
  6. package/lib/module/components/messages/PreviewScrollableText.js.map +1 -0
  7. package/lib/module/components/messages/fileMessage/index.js +9 -2
  8. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  9. package/lib/module/components/messages/linkMessage/index.js +21 -12
  10. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  11. package/lib/module/components/messages/quotedMessage/index.js +18 -3
  12. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  13. package/lib/module/components/messages/textMessage/index.js +7 -1
  14. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  15. package/lib/module/components/messages/types.js.map +1 -1
  16. package/lib/module/context/TimelinePressGuardContext.js +14 -0
  17. package/lib/module/context/TimelinePressGuardContext.js.map +1 -0
  18. package/lib/module/hooks/useConversationList.js +4 -0
  19. package/lib/module/hooks/useConversationList.js.map +1 -1
  20. package/lib/module/hooks/useDismissKeyboardFirst.js +19 -0
  21. package/lib/module/hooks/useDismissKeyboardFirst.js.map +1 -0
  22. package/lib/module/hooks/useExpandableText.js +125 -0
  23. package/lib/module/hooks/useExpandableText.js.map +1 -0
  24. package/lib/module/hooks/useGuardedPress.js +19 -0
  25. package/lib/module/hooks/useGuardedPress.js.map +1 -0
  26. package/lib/module/screens/chat-detail/ChatComposer.js +89 -498
  27. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  28. package/lib/module/screens/chat-detail/ChatDetail.js +49 -20
  29. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  30. package/lib/module/screens/chat-detail/ChatListLegend.js +113 -76
  31. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  32. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +7 -0
  33. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
  34. package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js +47 -0
  35. package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js.map +1 -0
  36. package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js +149 -0
  37. package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js.map +1 -0
  38. package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js +104 -0
  39. package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js.map +1 -0
  40. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +0 -17
  41. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
  42. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +16 -2
  43. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
  44. package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js +122 -0
  45. package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js.map +1 -0
  46. package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js +46 -0
  47. package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js.map +1 -0
  48. package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js +75 -0
  49. package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js.map +1 -0
  50. package/lib/module/screens/chat-detail/hooks/useComposerPanels.js +136 -0
  51. package/lib/module/screens/chat-detail/hooks/useComposerPanels.js.map +1 -0
  52. package/lib/module/screens/chat-detail/hooks/useComposerSend.js +74 -0
  53. package/lib/module/screens/chat-detail/hooks/useComposerSend.js.map +1 -0
  54. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +12 -3
  55. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  56. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +11 -2
  57. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
  58. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +79 -6
  59. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
  60. package/lib/module/screens/chat-detail/messageToolbox/useClonePreview.js +30 -0
  61. package/lib/module/screens/chat-detail/messageToolbox/useClonePreview.js.map +1 -0
  62. package/lib/module/screens/chat-detail/types.js +5 -0
  63. package/lib/module/screens/chat-detail/types.js.map +1 -1
  64. package/lib/module/store/expandedMessage.js +28 -0
  65. package/lib/module/store/expandedMessage.js.map +1 -0
  66. package/lib/module/store/message.js +14 -0
  67. package/lib/module/store/message.js.map +1 -1
  68. package/lib/module/utils/composerPanelDismissGesture.js +52 -0
  69. package/lib/module/utils/composerPanelDismissGesture.js.map +1 -0
  70. package/lib/module/utils/expandableText.js +41 -0
  71. package/lib/module/utils/expandableText.js.map +1 -0
  72. package/lib/module/utils/inlineTruncation.js +51 -0
  73. package/lib/module/utils/inlineTruncation.js.map +1 -0
  74. package/lib/module/utils/perf-marker.js +44 -0
  75. package/lib/module/utils/perf-marker.js.map +1 -0
  76. package/lib/module/utils/truncateTextChildren.js +38 -0
  77. package/lib/module/utils/truncateTextChildren.js.map +1 -0
  78. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  79. package/lib/typescript/src/components/messages/ExpandableMessageText.d.ts +25 -0
  80. package/lib/typescript/src/components/messages/ExpandableMessageText.d.ts.map +1 -0
  81. package/lib/typescript/src/components/messages/PreviewScrollableText.d.ts +29 -0
  82. package/lib/typescript/src/components/messages/PreviewScrollableText.d.ts.map +1 -0
  83. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
  85. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  87. package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
  88. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/messages/types.d.ts +18 -0
  90. package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
  91. package/lib/typescript/src/context/TimelinePressGuardContext.d.ts +3 -0
  92. package/lib/typescript/src/context/TimelinePressGuardContext.d.ts.map +1 -0
  93. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  94. package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts +8 -0
  95. package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts.map +1 -0
  96. package/lib/typescript/src/hooks/useExpandableText.d.ts +13 -0
  97. package/lib/typescript/src/hooks/useExpandableText.d.ts.map +1 -0
  98. package/lib/typescript/src/hooks/useGuardedPress.d.ts +9 -0
  99. package/lib/typescript/src/hooks/useGuardedPress.d.ts.map +1 -0
  100. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +7 -3
  101. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  102. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +5 -1
  104. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  105. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
  106. package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts +12 -0
  107. package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts.map +1 -0
  108. package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts +30 -0
  109. package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts.map +1 -0
  110. package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts +19 -0
  111. package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts.map +1 -0
  112. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +0 -1
  113. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
  114. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +7 -0
  115. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
  116. package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts +7 -0
  117. package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts.map +1 -0
  118. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts +17 -0
  119. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts.map +1 -0
  120. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts +12 -0
  121. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts.map +1 -0
  122. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts +25 -0
  123. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts.map +1 -0
  124. package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts +16 -0
  125. package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts.map +1 -0
  126. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +11 -1
  127. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  128. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
  129. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts +14 -1
  130. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
  131. package/lib/typescript/src/screens/chat-detail/messageToolbox/useClonePreview.d.ts +18 -0
  132. package/lib/typescript/src/screens/chat-detail/messageToolbox/useClonePreview.d.ts.map +1 -0
  133. package/lib/typescript/src/screens/chat-detail/types.d.ts +8 -0
  134. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  135. package/lib/typescript/src/store/expandedMessage.d.ts +15 -0
  136. package/lib/typescript/src/store/expandedMessage.d.ts.map +1 -0
  137. package/lib/typescript/src/store/message.d.ts.map +1 -1
  138. package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts +33 -0
  139. package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts.map +1 -0
  140. package/lib/typescript/src/utils/expandableText.d.ts +18 -0
  141. package/lib/typescript/src/utils/expandableText.d.ts.map +1 -0
  142. package/lib/typescript/src/utils/inlineTruncation.d.ts +24 -0
  143. package/lib/typescript/src/utils/inlineTruncation.d.ts.map +1 -0
  144. package/lib/typescript/src/utils/perf-marker.d.ts +8 -0
  145. package/lib/typescript/src/utils/perf-marker.d.ts.map +1 -0
  146. package/lib/typescript/src/utils/truncateTextChildren.d.ts +3 -0
  147. package/lib/typescript/src/utils/truncateTextChildren.d.ts.map +1 -0
  148. package/package.json +2 -1
  149. package/src/components/ThreadCard/ThreadCard.tsx +2 -0
  150. package/src/components/messages/ExpandableMessageText.tsx +145 -0
  151. package/src/components/messages/PreviewScrollableText.tsx +98 -0
  152. package/src/components/messages/fileMessage/index.tsx +17 -3
  153. package/src/components/messages/linkMessage/index.tsx +30 -21
  154. package/src/components/messages/quotedMessage/index.tsx +19 -3
  155. package/src/components/messages/textMessage/index.tsx +11 -2
  156. package/src/components/messages/types.ts +18 -0
  157. package/src/context/TimelinePressGuardContext.tsx +15 -0
  158. package/src/hooks/useConversationList.ts +2 -0
  159. package/src/hooks/useDismissKeyboardFirst.ts +19 -0
  160. package/src/hooks/useExpandableText.ts +143 -0
  161. package/src/hooks/useGuardedPress.ts +20 -0
  162. package/src/screens/chat-detail/ChatComposer.tsx +105 -631
  163. package/src/screens/chat-detail/ChatDetail.tsx +49 -20
  164. package/src/screens/chat-detail/ChatListLegend.tsx +122 -71
  165. package/src/screens/chat-detail/PinnedMessagesSheet.tsx +8 -0
  166. package/src/screens/chat-detail/components/ChatComposerAccessorySlot.tsx +61 -0
  167. package/src/screens/chat-detail/components/ChatComposerInputArea.tsx +198 -0
  168. package/src/screens/chat-detail/components/ChatComposerStatusBanner.tsx +116 -0
  169. package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +0 -29
  170. package/src/screens/chat-detail/hooks/useChatComposerState.ts +25 -2
  171. package/src/screens/chat-detail/hooks/useComposerBotMenu.tsx +140 -0
  172. package/src/screens/chat-detail/hooks/useComposerPanelActions.ts +57 -0
  173. package/src/screens/chat-detail/hooks/useComposerPanelDismiss.ts +111 -0
  174. package/src/screens/chat-detail/hooks/useComposerPanels.ts +197 -0
  175. package/src/screens/chat-detail/hooks/useComposerSend.ts +98 -0
  176. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +24 -2
  177. package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +20 -1
  178. package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +111 -5
  179. package/src/screens/chat-detail/messageToolbox/useClonePreview.ts +34 -0
  180. package/src/screens/chat-detail/types.ts +9 -0
  181. package/src/store/expandedMessage.ts +42 -0
  182. package/src/store/message.ts +14 -0
  183. package/src/utils/composerPanelDismissGesture.ts +72 -0
  184. package/src/utils/expandableText.ts +51 -0
  185. package/src/utils/inlineTruncation.ts +65 -0
  186. package/src/utils/perf-marker.ts +39 -0
  187. package/src/utils/truncateTextChildren.ts +46 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useComposerPanelDismiss.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/hooks/useComposerPanelDismiss.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAyB,SAAS,EAAE,MAAM,cAAc,CAAC;AAErE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAWxD,MAAM,MAAM,qBAAqB,GAAG,IAAI,CACtC,SAAS,EACT,cAAc,GAAG,aAAa,GAAG,YAAY,GAAG,eAAe,CAChE,CAAC;AAoBF,eAAO,MAAM,uBAAuB;;;;;;;CAyEnC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type RefObject } from 'react';
2
+ import type { ActivePanelType } from './useChatComposerState';
3
+ import type { ComposerPanelController } from '../types';
4
+ export type ClosePanelMode = 'immediate' | 'fast' | 'animated';
5
+ interface UseComposerPanelsProps {
6
+ activePanelType: ActivePanelType;
7
+ setActivePanelType: (type: ActivePanelType) => void;
8
+ isAttachmentOpen: boolean;
9
+ setIsAttachmentOpen: (open: boolean) => void;
10
+ isMentionPanelOpen: boolean;
11
+ closeMentionPanel: () => void;
12
+ isKeyboardVisible: boolean;
13
+ isSlotVisible: boolean;
14
+ panelControllerRef?: RefObject<ComposerPanelController | null>;
15
+ onPanelOpenChange?: (open: boolean) => void;
16
+ }
17
+ export declare const useComposerPanels: ({ activePanelType, setActivePanelType, isAttachmentOpen, setIsAttachmentOpen, isMentionPanelOpen, closeMentionPanel, isKeyboardVisible, isSlotVisible, panelControllerRef, onPanelOpenChange, }: UseComposerPanelsProps) => {
18
+ accessorySlotStyle: any;
19
+ closeActivePanel: (mode: ClosePanelMode) => void;
20
+ yieldingCloseMode: "immediate" | "fast";
21
+ handleToggleAttachment: () => void;
22
+ handleToggleSticker: () => void;
23
+ };
24
+ export {};
25
+ //# sourceMappingURL=useComposerPanels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useComposerPanels.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/hooks/useComposerPanels.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAOxD,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/D,UAAU,sBAAsB;IAC9B,eAAe,EAAE,eAAe,CAAC;IACjC,kBAAkB,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IACpD,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mBAAmB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,iBAAiB,EAAE,OAAO,CAAC;IAG3B,aAAa,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,SAAS,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IAE/D,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7C;AAED,eAAO,MAAM,iBAAiB,GAAI,iMAW/B,sBAAsB;;6BA8Bd,cAAc;;;;CAsHxB,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type RefObject } from 'react';
2
+ import type { useChatCompose } from '../../../hooks/useChatCompose';
3
+ import type { useMentionComposer } from './useMentionComposer';
4
+ type Mentions = ReturnType<typeof useMentionComposer>['mentions'];
5
+ interface UseComposerSendProps {
6
+ compose: ReturnType<typeof useChatCompose>;
7
+ isSendMessageAllowed: boolean;
8
+ sendDeniedReason?: string;
9
+ mentions: Mentions;
10
+ clearMentions: () => void;
11
+ notifyStoppedTyping: () => void;
12
+ inputRef: RefObject<any>;
13
+ }
14
+ export declare const useComposerSend: ({ compose, isSendMessageAllowed, sendDeniedReason, mentions, clearMentions, notifyStoppedTyping, inputRef, }: UseComposerSendProps) => () => Promise<void>;
15
+ export {};
16
+ //# sourceMappingURL=useComposerSend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useComposerSend.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/hooks/useComposerSend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAQpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,UAAU,CAAC,CAAC;AAElE,UAAU,oBAAoB;IAC5B,OAAO,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;IAC3C,oBAAoB,EAAE,OAAO,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,QAAQ,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;CAC1B;AAED,eAAO,MAAM,eAAe,GAAI,8GAQ7B,oBAAoB,wBAkEnB,CAAC"}
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import type { MessageItem } from '@droppii/openim-rn-client-sdk';
3
3
  import type { MediaItem } from '../../../hooks/useMediaViewer';
4
4
  import type { GroupPosition } from '../../../utils/legendListMessage';
5
+ import type { MessageSurface } from '../../../store/expandedMessage';
5
6
  interface LegendChatMessageProps {
6
7
  message: MessageItem;
7
8
  /** Stable per-row id (see getMessageRenderId) — bound into onPress/onLongPress locally so those callbacks stay referentially stable across unrelated list re-renders. */
@@ -38,7 +39,16 @@ interface LegendChatMessageProps {
38
39
  showSenderGutter?: boolean;
39
40
  /** Position within its same-sender cluster — drives the bubble tail. */
40
41
  groupPosition?: GroupPosition;
42
+ /**
43
+ * Which surface is rendering this message. Expansion state is scoped by it
44
+ * (BR-003), so Pin Management must pass `'pin'` explicitly — the default
45
+ * keeps the timeline and the toolbox overlay sharing one scope, which is
46
+ * what makes the overlay clone mirror the row it lifted from.
47
+ */
48
+ surface?: MessageSurface;
49
+ /** Toolbox overlay clone — see `BaseLegendMessageProps.isPreview`. */
50
+ isPreview?: boolean;
41
51
  }
42
- export declare const LegendChatMessage: import("react").MemoExoticComponent<({ message, messageId, isOutgoing, createdAtTime, onMediaPress, onPressUrl, onRetry, onPress, alwaysHandlePress, onLongPress, customMessageItemFactory, isHighlighted, isTimeRevealed, showName, showAvatar, showSenderGutter, groupPosition, }: LegendChatMessageProps) => string | number | bigint | true | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode>> | import("react").JSX.Element>;
52
+ export declare const LegendChatMessage: import("react").MemoExoticComponent<({ message, messageId, isOutgoing, createdAtTime, onMediaPress, onPressUrl, onRetry, onPress, alwaysHandlePress, onLongPress, customMessageItemFactory, isHighlighted, isTimeRevealed, showName, showAvatar, showSenderGutter, groupPosition, surface, isPreview, }: LegendChatMessageProps) => string | number | bigint | true | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode>> | import("react").JSX.Element>;
43
53
  export {};
44
54
  //# sourceMappingURL=LegendChatMessage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LegendChatMessage.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/legend/LegendChatMessage.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAoC,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAGjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAe/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAStE,UAAU,sBAAsB;IAC9B,OAAO,EAAE,WAAW,CAAC;IACrB,yKAAyK;IACzK,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACzC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAChE,wBAAwB,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,SAAS,CAAC;IACzD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,wEAAwE;IACxE,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAsCD,eAAO,MAAM,iBAAiB,uRAmBzB,sBAAsB,wRA8G1B,CAAC"}
1
+ {"version":3,"file":"LegendChatMessage.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/legend/LegendChatMessage.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAoC,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAGjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAe/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAUrE,UAAU,sBAAsB;IAC9B,OAAO,EAAE,WAAW,CAAC;IACrB,yKAAyK;IACzK,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACzC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAChE,wBAAwB,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,SAAS,CAAC;IACzD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,wEAAwE;IACxE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAsCD,eAAO,MAAM,iBAAiB,2SAqBzB,sBAAsB,wRAuH1B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"MessageToolboxOverlay.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,+BAA+B,CAAC;AAcvC,UAAU,0BAA0B;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC;IAC5D,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAIhD,MAAM,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CAC/B;AAED,eAAO,MAAM,qBAAqB,0HAsNjC,CAAC"}
1
+ {"version":3,"file":"MessageToolboxOverlay.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,+BAA+B,CAAC;AAqBvC,UAAU,0BAA0B;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC;IAC5D,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAIhD,MAAM,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CAC/B;AAED,eAAO,MAAM,qBAAqB,0HAiOjC,CAAC"}
@@ -3,9 +3,22 @@ type MessageToolboxWrapperProps = {
3
3
  isOutgoing?: boolean;
4
4
  rect: MeasuredRect;
5
5
  message?: React.ReactNode;
6
+ /**
7
+ * Lets touches reach the clone's own press targets — needed for `Xem thêm`
8
+ * (D1). Off by default: types like FileMessage and consumer custom nodes
9
+ * have self-contained handlers that must stay inert here.
10
+ */
11
+ isCloneInteractive?: boolean;
12
+ /**
13
+ * Tapping the bubble used to fall through to the backdrop and dismiss. The
14
+ * ScrollView below is hit-testable and swallows that, so the gesture is
15
+ * reinstated explicitly. A touch that a press target inside the clone
16
+ * claims (`Xem thêm`) never reaches it.
17
+ */
18
+ onDismiss?: () => void;
6
19
  reaction?: React.ReactNode;
7
20
  toolbox?: React.ReactNode;
8
21
  };
9
- export declare const MessageToolboxWrapper: import("react").MemoExoticComponent<({ isOutgoing, rect, message, reaction, toolbox, }: MessageToolboxWrapperProps) => import("react").JSX.Element>;
22
+ export declare const MessageToolboxWrapper: import("react").MemoExoticComponent<({ isOutgoing, rect, message, isCloneInteractive, onDismiss, reaction, toolbox, }: MessageToolboxWrapperProps) => import("react").JSX.Element>;
10
23
  export {};
11
24
  //# sourceMappingURL=MessageToolboxWrapper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MessageToolboxWrapper.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAO5C,KAAK,0BAA0B,GAAG;IAChC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,qBAAqB,0FAO7B,0BAA0B,iCAgF9B,CAAC"}
1
+ {"version":3,"file":"MessageToolboxWrapper.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAY5C,KAAK,0BAA0B,GAAG;IAChC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,qBAAqB,yHAS7B,0BAA0B,iCAuI9B,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ClonePreviewBudget } from '../../../components/messages/PreviewScrollableText';
2
+ /**
3
+ * Wiring between the overlay's clone and the expanded text that scrolls
4
+ * inside it (see `PreviewScrollableText`).
5
+ *
6
+ * `dismissDisabled`: the clone sits in a tap-to-dismiss Pressable. On iOS a
7
+ * ScrollView refuses to scroll while an ANCESTOR holds the JS responder
8
+ * (`RCTScrollView _shouldDisableScrollInteraction`), so with the Pressable
9
+ * enabled a drag on the text never scrolls — it ends as a tap and closes the
10
+ * overlay. While the text is scrollable the Pressable stands down; the
11
+ * backdrop and the menu still dismiss.
12
+ */
13
+ export declare const useClonePreview: (maxHeight: number) => {
14
+ budget: ClonePreviewBudget;
15
+ onContentSizeChange: (_width: number, height: number) => void;
16
+ dismissDisabled: boolean;
17
+ };
18
+ //# sourceMappingURL=useClonePreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useClonePreview.d.ts","sourceRoot":"","sources":["../../../../../../src/screens/chat-detail/messageToolbox/useClonePreview.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AAE7F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM;;kCAKpC,MAAM,UAAU,MAAM;;CAclC,CAAC"}
@@ -89,4 +89,12 @@ export interface ChatDetailProps extends Omit<ChatDetailHeaderProps, 'showAddMem
89
89
  groupJoinSource?: GroupJoinSource;
90
90
  renderConversationViewHeader?: () => ReactNode;
91
91
  }
92
+ /**
93
+ * @internal Imperative handle to the composer's attachment/sticker panel,
94
+ * used by ChatDetail for tap-outside dismiss. Not part of the public API.
95
+ */
96
+ export interface ComposerPanelController {
97
+ isOpen: () => boolean;
98
+ close: () => void;
99
+ }
92
100
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACZ,MAAM,+BAA+B,CAAC;AAEvC,MAAM,MAAM,uBAAuB,GAAG,wBAAwB,GAAG,YAAY,CAAC;AAE9E,oBAAY,yBAAyB;IACnC,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,SAAS,cAAc;IACvB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,iBAAiB,sBAAsB;CACxC;AAED,oBAAY,wBAAwB;IAClC,OAAO,YAAY;IACnB,SAAS,cAAc;IACvB,WAAW,gBAAgB;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,wBAAwB,GAAG,yBAAyB,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,EAAE,EAAE,wBAAwB,CAAC;CAC9B;AACD,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,EAAE,EAAE,yBAAyB,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,aAAa,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,CAAC,cAAc,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACnD;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACnD,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC;IAC5E,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,4BAA4B,KAAK,SAAS,CAAC;CAC1E;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC;IAC5D,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,oBAAoB,CAAC;IACnC,4BAA4B,CAAC,EAAE,MAAM,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,qBAAqB,EACrB,eAAe,GAAG,gBAAgB,CACnC;IACC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC;IAC5D,aAAa,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACpC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,4BAA4B,CAAC,EAAE,MAAM,SAAS,CAAC;CAChD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { expandedKey, type MessageSurface } from '../utils/expandableText';
2
+ export type { MessageSurface };
3
+ export { expandedKey };
4
+ export interface ExpandedMessageStore {
5
+ /**
6
+ * Flat record rather than a Set so a row can subscribe with a cheap boolean
7
+ * selector — `useExpandedMessageStore(s => !!s.expanded[key])` — which stays
8
+ * referentially stable for every message the change didn't touch.
9
+ */
10
+ expanded: Record<string, true>;
11
+ expand: (surface: MessageSurface, messageId: string) => void;
12
+ clearSurface: (surface: MessageSurface) => void;
13
+ }
14
+ export declare const useExpandedMessageStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ExpandedMessageStore>>;
15
+ //# sourceMappingURL=expandedMessage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expandedMessage.d.ts","sourceRoot":"","sources":["../../../../src/store/expandedMessage.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EAEX,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EAAE,cAAc,EAAE,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,CAAC;AAEvB,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC/B,MAAM,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,YAAY,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;CACjD;AAED,eAAO,MAAM,uBAAuB,mFAoBnC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../../src/store/message.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAGV,YAAY,EACb,MAAM,eAAe,CAAC;AAavB,eAAO,MAAM,eAAe,2EAwMzB,CAAC;AAEJ,eAAO,MAAM,kBAAkB,GAAI,aAAa,MAAM,SAiBrD,CAAC"}
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../../src/store/message.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAGV,YAAY,EACb,MAAM,eAAe,CAAC;AAcvB,eAAO,MAAM,eAAe,2EAqNzB,CAAC;AAEJ,eAAO,MAAM,kBAAkB,GAAI,aAAa,MAAM,SAiBrD,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Per-touch state for dismissing the composer's attachment/sticker panel by
3
+ * tapping the chat timeline: a touch that began while the panel was open and
4
+ * ended without dragging is a dismiss tap. Drag detection is explicit — a
5
+ * movement threshold or the list's scroll-begin — since the touch is only
6
+ * observed (touch events), never claimed as the JS responder.
7
+ */
8
+ export declare const DISMISS_DRAG_THRESHOLD_PX = 10;
9
+ export interface DismissTouch {
10
+ panelOpenAtStart: boolean;
11
+ dragged: boolean;
12
+ startX: number;
13
+ startY: number;
14
+ }
15
+ export declare const IDLE_DISMISS_TOUCH: DismissTouch;
16
+ export declare const startDismissTouch: (panelOpen: boolean, x: number, y: number) => DismissTouch;
17
+ export declare const moveDismissTouch: (touch: DismissTouch, x: number, y: number) => DismissTouch;
18
+ export declare const markDismissTouchDragged: (touch: DismissTouch) => DismissTouch;
19
+ /** Whether releasing this touch should close the panel. */
20
+ export declare const shouldDismissOnRelease: (touch: DismissTouch) => boolean;
21
+ /**
22
+ * How long after the finger lifts a press still belongs to the dismissing
23
+ * touch. RNGH Tap and JS onPress callbacks land right after touchEnd; a later
24
+ * press (e.g. a screen-reader activation, which sends no touch events) must
25
+ * not be swallowed by a stale flag.
26
+ */
27
+ export declare const SWALLOW_AFTER_RELEASE_MS = 500;
28
+ /**
29
+ * Whether a timeline press must be swallowed. `releasedAt` is null while the
30
+ * dismissing touch is still down.
31
+ */
32
+ export declare const isPressSwallowed: (swallow: boolean, releasedAt: number | null, now: number) => boolean;
33
+ //# sourceMappingURL=composerPanelDismissGesture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composerPanelDismissGesture.d.ts","sourceRoot":"","sources":["../../../../src/utils/composerPanelDismissGesture.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,WAAW,YAAY;IAC3B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,EAAE,YAKhC,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,OAAO,EAClB,GAAG,MAAM,EACT,GAAG,MAAM,KACR,YAKD,CAAC;AAEH,eAAO,MAAM,gBAAgB,GAC3B,OAAO,YAAY,EACnB,GAAG,MAAM,EACT,GAAG,MAAM,KACR,YAMF,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,OAAO,YAAY,KAAG,YACT,CAAC;AAEtD,2DAA2D;AAC3D,eAAO,MAAM,sBAAsB,GAAI,OAAO,YAAY,KAAG,OACnB,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,OAAO,EAChB,YAAY,MAAM,GAAG,IAAI,EACzB,KAAK,MAAM,KACV,OAEmE,CAAC"}
@@ -0,0 +1,18 @@
1
+ export type MessageSurface = 'timeline' | 'pin';
2
+ export declare const COLLAPSED_LINES = 5;
3
+ export declare const expandedKey: (surface: MessageSurface, messageId: string) => string;
4
+ /**
5
+ * Character count alone is not an approximation of rendered lines — it is
6
+ * wrong for the shapes this chat carries most (pasted addresses, order lists),
7
+ * where an eight-line message can be under 70 characters. Newlines cost one
8
+ * character and a whole line, so both signals are needed. Message text is
9
+ * sender-controlled, so a length-only gate is also trivially defeated.
10
+ */
11
+ export declare const isMeasurementCandidate: (text: string) => boolean;
12
+ /**
13
+ * Returns a copy without the entries belonging to `surface`, or the original
14
+ * reference when nothing matched — callers use that identity to skip a
15
+ * pointless store update.
16
+ */
17
+ export declare const withoutSurface: (expanded: Record<string, true>, surface: MessageSurface) => Record<string, true>;
18
+ //# sourceMappingURL=expandableText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expandableText.d.ts","sourceRoot":"","sources":["../../../../src/utils/expandableText.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,KAAK,CAAC;AAEhD,eAAO,MAAM,eAAe,IAAI,CAAC;AAIjC,eAAO,MAAM,WAAW,GAAI,SAAS,cAAc,EAAE,WAAW,MAAM,WAC3C,CAAC;AAU5B;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,MAAM,YAEX,CAAC;AAEzC;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAC9B,SAAS,cAAc,KACtB,MAAM,CAAC,MAAM,EAAE,IAAI,CAcrB,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Cut-point arithmetic for the inline `... Xem thêm` preview. Everything here
3
+ * works on the lines RN reports from `onTextLayout`, whose `text` includes the
4
+ * line's trailing break — so joining them reproduces the rendered string and a
5
+ * running length is a valid character offset into it.
6
+ */
7
+ export interface RenderedLine {
8
+ text: string;
9
+ }
10
+ export declare const joinLines: (lines: readonly RenderedLine[]) => string;
11
+ /**
12
+ * Backs `cut` off trailing whitespace — a kept `\n` would push the suffix onto
13
+ * a line of its own — and off half an emoji, which would render as a tofu box.
14
+ */
15
+ export declare const settleCut: (full: string, cut: number) => number;
16
+ /** First guess: everything on the first `maxLines` lines. */
17
+ export declare const initialCut: (lines: readonly RenderedLine[], maxLines: number) => number;
18
+ /**
19
+ * The suffix did not fit and wrapped past `maxLines`. Drop at least as many
20
+ * characters as spilled over — the spill includes the suffix itself, so this
21
+ * converges in one or two passes — then prefer ending on a word.
22
+ */
23
+ export declare const shrinkCut: (full: string, cut: number, fittedLines: readonly RenderedLine[], maxLines: number) => number;
24
+ //# sourceMappingURL=inlineTruncation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inlineTruncation.d.ts","sourceRoot":"","sources":["../../../../src/utils/inlineTruncation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAUD,eAAO,MAAM,SAAS,GAAI,OAAO,SAAS,YAAY,EAAE,WACf,CAAC;AAE1C;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,WAKlD,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,UAAU,GACrB,OAAO,SAAS,YAAY,EAAE,EAC9B,UAAU,MAAM,WAIjB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS,GACpB,MAAM,MAAM,EACZ,KAAK,MAAM,EACX,aAAa,SAAS,YAAY,EAAE,EACpC,UAAU,MAAM,WAcjB,CAAC"}
@@ -0,0 +1,8 @@
1
+ export declare const enablePerfMarkers: (on?: boolean) => void;
2
+ export declare const perfMark: (name: string, data?: Record<string, unknown>) => void;
3
+ /**
4
+ * Counts render-function invocations of `component`, including renders React later discards.
5
+ * Counts are batched and emitted as a delta every ~500 ms, so logging never happens per render.
6
+ */
7
+ export declare const perfCountRender: (component: string) => void;
8
+ //# sourceMappingURL=perf-marker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perf-marker.d.ts","sourceRoot":"","sources":["../../../../src/utils/perf-marker.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,iBAAiB,GAAI,YAAS,SAE1C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SAGpE,CAAC;AASF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,SAKhD,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare const truncateTextChildren: (node: ReactNode, limit: number) => ReactNode;
3
+ //# sourceMappingURL=truncateTextChildren.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncateTextChildren.d.ts","sourceRoot":"","sources":["../../../../src/utils/truncateTextChildren.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AA4CrE,eAAO,MAAM,oBAAoB,GAAI,MAAM,SAAS,EAAE,OAAO,MAAM,cAC7C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droppii-org/chat-mobile",
3
- "version": "0.2.106",
3
+ "version": "0.2.108",
4
4
  "description": "Droppii chat mobile",
5
5
  "keywords": [
6
6
  "react-native",
@@ -63,6 +63,7 @@
63
63
  "typecheck": "tsc -p tsconfig.build.json 2>&1 | grep -E 'src/.*error' || echo 'Typecheck passed (external dependency warnings ignored)'",
64
64
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
65
65
  "test": "jest",
66
+ "perf": "node performance/scripts/cli.mjs",
66
67
  "release": "release-it --no-git --ci --no-increment",
67
68
  "web": "vite",
68
69
  "build:web": "vite build",
@@ -25,6 +25,7 @@ import { MuteThread } from './MuteThread';
25
25
  import { PinThread } from './PinThread';
26
26
  import { useIsJoinedGroup } from '../../hooks/useIsJoinedGroup';
27
27
  import isEmpty from 'lodash/isEmpty';
28
+ import { perfCountRender } from '../../utils/perf-marker';
28
29
 
29
30
  interface ThreadCardProps {
30
31
  item: string;
@@ -35,6 +36,7 @@ interface ThreadCardProps {
35
36
  }
36
37
 
37
38
  const ThreadCard = memo(({ item, onPress }: ThreadCardProps) => {
39
+ perfCountRender('ThreadCard');
38
40
  const conversation = useConversation(item);
39
41
 
40
42
  const {
@@ -0,0 +1,145 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { KColors, KContainer, KLabel } from '@droppii/libs';
4
+ import { trans } from '../../translation';
5
+ import {
6
+ COLLAPSED_LINES,
7
+ useExpandableText,
8
+ } from '../../hooks/useExpandableText';
9
+ import { truncateTextChildren } from '../../utils/truncateTextChildren';
10
+ import { PreviewScrollableText } from './PreviewScrollableText';
11
+ import type { MessageSurface } from '../../store/expandedMessage';
12
+
13
+ type TypoType = ComponentProps<typeof KLabel.Text>['typo'];
14
+
15
+ // Values from `@droppii/libs`' Typography.tsx (`generateTextStyle`'s third
16
+ // argument), not looked up at runtime — the package does not cleanly
17
+ // re-export a way to do that. All three call sites use `TextMdNormal` today;
18
+ // add an entry here before passing any other `typo` to this component.
19
+ const TYPO_LINE_HEIGHT: Record<string, number> = {
20
+ TextMdNormal: 25.6,
21
+ };
22
+ const DEFAULT_LINE_HEIGHT = 22.4;
23
+
24
+ // Figma (Live Chat, node 15389-207764): `…và... Xem thêm` — the ellipsis in
25
+ // the body colour, the control inline on the last visible line.
26
+ const ELLIPSIS = '... ';
27
+
28
+ interface ExpandableMessageTextProps {
29
+ messageId: string;
30
+ surface: MessageSurface;
31
+ /** Raw body text — drives the length/newline pre-check only. */
32
+ text: string;
33
+ /**
34
+ * The already-rendered body. Taken as children rather than re-derived from
35
+ * `text` because the three call sites build structurally different bodies
36
+ * (plain run, mention segments, linkified runs) and re-tokenising them here
37
+ * would duplicate three parsers.
38
+ */
39
+ children: ReactNode;
40
+ typo?: TypoType;
41
+ color?: string;
42
+ }
43
+
44
+ export const ExpandableMessageText = ({
45
+ messageId,
46
+ surface,
47
+ text,
48
+ children,
49
+ typo = 'TextMdNormal',
50
+ color,
51
+ }: ExpandableMessageTextProps) => {
52
+ const { mode, cut, expand, onMeasureTextLayout, onFitTextLayout } =
53
+ useExpandableText(messageId, text, surface);
54
+
55
+ if (mode === 'plain') {
56
+ // Only the overlay provides a budget; everywhere else this is a no-op.
57
+ return (
58
+ <PreviewScrollableText>
59
+ <KLabel.Text typo={typo} color={color}>
60
+ {children}
61
+ </KLabel.Text>
62
+ </PreviewScrollableText>
63
+ );
64
+ }
65
+
66
+ const label = trans('chat:info_view_more') || 'Xem thêm';
67
+
68
+ if (mode === 'measuring') {
69
+ // One layout pass. The visible copy is clipped to N lines by height so the
70
+ // bubble does not flash at full length; the hidden copy is unconstrained,
71
+ // which is what makes its `onTextLayout` report every line on iOS too.
72
+ const lineHeight =
73
+ TYPO_LINE_HEIGHT[typo ?? 'TextMdNormal'] ?? DEFAULT_LINE_HEIGHT;
74
+ return (
75
+ <KContainer.View>
76
+ <View
77
+ style={[
78
+ styles.clampedBox,
79
+ { maxHeight: lineHeight * COLLAPSED_LINES },
80
+ ]}
81
+ >
82
+ <KLabel.Text typo={typo} color={color}>
83
+ {children}
84
+ </KLabel.Text>
85
+ </View>
86
+ <View
87
+ style={styles.measuringCopy}
88
+ pointerEvents="none"
89
+ accessibilityElementsHidden
90
+ importantForAccessibility="no-hide-descendants"
91
+ >
92
+ <KLabel.Text
93
+ typo={typo}
94
+ color={color}
95
+ onTextLayout={onMeasureTextLayout}
96
+ >
97
+ {children}
98
+ </KLabel.Text>
99
+ </View>
100
+ </KContainer.View>
101
+ );
102
+ }
103
+
104
+ // 'fitting' re-checks that the suffix still fits in N lines; 'collapsed'
105
+ // renders a cut that already passed that check.
106
+ return (
107
+ <KLabel.Text
108
+ typo={typo}
109
+ color={color}
110
+ onTextLayout={mode === 'fitting' ? onFitTextLayout : undefined}
111
+ >
112
+ {truncateTextChildren(children, cut)}
113
+ {ELLIPSIS}
114
+ <KLabel.Text
115
+ typo="TextMdMedium"
116
+ color={KColors.primary.normal}
117
+ onPress={expand}
118
+ accessibilityRole="button"
119
+ accessibilityLabel={label}
120
+ >
121
+ {label}
122
+ </KLabel.Text>
123
+ </KLabel.Text>
124
+ );
125
+ };
126
+
127
+ ExpandableMessageText.displayName = 'ExpandableMessageText';
128
+
129
+ const styles = StyleSheet.create({
130
+ clampedBox: {
131
+ overflow: 'hidden',
132
+ },
133
+ // `left`+`right` take the container's width in the same layout pass, so the
134
+ // copy wraps exactly like the visible one without a round trip through
135
+ // `onLayout`. No `bottom`: pinning it (i.e. absoluteFill) would make this
136
+ // inherit the clamped parent's height, and iOS would lay out only the lines
137
+ // that fit — reporting a clamped message as short.
138
+ measuringCopy: {
139
+ opacity: 0,
140
+ position: 'absolute',
141
+ left: 0,
142
+ right: 0,
143
+ top: 0,
144
+ },
145
+ });
@@ -0,0 +1,98 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useState,
7
+ type ReactNode,
8
+ } from 'react';
9
+ import { ScrollView, View, type LayoutChangeEvent } from 'react-native';
10
+
11
+ /**
12
+ * Set by the toolbox overlay around its clone. `maxHeight` is the space the
13
+ * clone may take; `contentHeight` is how tall the clone currently is.
14
+ * `onScrollableChange` reports whether the text is capped (and so scrolls),
15
+ * because the overlay's tap-to-dismiss would otherwise block that scroll.
16
+ */
17
+ export interface ClonePreviewBudget {
18
+ maxHeight: number;
19
+ contentHeight: number;
20
+ onScrollableChange: (scrollable: boolean) => void;
21
+ }
22
+
23
+ export const ClonePreviewBudgetContext =
24
+ createContext<ClonePreviewBudget | null>(null);
25
+
26
+ // Never squeeze the text below three lines to make room for a tall card.
27
+ const MIN_TEXT_HEIGHT = 25.6 * 3;
28
+
29
+ /**
30
+ * An expanded message can be taller than the overlay. Scrolling the whole
31
+ * clone clips the bubble along a straight edge — square corners — so inside
32
+ * the overlay the text region scrolls instead and the bubble keeps its shape.
33
+ *
34
+ * Budget: whatever the rest of the clone (padding, quote strip, link card,
35
+ * time row) does not use. `contentHeight` and this box's height arrive from
36
+ * native at different moments, and mixing a fresh one with a stale one makes
37
+ * the cap swing between two sizes indefinitely. So the rest is read only from
38
+ * a consistent pair — while uncapped, when the box equals the natural height —
39
+ * and frozen once a cap is applied, which breaks the feedback loop.
40
+ */
41
+ export const PreviewScrollableText = ({
42
+ children,
43
+ }: {
44
+ children: ReactNode;
45
+ }) => {
46
+ const budget = useContext(ClonePreviewBudgetContext);
47
+ const [naturalHeight, setNaturalHeight] = useState(0);
48
+ const [boxHeight, setBoxHeight] = useState(0);
49
+ const [frozenRest, setFrozenRest] = useState<number | null>(null);
50
+
51
+ const onBoxLayout = useCallback((e: LayoutChangeEvent) => {
52
+ setBoxHeight(e.nativeEvent.layout.height);
53
+ }, []);
54
+ const onContentLayout = useCallback((e: LayoutChangeEvent) => {
55
+ setNaturalHeight(e.nativeEvent.layout.height);
56
+ }, []);
57
+
58
+ let rest = frozenRest;
59
+ const uncappedPairIsConsistent =
60
+ !!budget &&
61
+ naturalHeight > 0 &&
62
+ Math.abs(boxHeight - naturalHeight) <= 1 &&
63
+ budget.contentHeight >= naturalHeight;
64
+ if (budget && rest == null && uncappedPairIsConsistent) {
65
+ rest = budget.contentHeight - naturalHeight;
66
+ }
67
+
68
+ let maxHeight: number | undefined;
69
+ if (budget && rest != null) {
70
+ const allowed = budget.maxHeight - rest;
71
+ if (naturalHeight > allowed + 1) {
72
+ maxHeight = Math.max(allowed, MIN_TEXT_HEIGHT);
73
+ if (frozenRest == null) setFrozenRest(rest);
74
+ }
75
+ }
76
+
77
+ const scrollable = maxHeight != null;
78
+ const onScrollableChange = budget?.onScrollableChange;
79
+ useEffect(() => {
80
+ if (!onScrollableChange) return;
81
+ onScrollableChange(scrollable);
82
+ return () => onScrollableChange(false);
83
+ }, [onScrollableChange, scrollable]);
84
+
85
+ if (!budget) return <>{children}</>;
86
+
87
+ return (
88
+ <ScrollView
89
+ style={maxHeight != null ? { maxHeight } : undefined}
90
+ onLayout={onBoxLayout}
91
+ nestedScrollEnabled
92
+ bounces={false}
93
+ showsVerticalScrollIndicator={false}
94
+ >
95
+ <View onLayout={onContentLayout}>{children}</View>
96
+ </ScrollView>
97
+ );
98
+ };
@@ -7,6 +7,9 @@ import { getTailCornerStyle } from '../bubbleTail.utils';
7
7
  import { BubbleTailRow } from '../BubbleTailRow';
8
8
  import type { BaseLegendMessageProps } from '../types';
9
9
  import { useFileDownload } from '../../../hooks/useFileDownload';
10
+ import { useGuardedPress } from '../../../hooks/useGuardedPress';
11
+ import { useDismissKeyboardFirst } from '../../../hooks/useDismissKeyboardFirst';
12
+ import { useTimelinePressGuard } from '../../../context/TimelinePressGuardContext';
10
13
  import {
11
14
  getFileIcon,
12
15
  getMimeType,
@@ -22,9 +25,20 @@ export const FileMessage = memo(
22
25
  (message.fileElem as any)?.url ||
23
26
  '';
24
27
 
25
- const { state, handleView, handleDownload } = useFileDownload(
26
- fileName,
27
- url
28
+ const {
29
+ state,
30
+ handleView: handleViewRaw,
31
+ handleDownload: handleDownloadRaw,
32
+ } = useFileDownload(fileName, url);
33
+ // Skipped for the tap that dismisses a composer panel (timeline only).
34
+ const shouldSwallowPress = useTimelinePressGuard();
35
+ const handleView = useGuardedPress(
36
+ useDismissKeyboardFirst(handleViewRaw),
37
+ shouldSwallowPress
38
+ );
39
+ const handleDownload = useGuardedPress(
40
+ handleDownloadRaw,
41
+ shouldSwallowPress
28
42
  );
29
43
 
30
44
  const mimeType = useMemo(() => getMimeType(fileName), [fileName]);