@droppii-org/chat-mobile 0.2.81 → 0.2.83
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/MentionBadge.js +24 -0
- package/lib/module/components/ThreadCard/MentionBadge.js.map +1 -0
- package/lib/module/components/ThreadCard/ThreadCard.js +5 -1
- package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
- package/lib/module/components/messages/linkMessage/LinkPreviewCard.js +67 -0
- package/lib/module/components/messages/linkMessage/LinkPreviewCard.js.map +1 -0
- package/lib/module/components/messages/linkMessage/index.js +49 -92
- package/lib/module/components/messages/linkMessage/index.js.map +1 -1
- package/lib/module/components/messages/quotedMessage/QuotedMessagePreviewStrip.js +152 -0
- package/lib/module/components/messages/quotedMessage/QuotedMessagePreviewStrip.js.map +1 -0
- package/lib/module/components/messages/quotedMessage/index.js +79 -55
- package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
- package/lib/module/components/messages/stickerMessage/index.js +38 -13
- package/lib/module/components/messages/stickerMessage/index.js.map +1 -1
- package/lib/module/components/messages/textMessage/index.js +126 -13
- 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 +1 -1
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/message/useSendMessage.js +64 -3
- package/lib/module/hooks/message/useSendMessage.js.map +1 -1
- package/lib/module/hooks/query-keys.js +4 -0
- package/lib/module/hooks/query-keys.js.map +1 -1
- package/lib/module/hooks/useChatCompose.js +3 -2
- package/lib/module/hooks/useChatCompose.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +170 -48
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +16 -7
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/components/ChatMessageInput.js +23 -2
- package/lib/module/screens/chat-detail/components/ChatMessageInput.js.map +1 -1
- package/lib/module/screens/chat-detail/components/MentionHighlightAdapter.js +51 -0
- package/lib/module/screens/chat-detail/components/MentionHighlightAdapter.js.map +1 -0
- package/lib/module/screens/chat-detail/components/MentionSuggestionItem.js +49 -0
- package/lib/module/screens/chat-detail/components/MentionSuggestionItem.js.map +1 -0
- package/lib/module/screens/chat-detail/components/MentionSuggestionList.js +114 -0
- package/lib/module/screens/chat-detail/components/MentionSuggestionList.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +8 -0
- package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useMentionComposer.js +251 -0
- package/lib/module/screens/chat-detail/hooks/useMentionComposer.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatDay.js +4 -9
- package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +14 -2
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/services/mentionSearch.js +49 -0
- package/lib/module/services/mentionSearch.js.map +1 -0
- package/lib/module/services/message.js +6 -1
- package/lib/module/services/message.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +5 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/types/mention.js +2 -0
- package/lib/module/types/mention.js.map +1 -0
- package/lib/module/utils/legendListMessage.js +9 -1
- package/lib/module/utils/legendListMessage.js.map +1 -1
- package/lib/module/utils/mentionSerializer.js +113 -0
- package/lib/module/utils/mentionSerializer.js.map +1 -0
- package/lib/module/utils/mentions/index.js +7 -0
- package/lib/module/utils/mentions/index.js.map +1 -0
- package/lib/module/utils/mentions/mentionDetector.js +49 -0
- package/lib/module/utils/mentions/mentionDetector.js.map +1 -0
- package/lib/module/utils/mentions/mentionDiff.js +66 -0
- package/lib/module/utils/mentions/mentionDiff.js.map +1 -0
- package/lib/module/utils/mentions/mentionRange.js +188 -0
- package/lib/module/utils/mentions/mentionRange.js.map +1 -0
- package/lib/module/utils/mentions/mentionTokenize.js +50 -0
- package/lib/module/utils/mentions/mentionTokenize.js.map +1 -0
- package/lib/module/utils/messageUtils.js +6 -1
- package/lib/module/utils/messageUtils.js.map +1 -1
- package/lib/module/utils/url.js +33 -0
- package/lib/module/utils/url.js.map +1 -1
- package/lib/typescript/src/components/ThreadCard/MentionBadge.d.ts +7 -0
- package/lib/typescript/src/components/ThreadCard/MentionBadge.d.ts.map +1 -0
- package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/linkMessage/LinkPreviewCard.d.ts +16 -0
- package/lib/typescript/src/components/messages/linkMessage/LinkPreviewCard.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts +2 -2
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.d.ts +13 -0
- package/lib/typescript/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +2 -2
- package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts +4 -2
- package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/types.d.ts +1 -0
- package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts +5 -2
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/src/hooks/query-keys.d.ts +4 -0
- package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useChatCompose.d.ts +2 -1
- package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.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/components/ChatMessageInput.d.ts +10 -2
- package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/components/MentionHighlightAdapter.d.ts +29 -0
- package/lib/typescript/src/screens/chat-detail/components/MentionHighlightAdapter.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionItem.d.ts +8 -0
- package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionItem.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionList.d.ts +12 -0
- package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionList.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useMentionComposer.d.ts +48 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useMentionComposer.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/services/mentionSearch.d.ts +15 -0
- package/lib/typescript/src/services/mentionSearch.d.ts.map +1 -0
- package/lib/typescript/src/services/message.d.ts +1 -1
- package/lib/typescript/src/services/message.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +23 -0
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/types/mention.d.ts +65 -0
- package/lib/typescript/src/types/mention.d.ts.map +1 -0
- package/lib/typescript/src/utils/legendListMessage.d.ts +4 -3
- package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
- package/lib/typescript/src/utils/mentionSerializer.d.ts +78 -0
- package/lib/typescript/src/utils/mentionSerializer.d.ts.map +1 -0
- package/lib/typescript/src/utils/mentions/index.d.ts +5 -0
- package/lib/typescript/src/utils/mentions/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/mentions/mentionDetector.d.ts +18 -0
- package/lib/typescript/src/utils/mentions/mentionDetector.d.ts.map +1 -0
- package/lib/typescript/src/utils/mentions/mentionDiff.d.ts +20 -0
- package/lib/typescript/src/utils/mentions/mentionDiff.d.ts.map +1 -0
- package/lib/typescript/src/utils/mentions/mentionRange.d.ts +86 -0
- package/lib/typescript/src/utils/mentions/mentionRange.d.ts.map +1 -0
- package/lib/typescript/src/utils/mentions/mentionTokenize.d.ts +29 -0
- package/lib/typescript/src/utils/mentions/mentionTokenize.d.ts.map +1 -0
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
- package/lib/typescript/src/utils/url.d.ts +12 -0
- package/lib/typescript/src/utils/url.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ThreadCard/MentionBadge.tsx +28 -0
- package/src/components/ThreadCard/ThreadCard.tsx +3 -0
- package/src/components/messages/linkMessage/LinkPreviewCard.tsx +92 -0
- package/src/components/messages/linkMessage/index.tsx +64 -121
- package/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.tsx +168 -0
- package/src/components/messages/quotedMessage/index.tsx +95 -64
- package/src/components/messages/stickerMessage/index.tsx +40 -11
- package/src/components/messages/textMessage/index.tsx +159 -17
- package/src/components/messages/types.ts +1 -0
- package/src/core/useChatListener.ts +1 -1
- package/src/hooks/message/useSendMessage.ts +60 -1
- package/src/hooks/query-keys.ts +6 -0
- package/src/hooks/useChatCompose.ts +115 -107
- package/src/screens/chat-detail/ChatComposer.tsx +201 -67
- package/src/screens/chat-detail/ChatListLegend.tsx +19 -4
- package/src/screens/chat-detail/components/ChatMessageInput.tsx +35 -2
- package/src/screens/chat-detail/components/MentionHighlightAdapter.tsx +57 -0
- package/src/screens/chat-detail/components/MentionSuggestionItem.tsx +53 -0
- package/src/screens/chat-detail/components/MentionSuggestionList.tsx +133 -0
- package/src/screens/chat-detail/hooks/useChatComposerState.ts +7 -1
- package/src/screens/chat-detail/hooks/useMentionComposer.ts +344 -0
- package/src/screens/chat-detail/legend/LegendChatDay.tsx +6 -9
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +25 -2
- package/src/services/mentionSearch.ts +57 -0
- package/src/services/message.ts +9 -1
- package/src/translation/resources/i18n.ts +6 -0
- package/src/types/chat.ts +22 -0
- package/src/types/mention.ts +70 -0
- package/src/utils/legendListMessage.ts +13 -4
- package/src/utils/mentionSerializer.ts +155 -0
- package/src/utils/mentions/index.ts +16 -0
- package/src/utils/mentions/mentionDetector.ts +58 -0
- package/src/utils/mentions/mentionDiff.ts +95 -0
- package/src/utils/mentions/mentionRange.ts +207 -0
- package/src/utils/mentions/mentionTokenize.ts +53 -0
- package/src/utils/messageUtils.ts +6 -1
- package/src/utils/url.ts +30 -0
|
@@ -66,6 +66,13 @@ import { UIUtils } from '../../utils/UIUtils';
|
|
|
66
66
|
import { PeerType } from '@droppii/openim-rn-client-sdk';
|
|
67
67
|
import { useCanSendMessage } from '../../hooks/useGroupPermission';
|
|
68
68
|
import { useIsJoinedGroup } from '../../hooks/useIsJoinedGroup';
|
|
69
|
+
import { useMentionComposer } from './hooks/useMentionComposer';
|
|
70
|
+
import { MentionSuggestionList } from './components/MentionSuggestionList';
|
|
71
|
+
import { createOpenIMMentionSearchSource } from '../../services/mentionSearch';
|
|
72
|
+
import {
|
|
73
|
+
serializeMentionsForSend,
|
|
74
|
+
MAX_AT_USER_COUNT,
|
|
75
|
+
} from '../../utils/mentionSerializer';
|
|
69
76
|
|
|
70
77
|
interface ChatComposerInternalProps {
|
|
71
78
|
isEmptyMessage?: boolean;
|
|
@@ -125,6 +132,28 @@ export const ChatComposer = memo(
|
|
|
125
132
|
inputRef,
|
|
126
133
|
} = useChatComposerState(compose.text);
|
|
127
134
|
|
|
135
|
+
const mentionSearchSource = useMemo(
|
|
136
|
+
() =>
|
|
137
|
+
conversation?.groupID
|
|
138
|
+
? createOpenIMMentionSearchSource(conversation.groupID)
|
|
139
|
+
: null,
|
|
140
|
+
[conversation?.groupID]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const mentionComposer = useMentionComposer({
|
|
144
|
+
text: compose.text,
|
|
145
|
+
setText: compose.setText,
|
|
146
|
+
// Non-null asserted only where actually invoked — the composer isn't
|
|
147
|
+
// rendered for non-group conversations in the first place (isGroupConversation
|
|
148
|
+
// below), so groupID is always present by the time a search fires.
|
|
149
|
+
searchSource: mentionSearchSource ?? {
|
|
150
|
+
search: async () => ({ items: [], hasMore: false }),
|
|
151
|
+
},
|
|
152
|
+
groupID: conversation?.groupID,
|
|
153
|
+
enabled: isGroupConversation,
|
|
154
|
+
inputRef,
|
|
155
|
+
});
|
|
156
|
+
|
|
128
157
|
const attachmentPanelHeight = useMemo(
|
|
129
158
|
() => getAttachmentPanelHeight(DEFAULT_PANNEL_ACTIONS.length),
|
|
130
159
|
[]
|
|
@@ -145,6 +174,19 @@ export const ChatComposer = memo(
|
|
|
145
174
|
onAttachmentOpenChange: setIsAttachmentOpen,
|
|
146
175
|
});
|
|
147
176
|
|
|
177
|
+
// Mutual exclusion (finding #5 in the mention plan): the mention picker
|
|
178
|
+
// never renders through the attachment/sticker panel slot, but opening
|
|
179
|
+
// one must still close the other — starting a mention query while the
|
|
180
|
+
// attachment/sticker panel is open closes that panel.
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (mentionComposer.isPanelOpen && isAttachmentOpen) {
|
|
183
|
+
const immediate = activePanelType === 'sticker';
|
|
184
|
+
closeAttachment(!immediate, immediate);
|
|
185
|
+
setActivePanelType(null);
|
|
186
|
+
}
|
|
187
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
188
|
+
}, [mentionComposer.isPanelOpen]);
|
|
189
|
+
|
|
148
190
|
const handleSend = useCallback(async () => {
|
|
149
191
|
if (!isSendMessageAllowed) {
|
|
150
192
|
UIUtils.toast.open({
|
|
@@ -155,12 +197,59 @@ export const ChatComposer = memo(
|
|
|
155
197
|
}
|
|
156
198
|
|
|
157
199
|
if (compose.isUploading || !compose.canSend) return;
|
|
200
|
+
const mentionsToSend = mentionComposer.mentions;
|
|
201
|
+
|
|
202
|
+
// OpenIM's Go core hard-rejects an AtTextMessage tagging more than
|
|
203
|
+
// MAX_AT_USER_COUNT users, so serializeMentionsForSend caps
|
|
204
|
+
// atUserIDList to the first MAX_AT_USER_COUNT (mentionSerializer.ts's
|
|
205
|
+
// doc comment) — the message still sends, with every mention still
|
|
206
|
+
// highlighted in the text, just only the first MAX_AT_USER_COUNT
|
|
207
|
+
// actually get natively tagged/notified. This is a heads-up, not a
|
|
208
|
+
// block, so no `return` here.
|
|
209
|
+
if (mentionsToSend.length > 0) {
|
|
210
|
+
const { truncated } = serializeMentionsForSend(mentionsToSend);
|
|
211
|
+
if (truncated) {
|
|
212
|
+
UIUtils.toast.open({
|
|
213
|
+
title: trans('group:tag_limit_exceeded', {
|
|
214
|
+
max: String(MAX_AT_USER_COUNT),
|
|
215
|
+
}),
|
|
216
|
+
theme: 'info',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Clear mention state synchronously, in the same tick as
|
|
222
|
+
// compose.send() (which clears `text`) — otherwise there's a real
|
|
223
|
+
// transient render where `value` is already '' but `mentions` still
|
|
224
|
+
// holds the just-sent entries, and the composer can visibly fail to
|
|
225
|
+
// clear (ChatMessageInput stays in children-controlled mode with a
|
|
226
|
+
// "ghost" mention anchored past the end of an empty string). See
|
|
227
|
+
// useMentionComposer.clear()'s doc comment.
|
|
228
|
+
mentionComposer.clear();
|
|
158
229
|
try {
|
|
159
|
-
await compose.send();
|
|
230
|
+
await compose.send(mentionsToSend);
|
|
160
231
|
} catch (error) {
|
|
161
232
|
if (__DEV__) console.error('[ChatComposer] Failed to send:', error);
|
|
233
|
+
} finally {
|
|
234
|
+
// Belt-and-suspenders: force the native view to actually clear,
|
|
235
|
+
// regardless of whether the value/children mode-switch (triggered
|
|
236
|
+
// by mentions going from non-empty to empty) synced correctly on
|
|
237
|
+
// its own — the underlying nested-Text-in-TextInput technique is
|
|
238
|
+
// known to have platform quirks around this kind of transition.
|
|
239
|
+
inputRef.current?.clear();
|
|
162
240
|
}
|
|
163
|
-
|
|
241
|
+
// Depend on mentionComposer.mentions/.clear specifically (both
|
|
242
|
+
// stable-per-value / [] deps), not the whole mentionComposer object —
|
|
243
|
+
// same reasoning as handleToggleAttachment above.
|
|
244
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
245
|
+
}, [
|
|
246
|
+
compose,
|
|
247
|
+
isSendMessageAllowed,
|
|
248
|
+
sendDeniedReason,
|
|
249
|
+
mentionComposer.mentions,
|
|
250
|
+
mentionComposer.clear,
|
|
251
|
+
inputRef,
|
|
252
|
+
]);
|
|
164
253
|
|
|
165
254
|
const handleInputFocus = useCallback(() => {
|
|
166
255
|
setIsInputFocused(true);
|
|
@@ -181,6 +270,7 @@ export const ChatComposer = memo(
|
|
|
181
270
|
]);
|
|
182
271
|
|
|
183
272
|
const handleToggleAttachment = useCallback(() => {
|
|
273
|
+
mentionComposer.closeMentionPanel();
|
|
184
274
|
if (activePanelType === 'sticker') {
|
|
185
275
|
// switch from sticker to attachment
|
|
186
276
|
setActivePanelType('attachment');
|
|
@@ -189,6 +279,16 @@ export const ChatComposer = memo(
|
|
|
189
279
|
}
|
|
190
280
|
setActivePanelType(isAttachmentOpen ? null : 'attachment');
|
|
191
281
|
toggleAttachmentVisibility();
|
|
282
|
+
// Deliberately depend on `mentionComposer.closeMentionPanel` (stable —
|
|
283
|
+
// [] deps in useMentionComposer), NOT the whole `mentionComposer`
|
|
284
|
+
// object, which is a fresh literal every render (useMentionComposer
|
|
285
|
+
// doesn't memoize its return value). Depending on the whole object
|
|
286
|
+
// recreated this callback — and therefore every prop built from it,
|
|
287
|
+
// e.g. ChatMessageInput's onEmojiPress — on every single render,
|
|
288
|
+
// which contributed to a real "Maximum update depth exceeded" loop
|
|
289
|
+
// found in testing. The lint rule can't prove member-access stability
|
|
290
|
+
// on its own, hence the disable.
|
|
291
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
192
292
|
}, [
|
|
193
293
|
activePanelType,
|
|
194
294
|
isAttachmentOpen,
|
|
@@ -196,9 +296,11 @@ export const ChatComposer = memo(
|
|
|
196
296
|
openWithHeight,
|
|
197
297
|
toggleAttachmentVisibility,
|
|
198
298
|
setActivePanelType,
|
|
299
|
+
mentionComposer.closeMentionPanel,
|
|
199
300
|
]);
|
|
200
301
|
|
|
201
302
|
const handleToggleSticker = useCallback(() => {
|
|
303
|
+
mentionComposer.closeMentionPanel();
|
|
202
304
|
if (activePanelType === 'sticker') {
|
|
203
305
|
closeAttachment();
|
|
204
306
|
setActivePanelType(null);
|
|
@@ -209,9 +311,12 @@ export const ChatComposer = memo(
|
|
|
209
311
|
}
|
|
210
312
|
setActivePanelType('sticker');
|
|
211
313
|
openWithHeight(STICKER_PICKER_HEIGHT);
|
|
314
|
+
// Same reasoning as handleToggleAttachment above — see that comment.
|
|
315
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
212
316
|
}, [
|
|
213
317
|
activePanelType,
|
|
214
318
|
isKeyboardVisible,
|
|
319
|
+
mentionComposer.closeMentionPanel,
|
|
215
320
|
closeAttachment,
|
|
216
321
|
openWithHeight,
|
|
217
322
|
setActivePanelType,
|
|
@@ -221,12 +326,10 @@ export const ChatComposer = memo(
|
|
|
221
326
|
setIsInputFocused(false);
|
|
222
327
|
}, [setIsInputFocused]);
|
|
223
328
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
[compose]
|
|
229
|
-
);
|
|
329
|
+
// Routes through mentionComposer instead of compose.setText directly —
|
|
330
|
+
// it owns the atomic-delete/reconcile pipeline and calls compose.setText
|
|
331
|
+
// itself once the corrected text is known (see useMentionComposer).
|
|
332
|
+
const handleChangeText = mentionComposer.handleChangeText;
|
|
230
333
|
|
|
231
334
|
const handleInputContentSizeChange = useCallback(
|
|
232
335
|
(event: { nativeEvent: { contentSize: { height: number } } }) => {
|
|
@@ -514,69 +617,88 @@ export const ChatComposer = memo(
|
|
|
514
617
|
onRemove={onRemove}
|
|
515
618
|
/>
|
|
516
619
|
|
|
517
|
-
<KContainer.View
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
620
|
+
<KContainer.View style={styles.inputRowWrapper}>
|
|
621
|
+
<KContainer.VisibleView visible={mentionComposer.isPanelOpen}>
|
|
622
|
+
<KContainer.View style={styles.mentionPanel}>
|
|
623
|
+
<MentionSuggestionList
|
|
624
|
+
suggestions={mentionComposer.suggestions}
|
|
625
|
+
isSearching={mentionComposer.isSearching}
|
|
626
|
+
isLoadingMore={mentionComposer.isLoadingMore}
|
|
627
|
+
hasMore={mentionComposer.hasMore}
|
|
628
|
+
onLoadMore={mentionComposer.loadMore}
|
|
629
|
+
onSelect={mentionComposer.selectSuggestion}
|
|
630
|
+
/>
|
|
631
|
+
</KContainer.View>
|
|
632
|
+
</KContainer.VisibleView>
|
|
633
|
+
|
|
634
|
+
<KContainer.View
|
|
635
|
+
row
|
|
636
|
+
alignItems
|
|
637
|
+
paddingH="0.75rem"
|
|
638
|
+
paddingT="0.75rem"
|
|
639
|
+
paddingB="0.75rem"
|
|
640
|
+
style={inputRowStyle}
|
|
529
641
|
>
|
|
530
|
-
<
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
642
|
+
<KContainer.Touchable
|
|
643
|
+
onPress={handleToggleAttachment}
|
|
644
|
+
center
|
|
645
|
+
br="round"
|
|
646
|
+
>
|
|
647
|
+
<KImage.VectorIcons
|
|
648
|
+
name={
|
|
649
|
+
activePanelType === 'attachment'
|
|
650
|
+
? 'close-circle-o'
|
|
651
|
+
: 'plus-circle-o'
|
|
652
|
+
}
|
|
653
|
+
size={24}
|
|
654
|
+
color={
|
|
655
|
+
activePanelType === 'attachment'
|
|
656
|
+
? KColors.palette.primary.w400
|
|
657
|
+
: KColors.palette.gray.w600
|
|
658
|
+
}
|
|
659
|
+
/>
|
|
660
|
+
</KContainer.Touchable>
|
|
661
|
+
|
|
662
|
+
<ChatMessageInput
|
|
663
|
+
value={compose.text}
|
|
664
|
+
placeholder="Nhập tin nhắn..."
|
|
665
|
+
onChangeText={handleChangeText}
|
|
666
|
+
onFocus={handleInputFocus}
|
|
667
|
+
onBlur={handleInputBlur}
|
|
668
|
+
onContentSizeChange={handleInputContentSizeChange}
|
|
669
|
+
onSelectionChange={mentionComposer.handleSelectionChange}
|
|
670
|
+
mentions={mentionComposer.mentions}
|
|
671
|
+
isInputFocused={isInputFocused}
|
|
672
|
+
isMultilineInput={isMultilineInput}
|
|
673
|
+
inputRef={inputRef}
|
|
674
|
+
onEmojiPress={handleToggleSticker}
|
|
675
|
+
onSuggestPress={
|
|
676
|
+
hasBotMenuOptions ? handleOpenBotMenu : undefined
|
|
541
677
|
}
|
|
542
678
|
/>
|
|
543
|
-
</KContainer.Touchable>
|
|
544
|
-
|
|
545
|
-
<ChatMessageInput
|
|
546
|
-
value={compose.text}
|
|
547
|
-
placeholder="Nhập tin nhắn..."
|
|
548
|
-
onChangeText={handleChangeText}
|
|
549
|
-
onFocus={handleInputFocus}
|
|
550
|
-
onBlur={handleInputBlur}
|
|
551
|
-
onContentSizeChange={handleInputContentSizeChange}
|
|
552
|
-
isInputFocused={isInputFocused}
|
|
553
|
-
isMultilineInput={isMultilineInput}
|
|
554
|
-
inputRef={inputRef}
|
|
555
|
-
onEmojiPress={handleToggleSticker}
|
|
556
|
-
onSuggestPress={hasBotMenuOptions ? handleOpenBotMenu : undefined}
|
|
557
|
-
/>
|
|
558
679
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
680
|
+
<KContainer.Touchable
|
|
681
|
+
onPress={compose.isUploading ? undefined : handleSend}
|
|
682
|
+
pointerEvents={compose.isUploading ? 'none' : 'auto'}
|
|
683
|
+
width={40}
|
|
684
|
+
height={40}
|
|
685
|
+
center
|
|
686
|
+
br="round"
|
|
687
|
+
background={
|
|
688
|
+
compose.isUploading
|
|
689
|
+
? KColors.palette.gray.w400
|
|
690
|
+
: KColors.palette.primary.w400
|
|
691
|
+
}
|
|
692
|
+
marginL="0.75rem"
|
|
693
|
+
opacity={compose.isUploading ? 0.6 : 1}
|
|
694
|
+
>
|
|
695
|
+
<KImage.VectorIcons
|
|
696
|
+
name="send-b"
|
|
697
|
+
size={20}
|
|
698
|
+
color={KColors.white}
|
|
699
|
+
/>
|
|
700
|
+
</KContainer.Touchable>
|
|
701
|
+
</KContainer.View>
|
|
580
702
|
</KContainer.View>
|
|
581
703
|
</KContainer.View>
|
|
582
704
|
);
|
|
@@ -618,4 +740,16 @@ const styles = StyleSheet.create({
|
|
|
618
740
|
overflow: 'hidden',
|
|
619
741
|
backgroundColor: KColors.palette.gray.w25,
|
|
620
742
|
},
|
|
743
|
+
inputRowWrapper: {
|
|
744
|
+
position: 'relative',
|
|
745
|
+
},
|
|
746
|
+
// Floats above the input row without going through the accessorySlot
|
|
747
|
+
// (attachment/sticker) render path — that slot intentionally takes over
|
|
748
|
+
// the keyboard area, which the mention picker must not do (finding #5).
|
|
749
|
+
mentionPanel: {
|
|
750
|
+
position: 'absolute',
|
|
751
|
+
left: 0,
|
|
752
|
+
right: 0,
|
|
753
|
+
bottom: '100%',
|
|
754
|
+
},
|
|
621
755
|
});
|
|
@@ -59,7 +59,6 @@ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
|
59
59
|
import { useUserStore } from '../../store';
|
|
60
60
|
import { ChatMessageAPI } from '../../services';
|
|
61
61
|
import { resolveImageDisplayDims } from '../../components/messages/imageMessage';
|
|
62
|
-
import { STICKER_SIZE } from '../../components/messages/stickerMessage';
|
|
63
62
|
import ChatImageDimens, {
|
|
64
63
|
CHAT_IMAGE_VIEW_SOURCE,
|
|
65
64
|
} from '../../utils/chatImageDimens';
|
|
@@ -151,6 +150,11 @@ export const ChatListLegend = memo(
|
|
|
151
150
|
const highlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
152
151
|
null
|
|
153
152
|
);
|
|
153
|
+
// Single revealed message at a time: tapping a message shows its own
|
|
154
|
+
// time below it, tapping it again (or a different message) hides/moves it.
|
|
155
|
+
const [revealedMessageId, setRevealedMessageId] = useState<string | null>(
|
|
156
|
+
null
|
|
157
|
+
);
|
|
154
158
|
|
|
155
159
|
const { applicationType } = useChatContext();
|
|
156
160
|
const searchMessages = useMessageStore((s) => s.searchMessages);
|
|
@@ -177,6 +181,10 @@ export const ChatListLegend = memo(
|
|
|
177
181
|
resetFlow();
|
|
178
182
|
}, [resetFlow]);
|
|
179
183
|
|
|
184
|
+
const handleMessagePress = useCallback((messageId: string) => {
|
|
185
|
+
setRevealedMessageId((prev) => (prev === messageId ? null : messageId));
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
180
188
|
const renderItem = useCallback(
|
|
181
189
|
({ item, index }: any) => {
|
|
182
190
|
try {
|
|
@@ -240,6 +248,7 @@ export const ChatListLegend = memo(
|
|
|
240
248
|
onMediaPress={onMediaPress}
|
|
241
249
|
onPressUrl={onPressUrl}
|
|
242
250
|
onRetry={retryMessage}
|
|
251
|
+
onPress={() => handleMessagePress(messageId)}
|
|
243
252
|
onLongPress={
|
|
244
253
|
onLongPress
|
|
245
254
|
? () => handleMessageLongPress(messageId, message)
|
|
@@ -247,6 +256,7 @@ export const ChatListLegend = memo(
|
|
|
247
256
|
}
|
|
248
257
|
customMessageItemFactory={customMessageItemFactory}
|
|
249
258
|
isHighlighted={highlightedId === messageId}
|
|
259
|
+
isTimeRevealed={revealedMessageId === messageId}
|
|
250
260
|
showName={showName}
|
|
251
261
|
showAvatar={showAvatar}
|
|
252
262
|
showSenderGutter={showSenderGutter}
|
|
@@ -291,6 +301,8 @@ export const ChatListLegend = memo(
|
|
|
291
301
|
conversationFlow?.havingBotMessage,
|
|
292
302
|
retryMessage,
|
|
293
303
|
highlightedId,
|
|
304
|
+
revealedMessageId,
|
|
305
|
+
handleMessagePress,
|
|
294
306
|
]
|
|
295
307
|
);
|
|
296
308
|
|
|
@@ -374,9 +386,10 @@ export const ChatListLegend = memo(
|
|
|
374
386
|
CHAT_IMAGE_VIEW_SOURCE.MessageListThumbnail
|
|
375
387
|
).height + rowMargin;
|
|
376
388
|
break;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
389
|
+
// StickerMessage is intentionally NOT fixed-size here: it now renders
|
|
390
|
+
// an optional revealed-time label below it (tap-to-reveal-time), so
|
|
391
|
+
// its true height can change after the first render — falls through
|
|
392
|
+
// to dynamic measurement like text/link/quote/file do.
|
|
380
393
|
default:
|
|
381
394
|
return undefined;
|
|
382
395
|
}
|
|
@@ -428,12 +441,14 @@ export const ChatListLegend = memo(
|
|
|
428
441
|
flowStepId: conversationFlow?.flowCurrentStep?.id ?? '',
|
|
429
442
|
lastMessageId,
|
|
430
443
|
highlightedId,
|
|
444
|
+
revealedMessageId,
|
|
431
445
|
}),
|
|
432
446
|
[
|
|
433
447
|
messageDataMap,
|
|
434
448
|
conversationFlow?.flowCurrentStep?.id,
|
|
435
449
|
lastMessageId,
|
|
436
450
|
highlightedId,
|
|
451
|
+
revealedMessageId,
|
|
437
452
|
]
|
|
438
453
|
);
|
|
439
454
|
|
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
TextInput,
|
|
6
6
|
type NativeSyntheticEvent,
|
|
7
7
|
type TextInputContentSizeChangeEventData,
|
|
8
|
+
type TextInputSelectionChangeEventData,
|
|
8
9
|
} from 'react-native';
|
|
9
10
|
import { KContainer, KImage, KColors, KSpacingValue } from '@droppii/libs';
|
|
11
|
+
import { MentionHighlightAdapter } from './MentionHighlightAdapter';
|
|
12
|
+
import type { Mention } from '../../../types/mention';
|
|
10
13
|
|
|
11
14
|
const INPUT_FONT_SIZE = 16;
|
|
12
15
|
const INPUT_LINE_HEIGHT = 22.4;
|
|
@@ -22,6 +25,15 @@ export interface ChatMessageInputProps {
|
|
|
22
25
|
onContentSizeChange: (
|
|
23
26
|
event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>
|
|
24
27
|
) => void;
|
|
28
|
+
onSelectionChange?: (
|
|
29
|
+
event: NativeSyntheticEvent<TextInputSelectionChangeEventData>
|
|
30
|
+
) => void;
|
|
31
|
+
/** Committed mentions — when non-empty, the input switches from plain
|
|
32
|
+
* `value` to a nested-Text `children` tree (MentionHighlightAdapter) so
|
|
33
|
+
* mentions render highlighted. Omitted/empty keeps the original plain
|
|
34
|
+
* `value`-controlled path, so the overwhelming majority of messages (no
|
|
35
|
+
* mentions) never pay for this. */
|
|
36
|
+
mentions?: Mention[];
|
|
25
37
|
isInputFocused: boolean;
|
|
26
38
|
isMultilineInput: boolean;
|
|
27
39
|
onEmojiPress?: () => void;
|
|
@@ -37,12 +49,24 @@ export const ChatMessageInput = memo(
|
|
|
37
49
|
onFocus,
|
|
38
50
|
onBlur,
|
|
39
51
|
onContentSizeChange,
|
|
52
|
+
onSelectionChange,
|
|
53
|
+
mentions,
|
|
40
54
|
isInputFocused,
|
|
41
55
|
isMultilineInput,
|
|
42
56
|
onEmojiPress,
|
|
43
57
|
onSuggestPress,
|
|
44
58
|
inputRef,
|
|
45
59
|
}: ChatMessageInputProps) => {
|
|
60
|
+
// Re-enabled: the earlier flicker/"Maximum update depth exceeded" loop
|
|
61
|
+
// was root-caused to two bugs in the composer wiring (unstable callback
|
|
62
|
+
// deps on `mentionComposer` recreating ChatMessageInput's onEmojiPress
|
|
63
|
+
// every render, and redundant/stale mention re-detection from
|
|
64
|
+
// onSelectionChange) — not the nested-Text children-mode rendering
|
|
65
|
+
// itself, which was disabled here as a precaution while that was still
|
|
66
|
+
// unclear. Both root causes are fixed in useMentionComposer.ts /
|
|
67
|
+
// ChatComposer.tsx. If a highlight-specific issue turns up on device,
|
|
68
|
+
// set this back to `false` — it's a one-line, fully reversible gate.
|
|
69
|
+
const hasMentions = !!mentions && mentions.length > 0;
|
|
46
70
|
const renderEmojiNode = useCallback(
|
|
47
71
|
() => (
|
|
48
72
|
<KContainer.Touchable marginR="0.5rem" onPress={onEmojiPress}>
|
|
@@ -85,20 +109,29 @@ export const ChatMessageInput = memo(
|
|
|
85
109
|
ref={inputRef}
|
|
86
110
|
placeholder={placeholder}
|
|
87
111
|
placeholderTextColor={KColors.palette.gray.w200}
|
|
88
|
-
|
|
112
|
+
{...(hasMentions ? {} : { value })}
|
|
89
113
|
onChangeText={onChangeText}
|
|
90
114
|
onFocus={onFocus}
|
|
91
115
|
onBlur={onBlur}
|
|
92
116
|
onContentSizeChange={onContentSizeChange}
|
|
117
|
+
onSelectionChange={onSelectionChange}
|
|
93
118
|
multiline
|
|
94
119
|
blurOnSubmit={false}
|
|
120
|
+
autoCorrect={false}
|
|
95
121
|
scrollEnabled
|
|
96
122
|
allowFontScaling={false}
|
|
97
123
|
underlineColorAndroid={KColors.transparent}
|
|
98
124
|
selectionColor={KColors.selector}
|
|
99
125
|
textAlignVertical={isMultilineInput ? 'top' : 'center'}
|
|
100
126
|
style={[styles.textInput]}
|
|
101
|
-
|
|
127
|
+
>
|
|
128
|
+
{hasMentions && (
|
|
129
|
+
<MentionHighlightAdapter
|
|
130
|
+
text={value ?? ''}
|
|
131
|
+
mentions={mentions!}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</TextInput>
|
|
102
135
|
|
|
103
136
|
{renderEmojiNode()}
|
|
104
137
|
</KContainer.View>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { memo, useMemo } from 'react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
import { KColors } from '@droppii/libs';
|
|
4
|
+
import { tokenize } from '../../../utils/mentions';
|
|
5
|
+
import type { Mention } from '../../../types/mention';
|
|
6
|
+
|
|
7
|
+
interface MentionHighlightAdapterProps {
|
|
8
|
+
text: string;
|
|
9
|
+
mentions: Mention[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Renders `text` as nested styled `<Text>` children for a TextInput's
|
|
14
|
+
* "undocumented" rich-text-via-children capability — the same primitive
|
|
15
|
+
* `react-native-controlled-mentions` and Bluesky's social-app both use.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately STATELESS: every render calls tokenize() fresh and rebuilds
|
|
18
|
+
* the whole children tree from scratch — no attempt to preserve/diff span
|
|
19
|
+
* identity across renders. This is the Bluesky-informed choice (see the
|
|
20
|
+
* mention plan's Phase A3): react-native-controlled-mentions instead tries
|
|
21
|
+
* to maintain *stateful* span identity across renders, and its known
|
|
22
|
+
* Android bugs (#34668 duplicate input, #41647 broken selection, #50731
|
|
23
|
+
* "spans recreated on every render") read like that statefulness fighting
|
|
24
|
+
* itself — Bluesky ships live-colored nested-Text in production by never
|
|
25
|
+
* attempting to preserve identity in the first place. Only tokenize()
|
|
26
|
+
* itself is memoized (on [text, mentions]); the resulting element tree is
|
|
27
|
+
* not further diffed or cached.
|
|
28
|
+
*
|
|
29
|
+
* Only mounted when mentions.length > 0 (see ChatMessageInput) — a plain
|
|
30
|
+
* `value`-controlled TextInput is used otherwise, so the vast majority of
|
|
31
|
+
* messages (no mentions) never pay for this at all.
|
|
32
|
+
*/
|
|
33
|
+
export const MentionHighlightAdapter = memo(
|
|
34
|
+
({ text, mentions }: MentionHighlightAdapterProps) => {
|
|
35
|
+
const segments = useMemo(() => tokenize(text, mentions), [text, mentions]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
{segments.map((segment, index) =>
|
|
40
|
+
segment.type === 'mention' ? (
|
|
41
|
+
<Text key={segment.mention.id} style={mentionTextStyle}>
|
|
42
|
+
{`@${segment.mention.displayName}`}
|
|
43
|
+
</Text>
|
|
44
|
+
) : (
|
|
45
|
+
// Plain-text runs have no stable identity of their own — index
|
|
46
|
+
// is fine for a tree rebuilt from scratch every render.
|
|
47
|
+
<Text key={`text-${index}`}>{segment.text}</Text>
|
|
48
|
+
)
|
|
49
|
+
)}
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
MentionHighlightAdapter.displayName = 'MentionHighlightAdapter';
|
|
56
|
+
|
|
57
|
+
const mentionTextStyle = { color: KColors.palette.primary.w400 };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { memo, useCallback } from 'react';
|
|
2
|
+
import { KContainer, KLabel, KColors, KImage } from '@droppii/libs';
|
|
3
|
+
import Avatar from '../../../components/Avatar/Avatar';
|
|
4
|
+
import { trans } from '../../../translation';
|
|
5
|
+
import type { MentionSuggestion } from '../../../types/mention';
|
|
6
|
+
|
|
7
|
+
const ALL_MEMBERS_USER_ID = 'ALL';
|
|
8
|
+
|
|
9
|
+
interface MentionSuggestionItemProps {
|
|
10
|
+
item: MentionSuggestion;
|
|
11
|
+
onPress: (item: MentionSuggestion) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const MentionSuggestionItem = memo(
|
|
15
|
+
({ item, onPress }: MentionSuggestionItemProps) => {
|
|
16
|
+
const isAllOption = item.userId === ALL_MEMBERS_USER_ID;
|
|
17
|
+
|
|
18
|
+
const handlePress = useCallback(() => onPress(item), [onPress, item]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<KContainer.Touchable row alignItems onPress={handlePress}>
|
|
22
|
+
{isAllOption ? (
|
|
23
|
+
<KContainer.View width={40} height={40} center>
|
|
24
|
+
<KImage.VectorIcons
|
|
25
|
+
name="user-three-b"
|
|
26
|
+
size={24}
|
|
27
|
+
color={KColors.palette.gray.w900}
|
|
28
|
+
/>
|
|
29
|
+
</KContainer.View>
|
|
30
|
+
) : (
|
|
31
|
+
<Avatar source={item.avatar} fullName={item.displayName} size="nm" />
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
<KContainer.View flex marginL="0.75rem">
|
|
35
|
+
<KLabel.Text typo="TextMdMedium" numberOfLines={1}>
|
|
36
|
+
{item.displayName}
|
|
37
|
+
</KLabel.Text>
|
|
38
|
+
{isAllOption && (
|
|
39
|
+
<KLabel.Text
|
|
40
|
+
typo="TextXsNormal"
|
|
41
|
+
color={KColors.palette.gray.w600}
|
|
42
|
+
numberOfLines={1}
|
|
43
|
+
>
|
|
44
|
+
{trans('group:tag_all_desc')}
|
|
45
|
+
</KLabel.Text>
|
|
46
|
+
)}
|
|
47
|
+
</KContainer.View>
|
|
48
|
+
</KContainer.Touchable>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
MentionSuggestionItem.displayName = 'MentionSuggestionItem';
|