@droppii-org/chat-mobile 0.2.105 → 0.2.107
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/TypingDots/TypingDots.js +131 -0
- package/lib/module/components/TypingDots/TypingDots.js.map +1 -0
- package/lib/module/components/TypingDots/index.js +4 -0
- package/lib/module/components/TypingDots/index.js.map +1 -0
- 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/linkMessage/index.js +21 -12
- package/lib/module/components/messages/linkMessage/index.js.map +1 -1
- package/lib/module/components/messages/quotedMessage/index.js +12 -2
- 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/core/useChatListener.js +26 -2
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/groups/useGroupMemberName.js +32 -0
- package/lib/module/hooks/groups/useGroupMemberName.js.map +1 -0
- package/lib/module/hooks/query-keys.js +2 -1
- package/lib/module/hooks/query-keys.js.map +1 -1
- package/lib/module/hooks/useExpandableText.js +120 -0
- package/lib/module/hooks/useExpandableText.js.map +1 -0
- package/lib/module/hooks/useInputState.js +38 -0
- package/lib/module/hooks/useInputState.js.map +1 -0
- package/lib/module/hooks/useInputStateBroadcast.js +53 -0
- package/lib/module/hooks/useInputStateBroadcast.js.map +1 -0
- package/lib/module/screens/chat-detail/ChatComposer.js +15 -3
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +11 -0
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +2 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatTypingIndicator.js +86 -0
- package/lib/module/screens/chat-detail/ChatTypingIndicator.js.map +1 -0
- 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/legend/LegendChatMessage.js +10 -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/store/expandedMessage.js +28 -0
- package/lib/module/store/expandedMessage.js.map +1 -0
- package/lib/module/store/index.js +1 -0
- package/lib/module/store/index.js.map +1 -1
- package/lib/module/store/inputState.js +64 -0
- package/lib/module/store/inputState.js.map +1 -0
- package/lib/module/translation/resources/i18n.js +2 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- 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/inputState.js +77 -0
- package/lib/module/utils/inputState.js.map +1 -0
- package/lib/module/utils/message.js +9 -0
- package/lib/module/utils/message.js.map +1 -1
- package/lib/module/utils/truncateTextChildren.js +38 -0
- package/lib/module/utils/truncateTextChildren.js.map +1 -0
- package/lib/typescript/src/components/TypingDots/TypingDots.d.ts +14 -0
- package/lib/typescript/src/components/TypingDots/TypingDots.d.ts.map +1 -0
- package/lib/typescript/src/components/TypingDots/index.d.ts +3 -0
- package/lib/typescript/src/components/TypingDots/index.d.ts.map +1 -0
- 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/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/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/groups/useGroupMemberName.d.ts +10 -0
- package/lib/typescript/src/hooks/groups/useGroupMemberName.d.ts.map +1 -0
- package/lib/typescript/src/hooks/query-keys.d.ts +1 -0
- package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
- 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/useInputState.d.ts +21 -0
- package/lib/typescript/src/hooks/useInputState.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useInputStateBroadcast.d.ts +14 -0
- package/lib/typescript/src/hooks/useInputStateBroadcast.d.ts.map +1 -0
- 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.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatTypingIndicator.d.ts +8 -0
- package/lib/typescript/src/screens/chat-detail/ChatTypingIndicator.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
- 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/store/expandedMessage.d.ts +15 -0
- package/lib/typescript/src/store/expandedMessage.d.ts.map +1 -0
- package/lib/typescript/src/store/index.d.ts +1 -0
- package/lib/typescript/src/store/index.d.ts.map +1 -1
- package/lib/typescript/src/store/inputState.d.ts +18 -0
- package/lib/typescript/src/store/inputState.d.ts.map +1 -0
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- 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/inputState.d.ts +54 -0
- package/lib/typescript/src/utils/inputState.d.ts.map +1 -0
- package/lib/typescript/src/utils/message.d.ts +8 -0
- package/lib/typescript/src/utils/message.d.ts.map +1 -1
- package/lib/typescript/src/utils/truncateTextChildren.d.ts +3 -0
- package/lib/typescript/src/utils/truncateTextChildren.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/TypingDots/TypingDots.tsx +172 -0
- package/src/components/TypingDots/index.ts +2 -0
- package/src/components/messages/ExpandableMessageText.tsx +145 -0
- package/src/components/messages/PreviewScrollableText.tsx +98 -0
- package/src/components/messages/linkMessage/index.tsx +30 -21
- package/src/components/messages/quotedMessage/index.tsx +14 -2
- package/src/components/messages/textMessage/index.tsx +11 -2
- package/src/components/messages/types.ts +18 -0
- package/src/core/useChatListener.ts +35 -1
- package/src/hooks/groups/useGroupMemberName.ts +31 -0
- package/src/hooks/query-keys.ts +2 -0
- package/src/hooks/useExpandableText.ts +139 -0
- package/src/hooks/useInputState.ts +57 -0
- package/src/hooks/useInputStateBroadcast.ts +53 -0
- package/src/screens/chat-detail/ChatComposer.tsx +17 -2
- package/src/screens/chat-detail/ChatDetail.tsx +11 -0
- package/src/screens/chat-detail/ChatListLegend.tsx +2 -0
- package/src/screens/chat-detail/ChatTypingIndicator.tsx +89 -0
- package/src/screens/chat-detail/PinnedMessagesSheet.tsx +8 -0
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +22 -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/store/expandedMessage.ts +42 -0
- package/src/store/index.ts +1 -0
- package/src/store/inputState.ts +76 -0
- package/src/translation/resources/i18n.ts +2 -0
- package/src/utils/expandableText.ts +51 -0
- package/src/utils/inlineTruncation.ts +65 -0
- package/src/utils/inputState.ts +104 -0
- package/src/utils/message.ts +15 -0
- package/src/utils/truncateTextChildren.ts +46 -0
|
@@ -27,6 +27,14 @@ export declare const resolveHistoryLastMinSeq: (history: {
|
|
|
27
27
|
}, fallbackMessage?: MessageItem) => number;
|
|
28
28
|
export declare const getHistoryPaginationAnchor: (messages: MessageItem[]) => HistoryPaginationAnchor | null;
|
|
29
29
|
export declare const hasNewHistoryMessages: (current: MessageItem[], incoming: MessageItem[]) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Conversation ids a message could be filed under.
|
|
32
|
+
*
|
|
33
|
+
* A 1:1 id is built from both participants and their order is not recoverable
|
|
34
|
+
* from the message alone, so both spellings come back — `belongsToConversation`
|
|
35
|
+
* compares against both for exactly the same reason.
|
|
36
|
+
*/
|
|
37
|
+
export declare const conversationIdsOfMessage: (message: MessageItem) => string[];
|
|
30
38
|
export declare const belongsToConversation: (message: MessageItem, conversationId?: string, targetUserId?: string) => boolean;
|
|
31
39
|
export declare const getReceiverId: (conversation?: ConversationItem) => {
|
|
32
40
|
groupID: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../../src/utils/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,+BAA+B,CAAC;AAMvC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,WACN,CAAC;AAK9C;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,WACA,CAAC;AAEvE,eAAO,MAAM,aAAa,GACxB,SAAS,WAAW,EAAE,EACtB,UAAU,WAAW,EAAE,KACtB,WAAW,EAab,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,SAAS,WAAW,GAAG,MAAM,KAC5B,WAYF,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAChC,kBAAkB,WAAW,KAC5B,MAMF,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACrC,UAAU,WAAW,EAAE,KACtB,uBAAuB,GAAG,IAkB5B,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,SAAS,WAAW,EAAE,EACtB,UAAU,WAAW,EAAE,KACtB,OASF,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,SAAS,WAAW,EACpB,iBAAiB,MAAM,EACvB,eAAe,MAAM,KACpB,OA2BF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,eAAe,gBAAgB;;;CAW5D,CAAC"}
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../../src/utils/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,+BAA+B,CAAC;AAMvC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,WACN,CAAC;AAK9C;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,WACA,CAAC;AAEvE,eAAO,MAAM,aAAa,GACxB,SAAS,WAAW,EAAE,EACtB,UAAU,WAAW,EAAE,KACtB,WAAW,EAab,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,SAAS,WAAW,GAAG,MAAM,KAC5B,WAYF,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAChC,kBAAkB,WAAW,KAC5B,MAMF,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACrC,UAAU,WAAW,EAAE,KACtB,uBAAuB,GAAG,IAkB5B,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,SAAS,WAAW,EAAE,EACtB,UAAU,WAAW,EAAE,KACtB,OASF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,WAAW,KAAG,MAAM,EAM/D,CAAC;AAER,eAAO,MAAM,qBAAqB,GAChC,SAAS,WAAW,EACpB,iBAAiB,MAAM,EACvB,eAAe,MAAM,KACpB,OA2BF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,eAAe,gBAAgB;;;CAW5D,CAAC"}
|
|
@@ -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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { memo, useEffect } from 'react';
|
|
2
|
+
import Animated, {
|
|
3
|
+
Easing,
|
|
4
|
+
useAnimatedProps,
|
|
5
|
+
useReducedMotion,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withDelay,
|
|
8
|
+
withRepeat,
|
|
9
|
+
withSequence,
|
|
10
|
+
withSpring,
|
|
11
|
+
withTiming,
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
import Svg, { G, Rect, type SvgProps } from 'react-native-svg';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The three bouncing dots from `assets/images/typing_indicator.svg`.
|
|
17
|
+
*
|
|
18
|
+
* SVGR cannot carry this one over: the motion lives in a `<style>` block of
|
|
19
|
+
* `@keyframes`, which it drops ("elements not supported by react-native-svg"),
|
|
20
|
+
* leaving `style={{ animation: ... }}` props pointing at rules that no longer
|
|
21
|
+
* exist. react-native-svg has no CSS animation engine either, so the keyframes
|
|
22
|
+
* are re-expressed here as reanimated timelines instead.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const VIEW_WIDTH = 48;
|
|
26
|
+
const VIEW_HEIGHT = 20;
|
|
27
|
+
const DOT_SIZE = 8;
|
|
28
|
+
/** x of each dot in the source — `translate(6 6)`, `(20 6)`, `(34 6)`. */
|
|
29
|
+
const DOT_X = [6, 20, 34];
|
|
30
|
+
const DOT_Y = 6;
|
|
31
|
+
/** The `translateY(-6px)` keyframe. */
|
|
32
|
+
const LIFT = 6;
|
|
33
|
+
|
|
34
|
+
const CYCLE_MS = 3150;
|
|
35
|
+
/** 7.14% of the cycle — the gap between one dot bouncing and the next. */
|
|
36
|
+
const BOUNCE_MS = CYCLE_MS / 14;
|
|
37
|
+
/** 14.29% of the cycle — one step of the opacity wave. */
|
|
38
|
+
const FADE_STEP_MS = CYCLE_MS / 7;
|
|
39
|
+
const FADE_STEPS = 7;
|
|
40
|
+
|
|
41
|
+
const HOLD_SHORT_MS = 900; // 14.29% → 42.86%
|
|
42
|
+
const HOLD_LONG_MS = 1350; // 57.14% → 100%
|
|
43
|
+
|
|
44
|
+
/** The three alpha levels the wave cycles through. */
|
|
45
|
+
const ALPHAS = [0.2, 0.35, 0.5];
|
|
46
|
+
|
|
47
|
+
/** The source's own colour, used when the caller does not pass one. */
|
|
48
|
+
const DEFAULT_FILL = '#1A1D22';
|
|
49
|
+
|
|
50
|
+
const RISE = {
|
|
51
|
+
duration: BOUNCE_MS,
|
|
52
|
+
easing: Easing.out(Easing.ease),
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The fall, not the rise, carries the character here: the source's `linear()`
|
|
57
|
+
* curve peaks at 1.364, so a dot dips 36.4% past its resting line before
|
|
58
|
+
* settling. A spring's first overshoot is exp(-piZ / sqrt(1 - Z^2)), which
|
|
59
|
+
* lands on 36.4% at a damping ratio of ~0.306 — 0.5 would reach only 16%,
|
|
60
|
+
* visibly flatter than drawn.
|
|
61
|
+
*
|
|
62
|
+
* `duration` matters as much as the ratio: a default spring settles whenever it
|
|
63
|
+
* settles, which would break the fixed-length cycle the two tracks share.
|
|
64
|
+
*/
|
|
65
|
+
const FALL = { duration: BOUNCE_MS, dampingRatio: 0.306 } as const;
|
|
66
|
+
|
|
67
|
+
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
68
|
+
|
|
69
|
+
export interface TypingDotsProps extends Omit<SvgProps, 'fill'> {
|
|
70
|
+
/**
|
|
71
|
+
* Dot colour. Overrides the `fill` SvgProps would otherwise carry, because
|
|
72
|
+
* that one lands on the root <Svg> where the dots' own group already shadows
|
|
73
|
+
* it — passing it there does nothing.
|
|
74
|
+
*
|
|
75
|
+
* Only the colour: the source animates `fill: rgba(26, 29, 34, a)` but never
|
|
76
|
+
* changes the RGB, so the alpha half is driven separately as `fillOpacity`.
|
|
77
|
+
*/
|
|
78
|
+
fill?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const Dot = memo(({ index }: { index: number }) => {
|
|
82
|
+
const reducedMotion = useReducedMotion();
|
|
83
|
+
const lift = useSharedValue(0);
|
|
84
|
+
const alpha = useSharedValue(ALPHAS[index % ALPHAS.length]!);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (reducedMotion) return;
|
|
88
|
+
|
|
89
|
+
// Two bounces per cycle. A dot's turn comes one BOUNCE_MS after the one
|
|
90
|
+
// before it, and that delay applies only to the first pass — every
|
|
91
|
+
// iteration runs exactly CYCLE_MS, so the offset holds instead of drifting.
|
|
92
|
+
lift.value = withDelay(
|
|
93
|
+
index * BOUNCE_MS,
|
|
94
|
+
withRepeat(
|
|
95
|
+
withSequence(
|
|
96
|
+
withTiming(-LIFT, RISE),
|
|
97
|
+
withSpring(0, FALL),
|
|
98
|
+
withTiming(0, { duration: HOLD_SHORT_MS }),
|
|
99
|
+
withTiming(-LIFT, RISE),
|
|
100
|
+
withSpring(0, FALL),
|
|
101
|
+
withTiming(0, { duration: HOLD_LONG_MS })
|
|
102
|
+
),
|
|
103
|
+
-1
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// The opacity wave needs no delay: each dot enters the cycle at a different
|
|
108
|
+
// alpha instead, which is how the source staggers it.
|
|
109
|
+
alpha.value = withRepeat(
|
|
110
|
+
withSequence(
|
|
111
|
+
...Array.from({ length: FADE_STEPS }, (_, step) =>
|
|
112
|
+
withTiming(ALPHAS[(index + step + 1) % ALPHAS.length]!, {
|
|
113
|
+
duration: FADE_STEP_MS,
|
|
114
|
+
easing: Easing.inOut(Easing.ease),
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
),
|
|
118
|
+
-1
|
|
119
|
+
);
|
|
120
|
+
}, [index, lift, alpha, reducedMotion]);
|
|
121
|
+
|
|
122
|
+
// Driven as SVG attributes rather than a style transform: react-native-svg
|
|
123
|
+
// maps these straight onto the native node, keeping the work off the JS
|
|
124
|
+
// thread. `y` also replaces the source's stacked translateY pair.
|
|
125
|
+
const animatedProps = useAnimatedProps(() => ({
|
|
126
|
+
y: DOT_Y + lift.value,
|
|
127
|
+
fillOpacity: alpha.value,
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<AnimatedRect
|
|
132
|
+
x={DOT_X[index]}
|
|
133
|
+
width={DOT_SIZE}
|
|
134
|
+
height={DOT_SIZE}
|
|
135
|
+
// The file says rx="50"; SVG clamps that to half the width. Stating the
|
|
136
|
+
// clamped value keeps the circle from depending on that behaviour.
|
|
137
|
+
rx={DOT_SIZE / 2}
|
|
138
|
+
animatedProps={animatedProps}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
Dot.displayName = 'TypingDots.Dot';
|
|
144
|
+
|
|
145
|
+
export const TypingDots = memo(
|
|
146
|
+
({
|
|
147
|
+
fill = DEFAULT_FILL,
|
|
148
|
+
width = VIEW_WIDTH,
|
|
149
|
+
height = VIEW_HEIGHT,
|
|
150
|
+
...props
|
|
151
|
+
}: TypingDotsProps) => (
|
|
152
|
+
// No `overflow="visible"` despite the source setting it: a dot runs from
|
|
153
|
+
// y=0 at the top of its bounce to y=16.23 at the bottom of the overshoot,
|
|
154
|
+
// so nothing leaves the 48x20 box. Leaning on overflow would be the fragile
|
|
155
|
+
// choice — react-native-svg does not honour it consistently on Android.
|
|
156
|
+
<Svg
|
|
157
|
+
width={width}
|
|
158
|
+
height={height}
|
|
159
|
+
viewBox={`0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`}
|
|
160
|
+
fill="none"
|
|
161
|
+
{...props}
|
|
162
|
+
>
|
|
163
|
+
<G fill={fill}>
|
|
164
|
+
{DOT_X.map((_, index) => (
|
|
165
|
+
<Dot key={index} index={index} />
|
|
166
|
+
))}
|
|
167
|
+
</G>
|
|
168
|
+
</Svg>
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
TypingDots.displayName = 'TypingDots';
|
|
@@ -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
|
+
};
|
|
@@ -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,6 +22,7 @@ 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';
|
|
27
28
|
import { ChatEvent } from '../../../types/events';
|
|
@@ -33,6 +34,9 @@ interface QuotedMessageProps extends BaseLegendMessageProps {
|
|
|
33
34
|
const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
34
35
|
const {
|
|
35
36
|
message,
|
|
37
|
+
messageId,
|
|
38
|
+
surface,
|
|
39
|
+
isPreview,
|
|
36
40
|
isOutgoing,
|
|
37
41
|
createdAtTime,
|
|
38
42
|
isHighlighted,
|
|
@@ -104,6 +108,8 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
|
104
108
|
<KContainer.Touchable
|
|
105
109
|
style={styles.quotedContainer}
|
|
106
110
|
onPress={handlePressQuotedMessage}
|
|
111
|
+
// Would scroll the timeline hidden behind the overlay.
|
|
112
|
+
disabled={isPreview}
|
|
107
113
|
>
|
|
108
114
|
<KContainer.View style={styles.dot} />
|
|
109
115
|
<KContainer.VisibleView visible={!!imageUrl}>
|
|
@@ -152,9 +158,15 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
|
|
|
152
158
|
</KLabel.Text>
|
|
153
159
|
</KContainer.View>
|
|
154
160
|
</KContainer.Touchable>
|
|
155
|
-
<
|
|
161
|
+
<ExpandableMessageText
|
|
162
|
+
messageId={messageId}
|
|
163
|
+
surface={surface}
|
|
164
|
+
text={replyText}
|
|
165
|
+
typo="TextMdNormal"
|
|
166
|
+
color={CHAT_BUBBLE_COLORS.text}
|
|
167
|
+
>
|
|
156
168
|
{renderLinkifiedText(replyText, urls, onPressUrl, 'reply')}
|
|
157
|
-
</
|
|
169
|
+
</ExpandableMessageText>
|
|
158
170
|
</KContainer.View>
|
|
159
171
|
</BubbleTailRow>
|
|
160
172
|
|
|
@@ -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;
|