@droppii-org/chat-mobile 0.2.81 → 0.2.82
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 +21 -68
- 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/textMessage/index.js +102 -8
- package/lib/module/components/messages/textMessage/index.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 +1 -0
- 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/LegendChatMessage.js +5 -1
- 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/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.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/textMessage/index.d.ts +3 -1
- package/lib/typescript/src/components/messages/textMessage/index.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/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/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 +25 -89
- package/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.tsx +168 -0
- package/src/components/messages/textMessage/index.tsx +124 -4
- 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 +1 -0
- 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/LegendChatMessage.tsx +7 -1
- 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/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
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
generateAndUploadVideoThumbnails,
|
|
14
14
|
type VideoWithUploadStatus,
|
|
15
15
|
} from '../utils/videoThumbnailUpload';
|
|
16
|
+
import type { Mention } from '../types/mention';
|
|
16
17
|
|
|
17
18
|
export function useChatCompose() {
|
|
18
19
|
const [text, setText] = useState('');
|
|
@@ -45,123 +46,130 @@ export function useChatCompose() {
|
|
|
45
46
|
]
|
|
46
47
|
);
|
|
47
48
|
|
|
48
|
-
const send = useCallback(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const send = useCallback(
|
|
50
|
+
async (mentions?: Mention[]) => {
|
|
51
|
+
const { selectedImages, clearAllImages } = imageAttachment;
|
|
52
|
+
const { selectedFiles, clearAllFiles } = fileAttachment;
|
|
53
|
+
const urlMeta = linkPreview.isVisible ? linkPreview.metadata : undefined;
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
// --- Attachments ---
|
|
56
|
+
if (selectedImages.length > 0 || selectedFiles.length > 0) {
|
|
57
|
+
const actualImages = selectedImages.filter(
|
|
58
|
+
(img: ImageWithUploadStatus) => !isVideoFromPicker(img.image)
|
|
59
|
+
);
|
|
60
|
+
const videos = selectedImages.filter((img: ImageWithUploadStatus) =>
|
|
61
|
+
isVideoFromPicker(img.image)
|
|
62
|
+
);
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
const uploadedImages = actualImages.filter(
|
|
65
|
+
(img: ImageWithUploadStatus) => img.uploadedUrl
|
|
66
|
+
);
|
|
67
|
+
const uploadedVideos = videos.filter(
|
|
68
|
+
(v) => v.uploadedUrl
|
|
69
|
+
) as unknown as VideoWithUploadStatus[];
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
let videoThumbnails: Record<string, { url: string; size: number }> = {};
|
|
76
|
-
if (videosMissingThumbnail.length > 0) {
|
|
77
|
-
videoThumbnails = await generateAndUploadVideoThumbnails(
|
|
78
|
-
videosMissingThumbnail
|
|
71
|
+
// Thumbnails are generated + uploaded at pick time (useImageAttachment).
|
|
72
|
+
// Only fall back to generating here for videos that don't already have one
|
|
73
|
+
// (e.g. thumbnail generation failed or hadn't finished before send).
|
|
74
|
+
const videosMissingThumbnail = uploadedVideos.filter(
|
|
75
|
+
(v) => !v.thumbnailUrl
|
|
79
76
|
);
|
|
80
|
-
|
|
77
|
+
let videoThumbnails: Record<string, { url: string; size: number }> = {};
|
|
78
|
+
if (videosMissingThumbnail.length > 0) {
|
|
79
|
+
videoThumbnails = await generateAndUploadVideoThumbnails(
|
|
80
|
+
videosMissingThumbnail
|
|
81
|
+
);
|
|
82
|
+
}
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
84
|
+
// Send images + videos
|
|
85
|
+
if (uploadedImages.length > 0 || uploadedVideos.length > 0) {
|
|
86
|
+
const attachmentItems = [
|
|
87
|
+
...uploadedImages.map((img: ImageWithUploadStatus) => ({
|
|
88
|
+
...img,
|
|
89
|
+
type: 'image' as const,
|
|
90
|
+
})),
|
|
91
|
+
...uploadedVideos.map(
|
|
92
|
+
(vid: VideoWithUploadStatus, vidIdx: number) => {
|
|
93
|
+
const videoId = vid.uploadedUrl || `video-${vidIdx}`;
|
|
94
|
+
const thumb = videoThumbnails[videoId];
|
|
95
|
+
return {
|
|
96
|
+
type: 'video' as const,
|
|
97
|
+
video: {
|
|
98
|
+
path: vid.image.path,
|
|
99
|
+
filename: vid.image.filename ?? undefined,
|
|
100
|
+
size: vid.image.size ?? undefined,
|
|
101
|
+
width: vid.image.width ?? undefined,
|
|
102
|
+
height: vid.image.height ?? undefined,
|
|
103
|
+
duration: vid.image.duration ?? undefined,
|
|
104
|
+
},
|
|
105
|
+
uploadProgress: vid.uploadProgress,
|
|
106
|
+
error: vid.error,
|
|
107
|
+
isUploading: vid.isUploading,
|
|
108
|
+
uploadedUrl: vid.uploadedUrl,
|
|
109
|
+
thumbnailUrl: vid.thumbnailUrl ?? thumb?.url,
|
|
110
|
+
thumbnailSize: vid.thumbnailSize ?? thumb?.size,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
),
|
|
114
|
+
];
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
// Clear composer immediately — message is already in chat list with Sending status.
|
|
117
|
+
// User retries from the bubble, not the composer.
|
|
118
|
+
ImagePicker.clean().catch(() => {});
|
|
119
|
+
clearAllImages();
|
|
120
|
+
// Awaited so files (lower priority per attachment-priority config)
|
|
121
|
+
// only start sending after images/videos are done.
|
|
122
|
+
await sendAttachments(attachmentItems, undefined);
|
|
123
|
+
}
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
125
|
+
// Send each file as its own message (never merged), in priority order
|
|
126
|
+
// after images/videos, one at a time to preserve pick order.
|
|
127
|
+
const uploadedFiles = selectedFiles.filter(
|
|
128
|
+
(f) => f.uploadedUrl && !f.error
|
|
129
|
+
);
|
|
130
|
+
if (uploadedFiles.length > 0) {
|
|
131
|
+
clearAllFiles();
|
|
132
|
+
for (const file of uploadedFiles) {
|
|
133
|
+
await sendAttachments(
|
|
134
|
+
[
|
|
135
|
+
{
|
|
136
|
+
type: 'file' as const,
|
|
137
|
+
file: {
|
|
138
|
+
path: file.uri,
|
|
139
|
+
filename: file.name,
|
|
140
|
+
size: file.size,
|
|
141
|
+
},
|
|
142
|
+
uploadProgress: 100,
|
|
143
|
+
error: null,
|
|
144
|
+
isUploading: false,
|
|
145
|
+
uploadedUrl: file.uploadedUrl,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
undefined
|
|
149
|
+
);
|
|
150
|
+
}
|
|
144
151
|
}
|
|
145
152
|
}
|
|
146
|
-
}
|
|
147
153
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
154
|
+
// --- Text / link preview ---
|
|
155
|
+
if (text.trim() || urlMeta) {
|
|
156
|
+
const plainText = text.trim();
|
|
157
|
+
// Clear composer immediately — message is already in chat list with Sending status.
|
|
158
|
+
// User retries from the bubble, not the composer.
|
|
159
|
+
setText('');
|
|
160
|
+
await sendMessage({ plainText, urlMetadata: urlMeta, mentions });
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[
|
|
164
|
+
imageAttachment,
|
|
165
|
+
fileAttachment,
|
|
166
|
+
linkPreview.isVisible,
|
|
167
|
+
linkPreview.metadata,
|
|
168
|
+
sendAttachments,
|
|
169
|
+
sendMessage,
|
|
170
|
+
text,
|
|
171
|
+
]
|
|
172
|
+
);
|
|
165
173
|
|
|
166
174
|
return {
|
|
167
175
|
// Text
|
|
@@ -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
|
});
|