@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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { GestureResponderEvent, ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ComposerPanelController } from '../types';
|
|
5
|
+
import {
|
|
6
|
+
IDLE_DISMISS_TOUCH,
|
|
7
|
+
isPressSwallowed,
|
|
8
|
+
markDismissTouchDragged,
|
|
9
|
+
moveDismissTouch,
|
|
10
|
+
shouldDismissOnRelease,
|
|
11
|
+
startDismissTouch,
|
|
12
|
+
type DismissTouch,
|
|
13
|
+
} from '../../../utils/composerPanelDismissGesture';
|
|
14
|
+
|
|
15
|
+
export type DismissAreaTouchProps = Pick<
|
|
16
|
+
ViewProps,
|
|
17
|
+
'onTouchStart' | 'onTouchMove' | 'onTouchEnd' | 'onTouchCancel'
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
// Tap-outside dismiss for the composer's attachment/sticker panel
|
|
21
|
+
// (DROPPII-30577).
|
|
22
|
+
//
|
|
23
|
+
// While a panel is open the timeline's content is made non-hittable
|
|
24
|
+
// (`isComposerPanelOpen` → `pointerEvents: 'none'` on the list content), so a
|
|
25
|
+
// tap can never reach a bubble — JS touchables and RNGH gestures alike — yet
|
|
26
|
+
// the scroll view itself still receives the touch and scrolls. Tap vs drag is
|
|
27
|
+
// read from plain touch events on a View wrapping the list; they observe the
|
|
28
|
+
// touch without claiming the JS responder, which on iOS would disable the
|
|
29
|
+
// scroll view's pan (RCTScrollView disables it while an ancestor holds the
|
|
30
|
+
// responder).
|
|
31
|
+
//
|
|
32
|
+
// `pointerEvents` only lands after a React round-trip (open → effect →
|
|
33
|
+
// setState → list re-render), so a tap right after opening a heavy panel can
|
|
34
|
+
// still reach a bubble. `shouldSwallowPress` closes that gap synchronously: it
|
|
35
|
+
// is true for every press belonging to a touch that began with the panel
|
|
36
|
+
// open — while it is down and for SWALLOW_AFTER_RELEASE_MS after it lifts,
|
|
37
|
+
// since RNGH gesture callbacks arrive after touchEnd.
|
|
38
|
+
export const useComposerPanelDismiss = () => {
|
|
39
|
+
const panelControllerRef = useRef<ComposerPanelController | null>(null);
|
|
40
|
+
const touchRef = useRef<DismissTouch>(IDLE_DISMISS_TOUCH);
|
|
41
|
+
const swallowPressRef = useRef(false);
|
|
42
|
+
// null while the tracked touch is down.
|
|
43
|
+
const releasedAtRef = useRef<number | null>(null);
|
|
44
|
+
const [isComposerPanelOpen, setIsComposerPanelOpen] = useState(false);
|
|
45
|
+
|
|
46
|
+
const dismissAreaTouchProps = useMemo<DismissAreaTouchProps>(
|
|
47
|
+
() => ({
|
|
48
|
+
onTouchStart: (event: GestureResponderEvent) => {
|
|
49
|
+
const panelOpen = !!panelControllerRef.current?.isOpen();
|
|
50
|
+
// A second finger must not restart the tracked touch, but a press it
|
|
51
|
+
// makes while the panel is open is still swallowed.
|
|
52
|
+
if (event.nativeEvent.touches.length > 1) {
|
|
53
|
+
if (panelOpen) swallowPressRef.current = true;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
touchRef.current = startDismissTouch(
|
|
57
|
+
panelOpen,
|
|
58
|
+
event.nativeEvent.pageX,
|
|
59
|
+
event.nativeEvent.pageY
|
|
60
|
+
);
|
|
61
|
+
swallowPressRef.current = panelOpen;
|
|
62
|
+
releasedAtRef.current = null;
|
|
63
|
+
},
|
|
64
|
+
onTouchMove: (event: GestureResponderEvent) => {
|
|
65
|
+
touchRef.current = moveDismissTouch(
|
|
66
|
+
touchRef.current,
|
|
67
|
+
event.nativeEvent.pageX,
|
|
68
|
+
event.nativeEvent.pageY
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
onTouchEnd: (event: GestureResponderEvent) => {
|
|
72
|
+
if (event.nativeEvent.touches.length > 0) return;
|
|
73
|
+
if (shouldDismissOnRelease(touchRef.current)) {
|
|
74
|
+
panelControllerRef.current?.close();
|
|
75
|
+
}
|
|
76
|
+
touchRef.current = IDLE_DISMISS_TOUCH;
|
|
77
|
+
releasedAtRef.current = Date.now();
|
|
78
|
+
},
|
|
79
|
+
// The scroll view took the touch over (pan began) — not a tap.
|
|
80
|
+
onTouchCancel: () => {
|
|
81
|
+
touchRef.current = IDLE_DISMISS_TOUCH;
|
|
82
|
+
releasedAtRef.current = Date.now();
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
[]
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// The list's onScrollBeginDrag — a drag signal on both platforms.
|
|
89
|
+
const notifyScrollBeginDrag = useCallback(() => {
|
|
90
|
+
touchRef.current = markDismissTouchDragged(touchRef.current);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const shouldSwallowPress = useCallback(
|
|
94
|
+
() =>
|
|
95
|
+
isPressSwallowed(
|
|
96
|
+
swallowPressRef.current,
|
|
97
|
+
releasedAtRef.current,
|
|
98
|
+
Date.now()
|
|
99
|
+
),
|
|
100
|
+
[]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
panelControllerRef,
|
|
105
|
+
isComposerPanelOpen,
|
|
106
|
+
onComposerPanelOpenChange: setIsComposerPanelOpen,
|
|
107
|
+
dismissAreaTouchProps,
|
|
108
|
+
notifyScrollBeginDrag,
|
|
109
|
+
shouldSwallowPress,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
type RefObject,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { Keyboard } from 'react-native';
|
|
10
|
+
|
|
11
|
+
import { useChatComposerAnimation } from './useChatComposerAnimation';
|
|
12
|
+
import type { ActivePanelType } from './useChatComposerState';
|
|
13
|
+
import type { ComposerPanelController } from '../types';
|
|
14
|
+
import { getAttachmentPanelHeight, DEFAULT_PANNEL_ACTIONS } from '../constants';
|
|
15
|
+
import { STICKER_PICKER_HEIGHT } from '../../../components/StickerPicker/constants';
|
|
16
|
+
|
|
17
|
+
// 'immediate' — no animation (sticker yielding to the keyboard);
|
|
18
|
+
// 'fast' — half-duration (attachment yielding to the keyboard);
|
|
19
|
+
// 'animated' — full duration, same as closing via the toggle icon.
|
|
20
|
+
export type ClosePanelMode = 'immediate' | 'fast' | 'animated';
|
|
21
|
+
|
|
22
|
+
interface UseComposerPanelsProps {
|
|
23
|
+
activePanelType: ActivePanelType;
|
|
24
|
+
setActivePanelType: (type: ActivePanelType) => void;
|
|
25
|
+
isAttachmentOpen: boolean;
|
|
26
|
+
setIsAttachmentOpen: (open: boolean) => void;
|
|
27
|
+
isMentionPanelOpen: boolean;
|
|
28
|
+
closeMentionPanel: () => void;
|
|
29
|
+
isKeyboardVisible: boolean;
|
|
30
|
+
// False while the accessory slot is unmounted or the input row is replaced
|
|
31
|
+
// by a status banner — a panel there is invisible and cannot be toggled.
|
|
32
|
+
isSlotVisible: boolean;
|
|
33
|
+
panelControllerRef?: RefObject<ComposerPanelController | null>;
|
|
34
|
+
// Effective open state (open AND visible), for the timeline's dismiss area.
|
|
35
|
+
onPanelOpenChange?: (open: boolean) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const useComposerPanels = ({
|
|
39
|
+
activePanelType,
|
|
40
|
+
setActivePanelType,
|
|
41
|
+
isAttachmentOpen,
|
|
42
|
+
setIsAttachmentOpen,
|
|
43
|
+
isMentionPanelOpen,
|
|
44
|
+
closeMentionPanel,
|
|
45
|
+
isKeyboardVisible,
|
|
46
|
+
isSlotVisible,
|
|
47
|
+
panelControllerRef,
|
|
48
|
+
onPanelOpenChange,
|
|
49
|
+
}: UseComposerPanelsProps) => {
|
|
50
|
+
const attachmentPanelHeight = useMemo(
|
|
51
|
+
() => getAttachmentPanelHeight(DEFAULT_PANNEL_ACTIONS.length),
|
|
52
|
+
[]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const panelHeight =
|
|
56
|
+
activePanelType === 'sticker'
|
|
57
|
+
? STICKER_PICKER_HEIGHT
|
|
58
|
+
: attachmentPanelHeight;
|
|
59
|
+
|
|
60
|
+
// Mirrors isAttachmentOpen synchronously (written before React commits),
|
|
61
|
+
// so imperative readers — the timeline's tap-outside dismiss — never see
|
|
62
|
+
// a stale value between an open/close and the next render.
|
|
63
|
+
const isAttachmentOpenRef = useRef(false);
|
|
64
|
+
const handleAttachmentOpenChange = useCallback(
|
|
65
|
+
(open: boolean) => {
|
|
66
|
+
isAttachmentOpenRef.current = open;
|
|
67
|
+
setIsAttachmentOpen(open);
|
|
68
|
+
},
|
|
69
|
+
[setIsAttachmentOpen]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const { accessorySlotStyle, openWithHeight, closeAttachment } =
|
|
73
|
+
useChatComposerAnimation({
|
|
74
|
+
panelHeight,
|
|
75
|
+
onAttachmentOpenChange: handleAttachmentOpenChange,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const closeActivePanel = useCallback(
|
|
79
|
+
(mode: ClosePanelMode) => {
|
|
80
|
+
closeAttachment(mode === 'fast', mode === 'immediate');
|
|
81
|
+
setActivePanelType(null);
|
|
82
|
+
},
|
|
83
|
+
[closeAttachment, setActivePanelType]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Close mode when yielding to the keyboard or the mention picker: the
|
|
87
|
+
// sticker panel closes immediately to avoid a layout conflict between the
|
|
88
|
+
// panel closing and the keyboard appearing; the attachment panel is small
|
|
89
|
+
// enough that animation is fine (speedUp=true).
|
|
90
|
+
const yieldingCloseMode: ClosePanelMode =
|
|
91
|
+
activePanelType === 'sticker' ? 'immediate' : 'fast';
|
|
92
|
+
|
|
93
|
+
// Mutual exclusion (finding #5 in the mention plan): the mention picker
|
|
94
|
+
// never renders through the attachment/sticker panel slot, but opening
|
|
95
|
+
// one must still close the other — starting a mention query while the
|
|
96
|
+
// attachment/sticker panel is open closes that panel.
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (isMentionPanelOpen && isAttachmentOpen) {
|
|
99
|
+
closeActivePanel(yieldingCloseMode);
|
|
100
|
+
}
|
|
101
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
+
}, [isMentionPanelOpen]);
|
|
103
|
+
|
|
104
|
+
// A panel left open when the slot disappears (composer disabled, bot
|
|
105
|
+
// restarted, removed from group) would otherwise stay "open" invisibly.
|
|
106
|
+
const isSlotVisibleRef = useRef(isSlotVisible);
|
|
107
|
+
isSlotVisibleRef.current = isSlotVisible;
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!isSlotVisible && isAttachmentOpenRef.current) {
|
|
110
|
+
closeActivePanel('immediate');
|
|
111
|
+
}
|
|
112
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
+
}, [isSlotVisible]);
|
|
114
|
+
|
|
115
|
+
const isPanelEffectivelyOpen = isAttachmentOpen && isSlotVisible;
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
onPanelOpenChange?.(isPanelEffectivelyOpen);
|
|
118
|
+
}, [isPanelEffectivelyOpen, onPanelOpenChange]);
|
|
119
|
+
|
|
120
|
+
useImperativeHandle(
|
|
121
|
+
panelControllerRef,
|
|
122
|
+
() => ({
|
|
123
|
+
isOpen: () => isAttachmentOpenRef.current && isSlotVisibleRef.current,
|
|
124
|
+
close: () => {
|
|
125
|
+
if (isAttachmentOpenRef.current) closeActivePanel('animated');
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
[closeActivePanel]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const handleToggleAttachment = useCallback(() => {
|
|
132
|
+
closeMentionPanel();
|
|
133
|
+
if (activePanelType === 'sticker') {
|
|
134
|
+
// switch from sticker to attachment
|
|
135
|
+
setActivePanelType('attachment');
|
|
136
|
+
openWithHeight(attachmentPanelHeight);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Open/close decided on the same source of truth as the icon: a
|
|
140
|
+
// toggle keyed on the animated height could close a panel that is
|
|
141
|
+
// still collapsing from a tap-outside dismiss while the icon shows it
|
|
142
|
+
// opening.
|
|
143
|
+
if (isAttachmentOpenRef.current) {
|
|
144
|
+
closeActivePanel('animated');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (isKeyboardVisible) {
|
|
148
|
+
Keyboard.dismiss();
|
|
149
|
+
}
|
|
150
|
+
setActivePanelType('attachment');
|
|
151
|
+
openWithHeight(attachmentPanelHeight);
|
|
152
|
+
// Deliberately depend on `closeMentionPanel` (stable — [] deps in
|
|
153
|
+
// useMentionComposer), NOT the whole `mentionComposer` object, which is
|
|
154
|
+
// a fresh literal every render (useMentionComposer doesn't memoize its
|
|
155
|
+
// return value). Depending on the whole object recreated this callback
|
|
156
|
+
// — and therefore every prop built from it, e.g. ChatMessageInput's
|
|
157
|
+
// onEmojiPress — on every single render, which contributed to a real
|
|
158
|
+
// "Maximum update depth exceeded" loop found in testing.
|
|
159
|
+
}, [
|
|
160
|
+
activePanelType,
|
|
161
|
+
attachmentPanelHeight,
|
|
162
|
+
isKeyboardVisible,
|
|
163
|
+
openWithHeight,
|
|
164
|
+
closeActivePanel,
|
|
165
|
+
setActivePanelType,
|
|
166
|
+
closeMentionPanel,
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const handleToggleSticker = useCallback(() => {
|
|
170
|
+
closeMentionPanel();
|
|
171
|
+
if (activePanelType === 'sticker') {
|
|
172
|
+
closeActivePanel('animated');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (isKeyboardVisible) {
|
|
176
|
+
Keyboard.dismiss();
|
|
177
|
+
}
|
|
178
|
+
setActivePanelType('sticker');
|
|
179
|
+
openWithHeight(STICKER_PICKER_HEIGHT);
|
|
180
|
+
// Same reasoning as handleToggleAttachment above — see that comment.
|
|
181
|
+
}, [
|
|
182
|
+
activePanelType,
|
|
183
|
+
isKeyboardVisible,
|
|
184
|
+
closeMentionPanel,
|
|
185
|
+
closeActivePanel,
|
|
186
|
+
openWithHeight,
|
|
187
|
+
setActivePanelType,
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
accessorySlotStyle,
|
|
192
|
+
closeActivePanel,
|
|
193
|
+
yieldingCloseMode,
|
|
194
|
+
handleToggleAttachment,
|
|
195
|
+
handleToggleSticker,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useCallback, type RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
import { trans } from '../../../translation';
|
|
4
|
+
import { UIUtils } from '../../../utils/UIUtils';
|
|
5
|
+
import {
|
|
6
|
+
serializeMentionsForSend,
|
|
7
|
+
MAX_AT_USER_COUNT,
|
|
8
|
+
} from '../../../utils/mentionSerializer';
|
|
9
|
+
import type { useChatCompose } from '../../../hooks/useChatCompose';
|
|
10
|
+
import type { useMentionComposer } from './useMentionComposer';
|
|
11
|
+
|
|
12
|
+
type Mentions = ReturnType<typeof useMentionComposer>['mentions'];
|
|
13
|
+
|
|
14
|
+
interface UseComposerSendProps {
|
|
15
|
+
compose: ReturnType<typeof useChatCompose>;
|
|
16
|
+
isSendMessageAllowed: boolean;
|
|
17
|
+
sendDeniedReason?: string;
|
|
18
|
+
mentions: Mentions;
|
|
19
|
+
clearMentions: () => void;
|
|
20
|
+
notifyStoppedTyping: () => void;
|
|
21
|
+
inputRef: RefObject<any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const useComposerSend = ({
|
|
25
|
+
compose,
|
|
26
|
+
isSendMessageAllowed,
|
|
27
|
+
sendDeniedReason,
|
|
28
|
+
mentions,
|
|
29
|
+
clearMentions,
|
|
30
|
+
notifyStoppedTyping,
|
|
31
|
+
inputRef,
|
|
32
|
+
}: UseComposerSendProps) =>
|
|
33
|
+
useCallback(async () => {
|
|
34
|
+
if (!isSendMessageAllowed) {
|
|
35
|
+
UIUtils.toast.open({
|
|
36
|
+
title: sendDeniedReason,
|
|
37
|
+
theme: 'info',
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (compose.isUploading || !compose.canSend) return;
|
|
43
|
+
const mentionsToSend = mentions;
|
|
44
|
+
|
|
45
|
+
// OpenIM's Go core hard-rejects an AtTextMessage tagging more than
|
|
46
|
+
// MAX_AT_USER_COUNT users, so serializeMentionsForSend caps
|
|
47
|
+
// atUserIDList to the first MAX_AT_USER_COUNT (mentionSerializer.ts's
|
|
48
|
+
// doc comment) — the message still sends, with every mention still
|
|
49
|
+
// highlighted in the text, just only the first MAX_AT_USER_COUNT
|
|
50
|
+
// actually get natively tagged/notified. This is a heads-up, not a
|
|
51
|
+
// block, so no `return` here.
|
|
52
|
+
if (mentionsToSend.length > 0) {
|
|
53
|
+
const { truncated } = serializeMentionsForSend(mentionsToSend);
|
|
54
|
+
if (truncated) {
|
|
55
|
+
UIUtils.toast.open({
|
|
56
|
+
title: trans('group:tag_limit_exceeded', {
|
|
57
|
+
max: String(MAX_AT_USER_COUNT),
|
|
58
|
+
}),
|
|
59
|
+
theme: 'info',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Clear mention state synchronously, in the same tick as
|
|
65
|
+
// compose.send() (which clears `text`) — otherwise there's a real
|
|
66
|
+
// transient render where `value` is already '' but `mentions` still
|
|
67
|
+
// holds the just-sent entries, and the composer can visibly fail to
|
|
68
|
+
// clear (ChatMessageInput stays in children-controlled mode with a
|
|
69
|
+
// "ghost" mention anchored past the end of an empty string). See
|
|
70
|
+
// useMentionComposer.clear()'s doc comment.
|
|
71
|
+
clearMentions();
|
|
72
|
+
// Stop before awaiting the send: the peer should lose the indicator as
|
|
73
|
+
// the message goes out, not once it round-trips.
|
|
74
|
+
notifyStoppedTyping();
|
|
75
|
+
try {
|
|
76
|
+
await compose.send(mentionsToSend);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (__DEV__) console.error('[ChatComposer] Failed to send:', error);
|
|
79
|
+
} finally {
|
|
80
|
+
// Belt-and-suspenders: force the native view to actually clear,
|
|
81
|
+
// regardless of whether the value/children mode-switch (triggered
|
|
82
|
+
// by mentions going from non-empty to empty) synced correctly on
|
|
83
|
+
// its own — the underlying nested-Text-in-TextInput technique is
|
|
84
|
+
// known to have platform quirks around this kind of transition.
|
|
85
|
+
inputRef.current?.clear();
|
|
86
|
+
}
|
|
87
|
+
// Depend on mentionComposer.mentions/.clear specifically (both
|
|
88
|
+
// stable-per-value / [] deps), not the whole mentionComposer object —
|
|
89
|
+
// same reasoning as handleToggleAttachment in useComposerPanels.
|
|
90
|
+
}, [
|
|
91
|
+
compose,
|
|
92
|
+
isSendMessageAllowed,
|
|
93
|
+
sendDeniedReason,
|
|
94
|
+
mentions,
|
|
95
|
+
clearMentions,
|
|
96
|
+
notifyStoppedTyping,
|
|
97
|
+
inputRef,
|
|
98
|
+
]);
|
|
@@ -28,8 +28,10 @@ import QuotedMessage from '../../../components/messages/quotedMessage';
|
|
|
28
28
|
import { StickerMessage } from '../../../components/messages/stickerMessage';
|
|
29
29
|
import { GROUP_NOTIFICATION_TYPES } from '../../../utils/messageUtils';
|
|
30
30
|
import type { GroupPosition } from '../../../utils/legendListMessage';
|
|
31
|
+
import type { MessageSurface } from '../../../store/expandedMessage';
|
|
31
32
|
import { Avatar } from '../../../components/Avatar';
|
|
32
33
|
import { TAIL_WIDTH } from '../../../components/messages/BubbleTail';
|
|
34
|
+
import { perfCountRender } from '../../../utils/perf-marker';
|
|
33
35
|
|
|
34
36
|
type MessageProps = BaseLegendMessageProps & {
|
|
35
37
|
onPressUrl?: (url: string) => void;
|
|
@@ -72,6 +74,15 @@ interface LegendChatMessageProps {
|
|
|
72
74
|
showSenderGutter?: boolean;
|
|
73
75
|
/** Position within its same-sender cluster — drives the bubble tail. */
|
|
74
76
|
groupPosition?: GroupPosition;
|
|
77
|
+
/**
|
|
78
|
+
* Which surface is rendering this message. Expansion state is scoped by it
|
|
79
|
+
* (BR-003), so Pin Management must pass `'pin'` explicitly — the default
|
|
80
|
+
* keeps the timeline and the toolbox overlay sharing one scope, which is
|
|
81
|
+
* what makes the overlay clone mirror the row it lifted from.
|
|
82
|
+
*/
|
|
83
|
+
surface?: MessageSurface;
|
|
84
|
+
/** Toolbox overlay clone — see `BaseLegendMessageProps.isPreview`. */
|
|
85
|
+
isPreview?: boolean;
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
const messageComponentMap: Partial<
|
|
@@ -129,7 +140,10 @@ export const LegendChatMessage = memo(
|
|
|
129
140
|
showAvatar = false,
|
|
130
141
|
showSenderGutter = false,
|
|
131
142
|
groupPosition,
|
|
143
|
+
surface = 'timeline',
|
|
144
|
+
isPreview,
|
|
132
145
|
}: LegendChatMessageProps) => {
|
|
146
|
+
perfCountRender('LegendChatMessage');
|
|
133
147
|
const handlePress = useCallback(() => {
|
|
134
148
|
onPress?.(messageId);
|
|
135
149
|
}, [onPress, messageId]);
|
|
@@ -151,6 +165,8 @@ export const LegendChatMessage = memo(
|
|
|
151
165
|
<KContainer.View center style={styles.systemMessageWrapper}>
|
|
152
166
|
<SystemMessageComponent
|
|
153
167
|
message={message}
|
|
168
|
+
messageId={messageId}
|
|
169
|
+
surface={surface}
|
|
154
170
|
isOutgoing={isOutgoing}
|
|
155
171
|
createdAtTime={createdAtTime}
|
|
156
172
|
/>
|
|
@@ -210,12 +226,18 @@ export const LegendChatMessage = memo(
|
|
|
210
226
|
{message.senderNickname}
|
|
211
227
|
</KLabel.Text>
|
|
212
228
|
)}
|
|
229
|
+
{/* No gestures on the overlay clone: a Tap here would race
|
|
230
|
+
`Xem thêm` for the same touch, and the overlay has its own
|
|
231
|
+
tap-to-dismiss around the clone. */}
|
|
213
232
|
<LongPressWrapper
|
|
214
|
-
onPress={shouldHandlePress ? handlePress : undefined}
|
|
215
|
-
onLongPress={handleLongPress}
|
|
233
|
+
onPress={shouldHandlePress && !isPreview ? handlePress : undefined}
|
|
234
|
+
onLongPress={isPreview ? undefined : handleLongPress}
|
|
216
235
|
>
|
|
217
236
|
<MessageComponent
|
|
218
237
|
message={message}
|
|
238
|
+
messageId={messageId}
|
|
239
|
+
surface={surface}
|
|
240
|
+
isPreview={isPreview}
|
|
219
241
|
isOutgoing={isOutgoing}
|
|
220
242
|
createdAtTime={createdAtTime}
|
|
221
243
|
onMediaPress={hasMediaPress ? effectiveOnMediaPress : undefined}
|
|
@@ -29,6 +29,7 @@ import { useSendMessage } from '../../../hooks/message/useSendMessage';
|
|
|
29
29
|
import { useChatContext, useChatDetailContext } from '../../../context';
|
|
30
30
|
import {
|
|
31
31
|
GroupPermission,
|
|
32
|
+
MessageType,
|
|
32
33
|
type MessageItem,
|
|
33
34
|
} from '@droppii/openim-rn-client-sdk';
|
|
34
35
|
import { useHasGroupPermission } from '../../../hooks/useGroupPermission';
|
|
@@ -44,6 +45,13 @@ import type { GroupPosition } from '../../../utils/legendListMessage';
|
|
|
44
45
|
// scrollTo. See `show`/`hide` below for the observed sequence.
|
|
45
46
|
const UNFREEZE_DELAY_MS = 300;
|
|
46
47
|
|
|
48
|
+
const EXPANDABLE_TYPES: number[] = [
|
|
49
|
+
MessageType.TextMessage,
|
|
50
|
+
MessageType.AtTextMessage,
|
|
51
|
+
MessageType.UrlTextMessage,
|
|
52
|
+
MessageType.QuoteMessage,
|
|
53
|
+
];
|
|
54
|
+
|
|
47
55
|
interface MessageToolboxOverlayProps {
|
|
48
56
|
currentUserId?: string;
|
|
49
57
|
customMessageItemFactory?: (item: MessageItem) => ReactNode;
|
|
@@ -209,11 +217,19 @@ export const MessageToolboxOverlay = forwardRef<
|
|
|
209
217
|
hide();
|
|
210
218
|
}, [message, hide, setQuotedMessage]);
|
|
211
219
|
|
|
220
|
+
const overlayMessageId = message?.clientMsgID || message?.serverMsgID || '';
|
|
221
|
+
|
|
212
222
|
if (!visible || !message || !rect) {
|
|
213
223
|
return null;
|
|
214
224
|
}
|
|
215
225
|
|
|
216
226
|
const isOutgoing = message.sendID === currentUserId;
|
|
227
|
+
// Only the types that can show `Xem thêm` get a touchable clone. File
|
|
228
|
+
// download and consumer custom nodes carry their own handlers and stay
|
|
229
|
+
// inert.
|
|
230
|
+
const isCloneInteractive =
|
|
231
|
+
EXPANDABLE_TYPES.includes(message.contentType) &&
|
|
232
|
+
!customMessageItemFactory?.(message);
|
|
217
233
|
const showSenderGutter = isGroupConversation && !isOutgoing;
|
|
218
234
|
const showName =
|
|
219
235
|
showSenderGutter &&
|
|
@@ -235,10 +251,13 @@ export const MessageToolboxOverlay = forwardRef<
|
|
|
235
251
|
<MessageToolboxWrapper
|
|
236
252
|
rect={rect}
|
|
237
253
|
isOutgoing={isOutgoing}
|
|
254
|
+
isCloneInteractive={isCloneInteractive}
|
|
255
|
+
onDismiss={hide}
|
|
238
256
|
message={
|
|
239
257
|
<LegendChatMessage
|
|
240
258
|
message={message}
|
|
241
|
-
messageId={
|
|
259
|
+
messageId={overlayMessageId}
|
|
260
|
+
isPreview
|
|
242
261
|
isOutgoing={isOutgoing}
|
|
243
262
|
createdAtTime={message.sendTime || message.createTime}
|
|
244
263
|
customMessageItemFactory={customMessageItemFactory}
|