@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.
- package/lib/module/components/ThreadCard/ThreadCard.js +2 -0
- package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
- package/lib/module/components/messages/ExpandableMessageText.js +112 -0
- package/lib/module/components/messages/ExpandableMessageText.js.map +1 -0
- package/lib/module/components/messages/PreviewScrollableText.js +80 -0
- package/lib/module/components/messages/PreviewScrollableText.js.map +1 -0
- package/lib/module/components/messages/fileMessage/index.js +9 -2
- package/lib/module/components/messages/fileMessage/index.js.map +1 -1
- package/lib/module/components/messages/linkMessage/index.js +21 -12
- package/lib/module/components/messages/linkMessage/index.js.map +1 -1
- package/lib/module/components/messages/quotedMessage/index.js +18 -3
- package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
- package/lib/module/components/messages/textMessage/index.js +7 -1
- package/lib/module/components/messages/textMessage/index.js.map +1 -1
- package/lib/module/components/messages/types.js.map +1 -1
- package/lib/module/context/TimelinePressGuardContext.js +14 -0
- package/lib/module/context/TimelinePressGuardContext.js.map +1 -0
- package/lib/module/hooks/useConversationList.js +4 -0
- package/lib/module/hooks/useConversationList.js.map +1 -1
- package/lib/module/hooks/useDismissKeyboardFirst.js +19 -0
- package/lib/module/hooks/useDismissKeyboardFirst.js.map +1 -0
- package/lib/module/hooks/useExpandableText.js +125 -0
- package/lib/module/hooks/useExpandableText.js.map +1 -0
- package/lib/module/hooks/useGuardedPress.js +19 -0
- package/lib/module/hooks/useGuardedPress.js.map +1 -0
- package/lib/module/screens/chat-detail/ChatComposer.js +89 -498
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +49 -20
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +113 -76
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +7 -0
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
- package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js +47 -0
- package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js.map +1 -0
- package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js +149 -0
- package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js.map +1 -0
- package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js +104 -0
- package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +0 -17
- package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +16 -2
- package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js +122 -0
- package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js +46 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js +75 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanels.js +136 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanels.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useComposerSend.js +74 -0
- package/lib/module/screens/chat-detail/hooks/useComposerSend.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +12 -3
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +11 -2
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +79 -6
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/useClonePreview.js +30 -0
- package/lib/module/screens/chat-detail/messageToolbox/useClonePreview.js.map +1 -0
- package/lib/module/screens/chat-detail/types.js +5 -0
- package/lib/module/screens/chat-detail/types.js.map +1 -1
- package/lib/module/store/expandedMessage.js +28 -0
- package/lib/module/store/expandedMessage.js.map +1 -0
- package/lib/module/store/message.js +14 -0
- package/lib/module/store/message.js.map +1 -1
- package/lib/module/utils/composerPanelDismissGesture.js +52 -0
- package/lib/module/utils/composerPanelDismissGesture.js.map +1 -0
- package/lib/module/utils/expandableText.js +41 -0
- package/lib/module/utils/expandableText.js.map +1 -0
- package/lib/module/utils/inlineTruncation.js +51 -0
- package/lib/module/utils/inlineTruncation.js.map +1 -0
- package/lib/module/utils/perf-marker.js +44 -0
- package/lib/module/utils/perf-marker.js.map +1 -0
- package/lib/module/utils/truncateTextChildren.js +38 -0
- package/lib/module/utils/truncateTextChildren.js.map +1 -0
- package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/ExpandableMessageText.d.ts +25 -0
- package/lib/typescript/src/components/messages/ExpandableMessageText.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/PreviewScrollableText.d.ts +29 -0
- package/lib/typescript/src/components/messages/PreviewScrollableText.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/types.d.ts +18 -0
- package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
- package/lib/typescript/src/context/TimelinePressGuardContext.d.ts +3 -0
- package/lib/typescript/src/context/TimelinePressGuardContext.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts +8 -0
- package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useExpandableText.d.ts +13 -0
- package/lib/typescript/src/hooks/useExpandableText.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useGuardedPress.d.ts +9 -0
- package/lib/typescript/src/hooks/useGuardedPress.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +7 -3
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +5 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts +12 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts +30 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts +19 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +0 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +7 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts +7 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts +17 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts +12 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts +25 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts +16 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +11 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts +14 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/useClonePreview.d.ts +18 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/useClonePreview.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/types.d.ts +8 -0
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/store/expandedMessage.d.ts +15 -0
- package/lib/typescript/src/store/expandedMessage.d.ts.map +1 -0
- package/lib/typescript/src/store/message.d.ts.map +1 -1
- package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts +33 -0
- package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts.map +1 -0
- package/lib/typescript/src/utils/expandableText.d.ts +18 -0
- package/lib/typescript/src/utils/expandableText.d.ts.map +1 -0
- package/lib/typescript/src/utils/inlineTruncation.d.ts +24 -0
- package/lib/typescript/src/utils/inlineTruncation.d.ts.map +1 -0
- package/lib/typescript/src/utils/perf-marker.d.ts +8 -0
- package/lib/typescript/src/utils/perf-marker.d.ts.map +1 -0
- package/lib/typescript/src/utils/truncateTextChildren.d.ts +3 -0
- package/lib/typescript/src/utils/truncateTextChildren.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/components/ThreadCard/ThreadCard.tsx +2 -0
- package/src/components/messages/ExpandableMessageText.tsx +145 -0
- package/src/components/messages/PreviewScrollableText.tsx +98 -0
- package/src/components/messages/fileMessage/index.tsx +17 -3
- package/src/components/messages/linkMessage/index.tsx +30 -21
- package/src/components/messages/quotedMessage/index.tsx +19 -3
- package/src/components/messages/textMessage/index.tsx +11 -2
- package/src/components/messages/types.ts +18 -0
- package/src/context/TimelinePressGuardContext.tsx +15 -0
- package/src/hooks/useConversationList.ts +2 -0
- package/src/hooks/useDismissKeyboardFirst.ts +19 -0
- package/src/hooks/useExpandableText.ts +143 -0
- package/src/hooks/useGuardedPress.ts +20 -0
- package/src/screens/chat-detail/ChatComposer.tsx +105 -631
- package/src/screens/chat-detail/ChatDetail.tsx +49 -20
- package/src/screens/chat-detail/ChatListLegend.tsx +122 -71
- package/src/screens/chat-detail/PinnedMessagesSheet.tsx +8 -0
- package/src/screens/chat-detail/components/ChatComposerAccessorySlot.tsx +61 -0
- package/src/screens/chat-detail/components/ChatComposerInputArea.tsx +198 -0
- package/src/screens/chat-detail/components/ChatComposerStatusBanner.tsx +116 -0
- package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +0 -29
- package/src/screens/chat-detail/hooks/useChatComposerState.ts +25 -2
- package/src/screens/chat-detail/hooks/useComposerBotMenu.tsx +140 -0
- package/src/screens/chat-detail/hooks/useComposerPanelActions.ts +57 -0
- package/src/screens/chat-detail/hooks/useComposerPanelDismiss.ts +111 -0
- package/src/screens/chat-detail/hooks/useComposerPanels.ts +197 -0
- package/src/screens/chat-detail/hooks/useComposerSend.ts +98 -0
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +24 -2
- package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +20 -1
- package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +111 -5
- package/src/screens/chat-detail/messageToolbox/useClonePreview.ts +34 -0
- package/src/screens/chat-detail/types.ts +9 -0
- package/src/store/expandedMessage.ts +42 -0
- package/src/store/message.ts +14 -0
- package/src/utils/composerPanelDismissGesture.ts +72 -0
- package/src/utils/expandableText.ts +51 -0
- package/src/utils/inlineTruncation.ts +65 -0
- package/src/utils/perf-marker.ts +39 -0
- package/src/utils/truncateTextChildren.ts +46 -0
|
@@ -10,35 +10,37 @@ import { LinkPreviewCard } from './LinkPreviewCard';
|
|
|
10
10
|
import { getTailCornerStyle } from '../bubbleTail.utils';
|
|
11
11
|
import { BubbleTailRow } from '../BubbleTailRow';
|
|
12
12
|
import { LINK_URL_COLOR } from '../../../utils/theme';
|
|
13
|
+
import { ExpandableMessageText } from '../ExpandableMessageText';
|
|
13
14
|
|
|
15
|
+
// Returns the inner runs only — the wrapping <Text> comes from
|
|
16
|
+
// ExpandableMessageText, which is the node the line clamp has to land on.
|
|
14
17
|
const renderTextWithLinks = (
|
|
15
18
|
content: string,
|
|
16
19
|
urls: string[],
|
|
17
20
|
onPressUrl?: (url: string) => void
|
|
18
|
-
) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)}
|
|
36
|
-
</KLabel.Text>
|
|
37
|
-
);
|
|
21
|
+
) =>
|
|
22
|
+
splitTextByUrls(content, urls).map((part, i) =>
|
|
23
|
+
part.isUrl ? (
|
|
24
|
+
<KLabel.Text
|
|
25
|
+
key={i}
|
|
26
|
+
onPress={() => onPressUrl?.(part.text)}
|
|
27
|
+
color={LINK_URL_COLOR}
|
|
28
|
+
typo="TextMdMedium"
|
|
29
|
+
>
|
|
30
|
+
{part.text}
|
|
31
|
+
</KLabel.Text>
|
|
32
|
+
) : (
|
|
33
|
+
<KLabel.Text typo="TextMdNormal" key={i}>
|
|
34
|
+
{part.text}
|
|
35
|
+
</KLabel.Text>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
38
|
|
|
39
39
|
export const LinkMessage = memo(
|
|
40
40
|
({
|
|
41
41
|
message,
|
|
42
|
+
messageId,
|
|
43
|
+
surface,
|
|
42
44
|
isOutgoing,
|
|
43
45
|
createdAtTime,
|
|
44
46
|
onPressUrl,
|
|
@@ -64,7 +66,14 @@ export const LinkMessage = memo(
|
|
|
64
66
|
>
|
|
65
67
|
{!!content?.trim() && (
|
|
66
68
|
<KContainer.View style={styles.textRow}>
|
|
67
|
-
|
|
69
|
+
<ExpandableMessageText
|
|
70
|
+
messageId={messageId}
|
|
71
|
+
surface={surface}
|
|
72
|
+
text={content}
|
|
73
|
+
typo="TextMdNormal"
|
|
74
|
+
>
|
|
75
|
+
{renderTextWithLinks(content, urls, onPressUrl)}
|
|
76
|
+
</ExpandableMessageText>
|
|
68
77
|
</KContainer.View>
|
|
69
78
|
)}
|
|
70
79
|
|
|
@@ -22,8 +22,10 @@ import {
|
|
|
22
22
|
import { getFileIcon, getMimeType } from '../../../utils/imageUtils';
|
|
23
23
|
import { extractUrls } from '../../../utils/url';
|
|
24
24
|
import { renderLinkifiedText } from '../LinkifiedText';
|
|
25
|
+
import { ExpandableMessageText } from '../ExpandableMessageText';
|
|
25
26
|
import { useUserStore } from '../../../store';
|
|
26
27
|
import { useChatDetailContext } from '../../../context';
|
|
28
|
+
import { useTimelinePressGuard } from '../../../context/TimelinePressGuardContext';
|
|
27
29
|
import { ChatEvent } from '../../../types/events';
|
|
28
30
|
|
|
29
31
|
interface QuotedMessageProps extends BaseLegendMessageProps {
|
|
@@ -33,6 +35,9 @@ interface QuotedMessageProps extends BaseLegendMessageProps {
|
|
|
33
35
|
const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
34
36
|
const {
|
|
35
37
|
message,
|
|
38
|
+
messageId,
|
|
39
|
+
surface,
|
|
40
|
+
isPreview,
|
|
36
41
|
isOutgoing,
|
|
37
42
|
createdAtTime,
|
|
38
43
|
isHighlighted,
|
|
@@ -81,11 +86,14 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
|
81
86
|
return getFileIcon(mimeType);
|
|
82
87
|
}, [quotedMessage]);
|
|
83
88
|
|
|
89
|
+
// Skipped for the tap that dismisses a composer panel (timeline only).
|
|
90
|
+
const shouldSwallowPress = useTimelinePressGuard();
|
|
84
91
|
const handlePressQuotedMessage = useCallback(() => {
|
|
92
|
+
if (shouldSwallowPress()) return;
|
|
85
93
|
DeviceEventEmitter.emit(ChatEvent.ScrollToMessage, {
|
|
86
94
|
clientMessageId: quotedMessage?.clientMsgID || '',
|
|
87
95
|
});
|
|
88
|
-
}, [quotedMessage]);
|
|
96
|
+
}, [quotedMessage, shouldSwallowPress]);
|
|
89
97
|
|
|
90
98
|
return (
|
|
91
99
|
<>
|
|
@@ -104,6 +112,8 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
|
104
112
|
<KContainer.Touchable
|
|
105
113
|
style={styles.quotedContainer}
|
|
106
114
|
onPress={handlePressQuotedMessage}
|
|
115
|
+
// Would scroll the timeline hidden behind the overlay.
|
|
116
|
+
disabled={isPreview}
|
|
107
117
|
>
|
|
108
118
|
<KContainer.View style={styles.dot} />
|
|
109
119
|
<KContainer.VisibleView visible={!!imageUrl}>
|
|
@@ -152,9 +162,15 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
|
152
162
|
</KLabel.Text>
|
|
153
163
|
</KContainer.View>
|
|
154
164
|
</KContainer.Touchable>
|
|
155
|
-
<
|
|
165
|
+
<ExpandableMessageText
|
|
166
|
+
messageId={messageId}
|
|
167
|
+
surface={surface}
|
|
168
|
+
text={replyText}
|
|
169
|
+
typo="TextMdNormal"
|
|
170
|
+
color={CHAT_BUBBLE_COLORS.text}
|
|
171
|
+
>
|
|
156
172
|
{renderLinkifiedText(replyText, urls, onPressUrl, 'reply')}
|
|
157
|
-
</
|
|
173
|
+
</ExpandableMessageText>
|
|
158
174
|
</KContainer.View>
|
|
159
175
|
</BubbleTailRow>
|
|
160
176
|
|
|
@@ -15,6 +15,7 @@ import { QuotedMessagePreviewStrip } from '../quotedMessage/QuotedMessagePreview
|
|
|
15
15
|
import { LinkPreviewCard } from '../linkMessage/LinkPreviewCard';
|
|
16
16
|
import { extractUrls } from '../../../utils/url';
|
|
17
17
|
import { renderLinkifiedText } from '../LinkifiedText';
|
|
18
|
+
import { ExpandableMessageText } from '../ExpandableMessageText';
|
|
18
19
|
|
|
19
20
|
const parseMessageEx = (ex?: string): IMessageItemEx | undefined => {
|
|
20
21
|
if (!ex) return undefined;
|
|
@@ -28,6 +29,8 @@ const parseMessageEx = (ex?: string): IMessageItemEx | undefined => {
|
|
|
28
29
|
export const TextMessage = memo(
|
|
29
30
|
({
|
|
30
31
|
message,
|
|
32
|
+
messageId,
|
|
33
|
+
surface,
|
|
31
34
|
isOutgoing,
|
|
32
35
|
createdAtTime,
|
|
33
36
|
isHighlighted,
|
|
@@ -121,7 +124,13 @@ export const TextMessage = memo(
|
|
|
121
124
|
{quotedMessage && (
|
|
122
125
|
<QuotedMessagePreviewStrip quotedMessage={quotedMessage} />
|
|
123
126
|
)}
|
|
124
|
-
<
|
|
127
|
+
<ExpandableMessageText
|
|
128
|
+
messageId={messageId}
|
|
129
|
+
surface={surface}
|
|
130
|
+
text={messageText}
|
|
131
|
+
typo="TextMdNormal"
|
|
132
|
+
color={CHAT_BUBBLE_COLORS.text}
|
|
133
|
+
>
|
|
125
134
|
{segments
|
|
126
135
|
? segments.map((segment, index) =>
|
|
127
136
|
segment.type === 'mention' ? (
|
|
@@ -133,7 +142,7 @@ export const TextMessage = memo(
|
|
|
133
142
|
)
|
|
134
143
|
)
|
|
135
144
|
: messageText}
|
|
136
|
-
</
|
|
145
|
+
</ExpandableMessageText>
|
|
137
146
|
{!!urlMetadata && (
|
|
138
147
|
<LinkPreviewCard metadata={urlMetadata} onPressUrl={onPressUrl} />
|
|
139
148
|
)}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import type { MediaItem } from '../../hooks/useMediaViewer';
|
|
2
2
|
import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
3
3
|
import type { GroupPosition } from '../../utils/legendListMessage';
|
|
4
|
+
import type { MessageSurface } from '../../store/expandedMessage';
|
|
4
5
|
|
|
5
6
|
export interface BaseLegendMessageProps {
|
|
6
7
|
message: MessageItem;
|
|
8
|
+
/**
|
|
9
|
+
* Resolved once by the caller and threaded down — never re-derived here.
|
|
10
|
+
* The timeline, the pin sheet and the toolbox overlay each compute a
|
|
11
|
+
* message's id differently, and a mismatch silently splits its expansion
|
|
12
|
+
* state across surfaces.
|
|
13
|
+
*/
|
|
14
|
+
messageId: string;
|
|
15
|
+
/** Expansion is scoped per surface (BR-003): the same message collapsed in
|
|
16
|
+
* Pin Management while expanded in the timeline, and vice versa. */
|
|
17
|
+
surface: MessageSurface;
|
|
18
|
+
/**
|
|
19
|
+
* Rendered as the toolbox overlay's clone. The clone is touchable so that
|
|
20
|
+
* `Xem thêm` works there (D1), which makes every other press target in the
|
|
21
|
+
* bubble live too — components with a self-contained handler must disable
|
|
22
|
+
* it when this is set.
|
|
23
|
+
*/
|
|
24
|
+
isPreview?: boolean;
|
|
7
25
|
isOutgoing: boolean;
|
|
8
26
|
createdAtTime: number;
|
|
9
27
|
isHighlighted?: boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
const neverSwallow = () => false;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Timeline-only press guard. ChatListLegend provides the tap-outside panel
|
|
7
|
+
* dismiss's `shouldSwallowPress` (useComposerPanelDismiss); every other
|
|
8
|
+
* surface (Pin Management, toolbox overlay) gets the no-op default.
|
|
9
|
+
*/
|
|
10
|
+
const TimelinePressGuardContext = createContext<() => boolean>(neverSwallow);
|
|
11
|
+
|
|
12
|
+
export const TimelinePressGuardProvider = TimelinePressGuardContext.Provider;
|
|
13
|
+
|
|
14
|
+
export const useTimelinePressGuard = () =>
|
|
15
|
+
useContext(TimelinePressGuardContext);
|
|
@@ -4,6 +4,7 @@ import OpenIMSDK from '@droppii/openim-rn-client-sdk';
|
|
|
4
4
|
import { conversationQueryKeys } from './query-keys';
|
|
5
5
|
import { useConversationStore } from '../store';
|
|
6
6
|
import type { DChatApplicationType } from '../types/chat';
|
|
7
|
+
import { perfMark } from '../utils/perf-marker';
|
|
7
8
|
|
|
8
9
|
type Params = {
|
|
9
10
|
applicationType: DChatApplicationType;
|
|
@@ -33,6 +34,7 @@ export function useConversationList({
|
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
useConversationStore.getState().updateConversations(conversations);
|
|
37
|
+
perfMark('conversation_list_loaded', { count: conversations.length });
|
|
36
38
|
return conversations;
|
|
37
39
|
},
|
|
38
40
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Keyboard } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a press that leaves the timeline (media viewer, link, file viewer) so
|
|
6
|
+
* the keyboard is dismissed first. The timeline uses
|
|
7
|
+
* `keyboardShouldPersistTaps="handled"`, so the keyboard would otherwise stay
|
|
8
|
+
* up over the screen the press opens. An absent handler stays absent.
|
|
9
|
+
*/
|
|
10
|
+
export const useDismissKeyboardFirst = <A extends unknown[]>(
|
|
11
|
+
handler: ((...args: A) => unknown) | undefined
|
|
12
|
+
): ((...args: A) => void) | undefined =>
|
|
13
|
+
useMemo(() => {
|
|
14
|
+
if (!handler) return handler;
|
|
15
|
+
return (...args: A) => {
|
|
16
|
+
Keyboard.dismiss();
|
|
17
|
+
handler(...args);
|
|
18
|
+
};
|
|
19
|
+
}, [handler]);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { useWindowDimensions, type TextLayoutEvent } from 'react-native';
|
|
3
|
+
import { useExpandedMessageStore } from '../store/expandedMessage';
|
|
4
|
+
import { useTimelinePressGuard } from '../context/TimelinePressGuardContext';
|
|
5
|
+
import {
|
|
6
|
+
COLLAPSED_LINES,
|
|
7
|
+
expandedKey,
|
|
8
|
+
isMeasurementCandidate,
|
|
9
|
+
type MessageSurface,
|
|
10
|
+
} from '../utils/expandableText';
|
|
11
|
+
import { initialCut, joinLines, shrinkCut } from '../utils/inlineTruncation';
|
|
12
|
+
|
|
13
|
+
export { COLLAPSED_LINES };
|
|
14
|
+
|
|
15
|
+
type Verdict = { truncated: false } | { truncated: true; cut: number };
|
|
16
|
+
|
|
17
|
+
// Each pass is one extra layout; in practice the suffix fits by the second.
|
|
18
|
+
// Past this the suffix may sit on a sixth line — still visible and tappable.
|
|
19
|
+
const MAX_FIT_PASSES = 4;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Truncation is a pure function of (text, available width, typography), so it
|
|
23
|
+
* belongs to the message — not to the list row that happens to render it.
|
|
24
|
+
* `recycleItems` reuses row instances, so a verdict held in component state
|
|
25
|
+
* bleeds onto the next message in that slot. Keyed here, a recycled row reads
|
|
26
|
+
* the right answer during render and pays no measurement for a message seen.
|
|
27
|
+
*/
|
|
28
|
+
const verdictCache = new Map<string, Verdict>();
|
|
29
|
+
|
|
30
|
+
export const clearExpandableTextCache = () => verdictCache.clear();
|
|
31
|
+
|
|
32
|
+
// Window width, not the bubble's: the bubble shrinks to its content, and the
|
|
33
|
+
// content changes when the suffix is appended, so keying on it would re-measure
|
|
34
|
+
// in a loop. Surface is in the key because the pin sheet is a different width.
|
|
35
|
+
// Text length makes a body that mutates in place (TextMessage re-derives its
|
|
36
|
+
// runs when `ex` arrives) miss the cache rather than reuse a stale cut.
|
|
37
|
+
const verdictKey = (
|
|
38
|
+
surface: MessageSurface,
|
|
39
|
+
messageId: string,
|
|
40
|
+
windowWidth: number,
|
|
41
|
+
text: string
|
|
42
|
+
) => `${surface}:${messageId}:${Math.round(windowWidth)}:${text.length}`;
|
|
43
|
+
|
|
44
|
+
interface Fit {
|
|
45
|
+
key: string | null;
|
|
46
|
+
full?: string;
|
|
47
|
+
cut?: number;
|
|
48
|
+
passes: number;
|
|
49
|
+
verdict?: Verdict;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ExpandableTextMode =
|
|
53
|
+
| 'plain'
|
|
54
|
+
| 'measuring'
|
|
55
|
+
| 'fitting'
|
|
56
|
+
| 'collapsed';
|
|
57
|
+
|
|
58
|
+
export function useExpandableText(
|
|
59
|
+
messageId: string,
|
|
60
|
+
text: string,
|
|
61
|
+
surface: MessageSurface
|
|
62
|
+
) {
|
|
63
|
+
const expanded = useExpandedMessageStore(
|
|
64
|
+
(s) => !!s.expanded[expandedKey(surface, messageId)]
|
|
65
|
+
);
|
|
66
|
+
const expandInStore = useExpandedMessageStore((s) => s.expand);
|
|
67
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
68
|
+
|
|
69
|
+
const key = messageId
|
|
70
|
+
? verdictKey(surface, messageId, windowWidth, text)
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
// Reset during render rather than in an effect: an effect lands after paint,
|
|
74
|
+
// so a recycled row would show one frame with the previous message's cut.
|
|
75
|
+
const [fit, setFit] = useState<Fit>({ key, passes: 0 });
|
|
76
|
+
const current = fit.key === key ? fit : { key, passes: 0 };
|
|
77
|
+
if (fit.key !== key) setFit(current);
|
|
78
|
+
|
|
79
|
+
// Without an id there is nowhere to record an expansion, so the control
|
|
80
|
+
// would do nothing. Never collapse those.
|
|
81
|
+
const isCandidate = !!messageId && !expanded && isMeasurementCandidate(text);
|
|
82
|
+
const verdict = (key && verdictCache.get(key)) || current.verdict;
|
|
83
|
+
|
|
84
|
+
let mode: ExpandableTextMode = 'plain';
|
|
85
|
+
if (isCandidate) {
|
|
86
|
+
if (verdict) mode = verdict.truncated ? 'collapsed' : 'plain';
|
|
87
|
+
else mode = current.cut == null ? 'measuring' : 'fitting';
|
|
88
|
+
}
|
|
89
|
+
const cut = verdict?.truncated ? verdict.cut : (current.cut ?? 0);
|
|
90
|
+
|
|
91
|
+
const settle = useCallback(
|
|
92
|
+
(next: Verdict) => {
|
|
93
|
+
if (key) verdictCache.set(key, next);
|
|
94
|
+
setFit((prev) => (prev.key === key ? { ...prev, verdict: next } : prev));
|
|
95
|
+
},
|
|
96
|
+
[key]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Full, unclamped copy: how many lines, and where line N ends.
|
|
100
|
+
const onMeasureTextLayout = useCallback(
|
|
101
|
+
({ nativeEvent: { lines } }: TextLayoutEvent) => {
|
|
102
|
+
if (lines.length <= COLLAPSED_LINES) {
|
|
103
|
+
settle({ truncated: false });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const full = joinLines(lines);
|
|
107
|
+
const first = initialCut(lines, COLLAPSED_LINES);
|
|
108
|
+
setFit((prev) =>
|
|
109
|
+
prev.key === key ? { ...prev, full, cut: first, passes: 0 } : prev
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
[key, settle]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// The visible copy with the suffix appended: accept once it fits in N lines.
|
|
116
|
+
const onFitTextLayout = useCallback(
|
|
117
|
+
({ nativeEvent: { lines } }: TextLayoutEvent) => {
|
|
118
|
+
if (fit.key !== key || fit.full == null || fit.cut == null) return;
|
|
119
|
+
if (lines.length <= COLLAPSED_LINES || fit.passes >= MAX_FIT_PASSES) {
|
|
120
|
+
settle({ truncated: true, cut: fit.cut });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const next = shrinkCut(fit.full, fit.cut, lines, COLLAPSED_LINES);
|
|
124
|
+
setFit({ ...fit, cut: next, passes: fit.passes + 1 });
|
|
125
|
+
},
|
|
126
|
+
[fit, key, settle]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Skipped for the tap that dismisses a composer panel (timeline only).
|
|
130
|
+
const shouldSwallowPress = useTimelinePressGuard();
|
|
131
|
+
const expand = useCallback(() => {
|
|
132
|
+
if (shouldSwallowPress()) return;
|
|
133
|
+
expandInStore(surface, messageId);
|
|
134
|
+
}, [expandInStore, surface, messageId, shouldSwallowPress]);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
mode,
|
|
138
|
+
cut,
|
|
139
|
+
expand,
|
|
140
|
+
onMeasureTextLayout,
|
|
141
|
+
onFitTextLayout,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a timeline press handler so it is skipped while `shouldSwallow()` is
|
|
5
|
+
* true — the tap-outside panel dismiss (useComposerPanelDismiss) swallows the
|
|
6
|
+
* press of a touch that began with a composer panel open. An absent handler
|
|
7
|
+
* stays absent, so "not tappable" rows keep behaving as before. The wrapper's
|
|
8
|
+
* return value is dropped: press handlers are fire-and-forget.
|
|
9
|
+
*/
|
|
10
|
+
export const useGuardedPress = <A extends unknown[]>(
|
|
11
|
+
handler: ((...args: A) => unknown) | undefined,
|
|
12
|
+
shouldSwallow: (() => boolean) | undefined
|
|
13
|
+
): ((...args: A) => void) | undefined =>
|
|
14
|
+
useMemo(() => {
|
|
15
|
+
if (!handler || !shouldSwallow) return handler;
|
|
16
|
+
return (...args: A) => {
|
|
17
|
+
if (shouldSwallow()) return;
|
|
18
|
+
handler(...args);
|
|
19
|
+
};
|
|
20
|
+
}, [handler, shouldSwallow]);
|