@droppii-org/chat-mobile 0.2.107 → 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/fileMessage/index.js +9 -2
- package/lib/module/components/messages/fileMessage/index.js.map +1 -1
- package/lib/module/components/messages/quotedMessage/index.js +6 -1
- package/lib/module/components/messages/quotedMessage/index.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 +6 -1
- package/lib/module/hooks/useExpandableText.js.map +1 -1
- 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 +38 -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/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 +2 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- 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/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/perf-marker.js +44 -0
- package/lib/module/utils/perf-marker.js.map +1 -0
- package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/quotedMessage/index.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.map +1 -1
- 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/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.map +1 -1
- 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/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/perf-marker.d.ts +8 -0
- package/lib/typescript/src/utils/perf-marker.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/components/ThreadCard/ThreadCard.tsx +2 -0
- package/src/components/messages/fileMessage/index.tsx +17 -3
- package/src/components/messages/quotedMessage/index.tsx +5 -1
- 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 +5 -1
- package/src/hooks/useGuardedPress.ts +20 -0
- package/src/screens/chat-detail/ChatComposer.tsx +105 -631
- package/src/screens/chat-detail/ChatDetail.tsx +38 -20
- package/src/screens/chat-detail/ChatListLegend.tsx +122 -71
- 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 +2 -0
- package/src/screens/chat-detail/types.ts +9 -0
- package/src/store/message.ts +14 -0
- package/src/utils/composerPanelDismissGesture.ts +72 -0
- package/src/utils/perf-marker.ts +39 -0
|
@@ -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
|
+
]);
|
|
@@ -31,6 +31,7 @@ import type { GroupPosition } from '../../../utils/legendListMessage';
|
|
|
31
31
|
import type { MessageSurface } from '../../../store/expandedMessage';
|
|
32
32
|
import { Avatar } from '../../../components/Avatar';
|
|
33
33
|
import { TAIL_WIDTH } from '../../../components/messages/BubbleTail';
|
|
34
|
+
import { perfCountRender } from '../../../utils/perf-marker';
|
|
34
35
|
|
|
35
36
|
type MessageProps = BaseLegendMessageProps & {
|
|
36
37
|
onPressUrl?: (url: string) => void;
|
|
@@ -142,6 +143,7 @@ export const LegendChatMessage = memo(
|
|
|
142
143
|
surface = 'timeline',
|
|
143
144
|
isPreview,
|
|
144
145
|
}: LegendChatMessageProps) => {
|
|
146
|
+
perfCountRender('LegendChatMessage');
|
|
145
147
|
const handlePress = useCallback(() => {
|
|
146
148
|
onPress?.(messageId);
|
|
147
149
|
}, [onPress, messageId]);
|
|
@@ -107,3 +107,12 @@ export interface ChatDetailProps extends Omit<
|
|
|
107
107
|
groupJoinSource?: GroupJoinSource;
|
|
108
108
|
renderConversationViewHeader?: () => ReactNode;
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @internal Imperative handle to the composer's attachment/sticker panel,
|
|
113
|
+
* used by ChatDetail for tap-outside dismiss. Not part of the public API.
|
|
114
|
+
*/
|
|
115
|
+
export interface ComposerPanelController {
|
|
116
|
+
isOpen: () => boolean;
|
|
117
|
+
close: () => void;
|
|
118
|
+
}
|
package/src/store/message.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import { mergeMessages } from '../utils/message';
|
|
15
15
|
import { isMessageRevoked } from '../utils/messageUtils';
|
|
16
16
|
import { useConversationStore } from './conversation';
|
|
17
|
+
import { perfMark } from '../utils/perf-marker';
|
|
17
18
|
|
|
18
19
|
// Smaller than a typical "load more" page size elsewhere in the app on
|
|
19
20
|
// purpose: each page becomes a single prepend batch in ChatListLegend, and a
|
|
@@ -77,7 +78,16 @@ export const useMessageStore = create<MessageStore>()((set, get) => ({
|
|
|
77
78
|
viewType: ViewType.History,
|
|
78
79
|
applicationType: applicationType,
|
|
79
80
|
};
|
|
81
|
+
perfMark('history_page_request', {
|
|
82
|
+
loadMore: !!loadMore,
|
|
83
|
+
loaded: get().messages.length,
|
|
84
|
+
});
|
|
80
85
|
const res = await OpenIMSDK.getAdvancedHistoryMessageListApp(params);
|
|
86
|
+
perfMark('history_page_response', {
|
|
87
|
+
loadMore: !!loadMore,
|
|
88
|
+
received: res.messageList?.length ?? 0,
|
|
89
|
+
isEnd: !!res.isEnd,
|
|
90
|
+
});
|
|
81
91
|
if (
|
|
82
92
|
conversationId === useConversationStore.getState().currentConversationId
|
|
83
93
|
) {
|
|
@@ -89,6 +99,10 @@ export const useMessageStore = create<MessageStore>()((set, get) => ({
|
|
|
89
99
|
initLoading: false,
|
|
90
100
|
moreOldLoading: false,
|
|
91
101
|
}));
|
|
102
|
+
perfMark('history_list_update', {
|
|
103
|
+
loadMore: !!loadMore,
|
|
104
|
+
total: get().messages.length,
|
|
105
|
+
});
|
|
92
106
|
}
|
|
93
107
|
} catch (error) {
|
|
94
108
|
console.log('Failed to get more old messages', error);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-touch state for dismissing the composer's attachment/sticker panel by
|
|
3
|
+
* tapping the chat timeline: a touch that began while the panel was open and
|
|
4
|
+
* ended without dragging is a dismiss tap. Drag detection is explicit — a
|
|
5
|
+
* movement threshold or the list's scroll-begin — since the touch is only
|
|
6
|
+
* observed (touch events), never claimed as the JS responder.
|
|
7
|
+
*/
|
|
8
|
+
export const DISMISS_DRAG_THRESHOLD_PX = 10;
|
|
9
|
+
|
|
10
|
+
export interface DismissTouch {
|
|
11
|
+
panelOpenAtStart: boolean;
|
|
12
|
+
dragged: boolean;
|
|
13
|
+
startX: number;
|
|
14
|
+
startY: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const IDLE_DISMISS_TOUCH: DismissTouch = {
|
|
18
|
+
panelOpenAtStart: false,
|
|
19
|
+
dragged: false,
|
|
20
|
+
startX: 0,
|
|
21
|
+
startY: 0,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const startDismissTouch = (
|
|
25
|
+
panelOpen: boolean,
|
|
26
|
+
x: number,
|
|
27
|
+
y: number
|
|
28
|
+
): DismissTouch => ({
|
|
29
|
+
panelOpenAtStart: panelOpen,
|
|
30
|
+
dragged: false,
|
|
31
|
+
startX: x,
|
|
32
|
+
startY: y,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const moveDismissTouch = (
|
|
36
|
+
touch: DismissTouch,
|
|
37
|
+
x: number,
|
|
38
|
+
y: number
|
|
39
|
+
): DismissTouch => {
|
|
40
|
+
if (touch.dragged) return touch;
|
|
41
|
+
const movedBeyondThreshold =
|
|
42
|
+
Math.abs(x - touch.startX) > DISMISS_DRAG_THRESHOLD_PX ||
|
|
43
|
+
Math.abs(y - touch.startY) > DISMISS_DRAG_THRESHOLD_PX;
|
|
44
|
+
return movedBeyondThreshold ? { ...touch, dragged: true } : touch;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const markDismissTouchDragged = (touch: DismissTouch): DismissTouch =>
|
|
48
|
+
touch.dragged ? touch : { ...touch, dragged: true };
|
|
49
|
+
|
|
50
|
+
/** Whether releasing this touch should close the panel. */
|
|
51
|
+
export const shouldDismissOnRelease = (touch: DismissTouch): boolean =>
|
|
52
|
+
touch.panelOpenAtStart && !touch.dragged;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* How long after the finger lifts a press still belongs to the dismissing
|
|
56
|
+
* touch. RNGH Tap and JS onPress callbacks land right after touchEnd; a later
|
|
57
|
+
* press (e.g. a screen-reader activation, which sends no touch events) must
|
|
58
|
+
* not be swallowed by a stale flag.
|
|
59
|
+
*/
|
|
60
|
+
export const SWALLOW_AFTER_RELEASE_MS = 500;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a timeline press must be swallowed. `releasedAt` is null while the
|
|
64
|
+
* dismissing touch is still down.
|
|
65
|
+
*/
|
|
66
|
+
export const isPressSwallowed = (
|
|
67
|
+
swallow: boolean,
|
|
68
|
+
releasedAt: number | null,
|
|
69
|
+
now: number
|
|
70
|
+
): boolean =>
|
|
71
|
+
swallow &&
|
|
72
|
+
(releasedAt === null || now - releasedAt < SWALLOW_AFTER_RELEASE_MS);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in performance markers for the local perf framework (`performance/`).
|
|
3
|
+
*
|
|
4
|
+
* Internal, not exported from the package: only exampleLegacy (the perf harness) imports `enablePerfMarkers` directly.
|
|
5
|
+
* Disabled by default, so every call is a no-op in consumer apps. When enabled, markers are written as
|
|
6
|
+
* `[PERF] {"name":..,"t":<epoch ms>,...}` through console.log, which the perf runner reads from the device log.
|
|
7
|
+
* Durations are computed between markers on the device clock, so no host/device clock sync is needed.
|
|
8
|
+
*/
|
|
9
|
+
let enabled = false;
|
|
10
|
+
const FLUSH_INTERVAL_MS = 500;
|
|
11
|
+
let renderCounts: Record<string, number> = {};
|
|
12
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
|
|
14
|
+
export const enablePerfMarkers = (on = true) => {
|
|
15
|
+
enabled = on;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const perfMark = (name: string, data?: Record<string, unknown>) => {
|
|
19
|
+
if (!enabled) return;
|
|
20
|
+
console.log(`[PERF] ${JSON.stringify({ name, t: Date.now(), ...data })}`);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const flushRenderCounts = () => {
|
|
24
|
+
flushTimer = null;
|
|
25
|
+
const counts = renderCounts;
|
|
26
|
+
renderCounts = {};
|
|
27
|
+
perfMark('render_counts', { counts });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Counts render-function invocations of `component`, including renders React later discards.
|
|
32
|
+
* Counts are batched and emitted as a delta every ~500 ms, so logging never happens per render.
|
|
33
|
+
*/
|
|
34
|
+
export const perfCountRender = (component: string) => {
|
|
35
|
+
if (!enabled) return;
|
|
36
|
+
renderCounts[component] = (renderCounts[component] ?? 0) + 1;
|
|
37
|
+
if (!flushTimer)
|
|
38
|
+
flushTimer = setTimeout(flushRenderCounts, FLUSH_INTERVAL_MS);
|
|
39
|
+
};
|