@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
|
@@ -1,43 +1,185 @@
|
|
|
1
|
-
import { memo } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { memo, useMemo } from 'react';
|
|
2
|
+
import { Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { MessageType } from '@droppii/openim-rn-client-sdk';
|
|
4
|
+
import { KContainer, KLabel, KColors } from '@droppii/libs';
|
|
3
5
|
import { getMessageText } from '../../../utils/legendListMessage';
|
|
4
6
|
import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
|
|
5
7
|
import { messageStyles } from '../styles';
|
|
6
8
|
import { getTailCornerStyle } from '../bubbleTail.utils';
|
|
7
9
|
import { BubbleTailRow } from '../BubbleTailRow';
|
|
8
|
-
import type
|
|
10
|
+
import { formatMessageTime, type BaseLegendMessageProps } from '../types';
|
|
11
|
+
import { tokenize } from '../../../utils/mentions';
|
|
12
|
+
import { deserializeMentionRanges } from '../../../utils/mentionSerializer';
|
|
13
|
+
import type { IMessageItemEx } from '../../../types/chat';
|
|
14
|
+
import { QuotedMessagePreviewStrip } from '../quotedMessage/QuotedMessagePreviewStrip';
|
|
15
|
+
import { LinkPreviewCard } from '../linkMessage/LinkPreviewCard';
|
|
16
|
+
import { extractUrls, splitTextByUrls } from '../../../utils/url';
|
|
17
|
+
|
|
18
|
+
const parseMessageEx = (ex?: string): IMessageItemEx | undefined => {
|
|
19
|
+
if (!ex) return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(ex) as IMessageItemEx;
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
9
27
|
export const TextMessage = memo(
|
|
10
28
|
({
|
|
11
29
|
message,
|
|
12
30
|
isOutgoing,
|
|
31
|
+
createdAtTime,
|
|
13
32
|
isHighlighted,
|
|
33
|
+
isTimeRevealed,
|
|
34
|
+
onPressUrl,
|
|
14
35
|
groupPosition,
|
|
15
|
-
}: BaseLegendMessageProps) => {
|
|
36
|
+
}: BaseLegendMessageProps & { onPressUrl?: (url: string) => void }) => {
|
|
16
37
|
const messageText = getMessageText(message);
|
|
38
|
+
const timeLabel = formatMessageTime(createdAtTime);
|
|
39
|
+
|
|
40
|
+
const isAtTextMessage = message.contentType === MessageType.AtTextMessage;
|
|
41
|
+
|
|
42
|
+
// Both the reply-preview strip and the link-preview card below piggyback
|
|
43
|
+
// on an AtTextMessage instead of getting their own content type — OpenIM
|
|
44
|
+
// content types are mutually exclusive (AtText vs Quote vs UrlText), so
|
|
45
|
+
// once mention picks AtText (to keep native tag/notify semantics — see
|
|
46
|
+
// useSendMessage.ts), quote rides along via the native
|
|
47
|
+
// AtTextElem.quoteMessage field and the URL card rides along via our own
|
|
48
|
+
// ex.urlMetadata channel (already attached unconditionally on send).
|
|
49
|
+
const quotedMessage = isAtTextMessage
|
|
50
|
+
? message.atTextElem?.quoteMessage
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
const parsedEx = useMemo(
|
|
54
|
+
() => (isAtTextMessage ? parseMessageEx(message.ex) : undefined),
|
|
55
|
+
[isAtTextMessage, message.ex]
|
|
56
|
+
);
|
|
57
|
+
const urlMetadata = parsedEx?.urlMetadata;
|
|
58
|
+
|
|
59
|
+
const mentions = useMemo(() => {
|
|
60
|
+
if (!isAtTextMessage || !message.atTextElem) return [];
|
|
61
|
+
// Wrapped under `offsets` — matches the wire shape other Droppii
|
|
62
|
+
// clients (e.g. the web/CRM app) already send (see
|
|
63
|
+
// IMessageItemEx.mentions doc comment for how this was confirmed).
|
|
64
|
+
return deserializeMentionRanges(
|
|
65
|
+
message.atTextElem,
|
|
66
|
+
parsedEx?.mentions?.offsets
|
|
67
|
+
);
|
|
68
|
+
}, [isAtTextMessage, message.atTextElem, parsedEx]);
|
|
69
|
+
|
|
70
|
+
const segments = useMemo(
|
|
71
|
+
() => (mentions.length > 0 ? tokenize(messageText, mentions) : null),
|
|
72
|
+
[mentions, messageText]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Scoped to the mention+URL combo only (`segments` implies mentions are
|
|
76
|
+
// present) — a plain TextMessage with no mention falls through to
|
|
77
|
+
// `messageText` unchanged below, same as before this feature existed.
|
|
78
|
+
// Widening this to auto-linkify every TextMessage is a separate,
|
|
79
|
+
// deliberate product decision, not a side effect of this fix.
|
|
80
|
+
const urls = useMemo(
|
|
81
|
+
() => (segments ? extractUrls(messageText) : []),
|
|
82
|
+
[segments, messageText]
|
|
83
|
+
);
|
|
17
84
|
|
|
18
85
|
if (!messageText?.trim()) {
|
|
19
86
|
return null;
|
|
20
87
|
}
|
|
21
88
|
|
|
89
|
+
// A plain-text run (the space between two mentions) may itself contain
|
|
90
|
+
// a URL — split further so it gets the same tappable/colored treatment
|
|
91
|
+
// LinkMessage gives a pure URL message, instead of sitting there as
|
|
92
|
+
// unstyled black text next to a colored mention.
|
|
93
|
+
const renderTextRun = (text: string, keyPrefix: string) => {
|
|
94
|
+
if (!urls.length) return <Text key={keyPrefix}>{text}</Text>;
|
|
95
|
+
return splitTextByUrls(text, urls).map((part, i) =>
|
|
96
|
+
part.isUrl ? (
|
|
97
|
+
<Text
|
|
98
|
+
key={`${keyPrefix}-url-${i}`}
|
|
99
|
+
style={urlTextStyle}
|
|
100
|
+
onPress={() => onPressUrl?.(part.text)}
|
|
101
|
+
>
|
|
102
|
+
{part.text}
|
|
103
|
+
</Text>
|
|
104
|
+
) : (
|
|
105
|
+
<Text key={`${keyPrefix}-text-${i}`}>{part.text}</Text>
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
22
110
|
return (
|
|
23
|
-
|
|
111
|
+
<>
|
|
112
|
+
<BubbleTailRow isOutgoing={isOutgoing} groupPosition={groupPosition}>
|
|
113
|
+
<KContainer.View
|
|
114
|
+
style={[
|
|
115
|
+
messageStyles.bubble,
|
|
116
|
+
styles.bubbleContent,
|
|
117
|
+
isOutgoing
|
|
118
|
+
? messageStyles.bubbleSent
|
|
119
|
+
: messageStyles.bubbleReceived,
|
|
120
|
+
isHighlighted && messageStyles.bubbleHighlighted,
|
|
121
|
+
getTailCornerStyle(isOutgoing, groupPosition),
|
|
122
|
+
]}
|
|
123
|
+
>
|
|
124
|
+
{quotedMessage && (
|
|
125
|
+
<QuotedMessagePreviewStrip quotedMessage={quotedMessage} />
|
|
126
|
+
)}
|
|
127
|
+
<KLabel.Text typo="TextMdNormal" color={CHAT_BUBBLE_COLORS.text}>
|
|
128
|
+
{segments
|
|
129
|
+
? segments.map((segment, index) =>
|
|
130
|
+
segment.type === 'mention' ? (
|
|
131
|
+
<Text key={segment.mention.id} style={mentionTextStyle}>
|
|
132
|
+
{`@${segment.mention.displayName}`}
|
|
133
|
+
</Text>
|
|
134
|
+
) : (
|
|
135
|
+
renderTextRun(segment.text, `seg-${index}`)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
: messageText}
|
|
139
|
+
</KLabel.Text>
|
|
140
|
+
{!!urlMetadata && (
|
|
141
|
+
<LinkPreviewCard metadata={urlMetadata} onPressUrl={onPressUrl} />
|
|
142
|
+
)}
|
|
143
|
+
</KContainer.View>
|
|
144
|
+
</BubbleTailRow>
|
|
145
|
+
|
|
24
146
|
<KContainer.View
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
? messageStyles.bubbleSent
|
|
29
|
-
: messageStyles.bubbleReceived,
|
|
30
|
-
isHighlighted && messageStyles.bubbleHighlighted,
|
|
31
|
-
getTailCornerStyle(isOutgoing, groupPosition),
|
|
32
|
-
]}
|
|
147
|
+
row
|
|
148
|
+
alignItems
|
|
149
|
+
style={[styles.timeLabel, isOutgoing && styles.timeLabelRight]}
|
|
33
150
|
>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
151
|
+
{isTimeRevealed && timeLabel ? (
|
|
152
|
+
<KLabel.Text
|
|
153
|
+
typo="TextXsNormal"
|
|
154
|
+
color={CHAT_BUBBLE_COLORS.timestamp}
|
|
155
|
+
marginL={isOutgoing ? undefined : '0.25rem'}
|
|
156
|
+
marginR={isOutgoing ? '0.25rem' : undefined}
|
|
157
|
+
>
|
|
158
|
+
{timeLabel}
|
|
159
|
+
</KLabel.Text>
|
|
160
|
+
) : null}
|
|
37
161
|
</KContainer.View>
|
|
38
|
-
|
|
162
|
+
</>
|
|
39
163
|
);
|
|
40
164
|
}
|
|
41
165
|
);
|
|
42
166
|
|
|
43
167
|
TextMessage.displayName = 'TextMessage';
|
|
168
|
+
|
|
169
|
+
const styles = StyleSheet.create({
|
|
170
|
+
bubbleContent: { gap: 4 },
|
|
171
|
+
timeLabel: { gap: 4, marginTop: 2 },
|
|
172
|
+
timeLabelRight: { justifyContent: 'flex-end' },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Same highlight color as the composer (MentionHighlightAdapter) and the
|
|
176
|
+
// Figma screenshots. This is a read-only <Text>, not an editable
|
|
177
|
+
// TextInput, so none of the nested-Text-in-TextInput bug class applies —
|
|
178
|
+
// nesting styled Text here is unconditionally safe.
|
|
179
|
+
const mentionTextStyle = { color: KColors.palette.primary.w400 };
|
|
180
|
+
|
|
181
|
+
// Same color LinkMessage uses for a URL substring — a mention message's
|
|
182
|
+
// plain-text run can contain a URL too (mention+URL combo, see
|
|
183
|
+
// useSendMessage.ts's doc comment), and it should look tappable/colored
|
|
184
|
+
// the same way there as it does in a pure UrlTextMessage.
|
|
185
|
+
const urlTextStyle = { color: KColors.primary.normal };
|
|
@@ -7,6 +7,7 @@ export interface BaseLegendMessageProps {
|
|
|
7
7
|
isOutgoing: boolean;
|
|
8
8
|
createdAtTime: number;
|
|
9
9
|
isHighlighted?: boolean;
|
|
10
|
+
isTimeRevealed?: boolean;
|
|
10
11
|
/** Position within its same-sender cluster — drives the bubble tail. */
|
|
11
12
|
groupPosition?: GroupPosition;
|
|
12
13
|
onMediaPress?: (items: MediaItem[], index?: number) => void;
|
|
@@ -54,7 +54,7 @@ export const useChatListener = () => {
|
|
|
54
54
|
|
|
55
55
|
const onConversationChanged = useCallback(
|
|
56
56
|
(conversations: ConversationItem[]) => {
|
|
57
|
-
|
|
57
|
+
console.log('[Listener] OnConversationChanged', conversations);
|
|
58
58
|
|
|
59
59
|
// return; // TEMPORARILY DISABLED: OnConversationChanged/OnNewConversation can
|
|
60
60
|
const oldMap = useConversationStore.getState().map;
|
|
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|
|
2
2
|
import { useConversationStore } from '../../store/conversation';
|
|
3
3
|
import { useUserStore } from '../../store/user';
|
|
4
4
|
import type {
|
|
5
|
+
AtMsgParams,
|
|
5
6
|
CustomMsgParams,
|
|
6
7
|
MessageItem,
|
|
7
8
|
OfflinePush,
|
|
@@ -35,6 +36,8 @@ import { stickerQueryKeys } from '../query-keys';
|
|
|
35
36
|
import { getMessagePreview } from '../../utils/messageUtils';
|
|
36
37
|
import { useCanSendMessage } from '../useGroupPermission';
|
|
37
38
|
import { UIUtils } from '../../utils/UIUtils';
|
|
39
|
+
import type { Mention } from '../../types/mention';
|
|
40
|
+
import { serializeMentionsForSend } from '../../utils/mentionSerializer';
|
|
38
41
|
|
|
39
42
|
const buildSendParams = (
|
|
40
43
|
message: MessageItem,
|
|
@@ -98,12 +101,14 @@ export const useSendMessage = () => {
|
|
|
98
101
|
flow,
|
|
99
102
|
customMessage,
|
|
100
103
|
sticker,
|
|
104
|
+
mentions,
|
|
101
105
|
}: {
|
|
102
106
|
plainText?: string;
|
|
103
107
|
urlMetadata?: IUrlMetadata;
|
|
104
108
|
flow?: IRunConversationFlowPayload;
|
|
105
109
|
customMessage?: CustomMessagePayload;
|
|
106
110
|
sticker?: StickerItem;
|
|
111
|
+
mentions?: Mention[];
|
|
107
112
|
}) => {
|
|
108
113
|
if (!isSendMessageAllowed) {
|
|
109
114
|
UIUtils.toast.open({
|
|
@@ -124,9 +129,11 @@ export const useSendMessage = () => {
|
|
|
124
129
|
if (!recvID && !groupID) return;
|
|
125
130
|
|
|
126
131
|
const messageList: MessageItem[] = [];
|
|
132
|
+
let exMentions: IMessageItemEx['mentions'];
|
|
127
133
|
|
|
128
134
|
const hasText = !!plainText && plainText.trim() !== '';
|
|
129
135
|
const hasPreview = !!urlMetadata?.url;
|
|
136
|
+
const hasMentions = !!mentions && mentions.length > 0;
|
|
130
137
|
|
|
131
138
|
//text msg
|
|
132
139
|
if (hasText) {
|
|
@@ -134,12 +141,51 @@ export const useSendMessage = () => {
|
|
|
134
141
|
const isUrlMessage = urls.length > 0 || hasPreview;
|
|
135
142
|
let message: MessageItem | null = null;
|
|
136
143
|
|
|
137
|
-
|
|
144
|
+
// Mention always wins the content type over a URL preview: AtText
|
|
145
|
+
// and UrlText are mutually exclusive OpenIM content types (no field
|
|
146
|
+
// for both), but `ex.urlMetadata` is attached unconditionally below
|
|
147
|
+
// regardless of which branch builds `message` — so the URL card
|
|
148
|
+
// still rides along via `ex` on an AtTextMessage. TextMessage's
|
|
149
|
+
// render side reads it and shows the card next to the
|
|
150
|
+
// mention-highlighted text (see its doc comment). Other clients
|
|
151
|
+
// without that rendering just see plain "@Name ... https://..."
|
|
152
|
+
// text — mention intact, only the rich card is missing.
|
|
153
|
+
if (quotedMessage && hasMentions && groupID) {
|
|
154
|
+
// Native mention+quote: AtMsgParams.message embeds the quoted
|
|
155
|
+
// message into AtTextElem.quoteMessage server/core-side (verified
|
|
156
|
+
// down to openimsdk-core/internal/conversation_msg/
|
|
157
|
+
// create_message.go:76-101 — QuoteMessage: qs is a real field
|
|
158
|
+
// assignment, not just a bridge pass-through) — keeps the mention's
|
|
159
|
+
// native tag/notify semantics instead of falling back to a plain
|
|
160
|
+
// QuoteMessage, which has no atUserIDList at all.
|
|
161
|
+
const serialized = serializeMentionsForSend(mentions);
|
|
162
|
+
exMentions = { offsets: serialized.exMentions };
|
|
163
|
+
message = await createTextAtMessage({
|
|
164
|
+
text: plainText || '',
|
|
165
|
+
atUserIDList: serialized.atUserIDList,
|
|
166
|
+
atUsersInfo: serialized.atUsersInfo,
|
|
167
|
+
message: quotedMessage,
|
|
168
|
+
});
|
|
169
|
+
setQuotedMessage(undefined);
|
|
170
|
+
} else if (quotedMessage) {
|
|
138
171
|
message = await createQuotedMessage({
|
|
139
172
|
text: plainText || '',
|
|
140
173
|
message: quotedMessage,
|
|
141
174
|
});
|
|
142
175
|
setQuotedMessage(undefined);
|
|
176
|
+
} else if (hasMentions && groupID) {
|
|
177
|
+
// "Tất cả" serializes to OpenIM's own AT_ALL_TAG sentinel, not an
|
|
178
|
+
// expanded member list — no group-member fetch needed here, see
|
|
179
|
+
// AT_ALL_TAG's doc comment in mentionSerializer.ts for why.
|
|
180
|
+
const serialized = serializeMentionsForSend(mentions);
|
|
181
|
+
// Wrapped under `offsets` to match the wire shape other Droppii
|
|
182
|
+
// clients already send (see IMessageItemEx.mentions doc comment).
|
|
183
|
+
exMentions = { offsets: serialized.exMentions };
|
|
184
|
+
message = await createTextAtMessage({
|
|
185
|
+
text: plainText || '',
|
|
186
|
+
atUserIDList: serialized.atUserIDList,
|
|
187
|
+
atUsersInfo: serialized.atUsersInfo,
|
|
188
|
+
});
|
|
143
189
|
} else if (isUrlMessage) {
|
|
144
190
|
message = await createUrlTextMessage(plainText || '', urls);
|
|
145
191
|
} else if (hasText) {
|
|
@@ -178,6 +224,7 @@ export const useSendMessage = () => {
|
|
|
178
224
|
urlMetadata,
|
|
179
225
|
flow,
|
|
180
226
|
stickerId: sticker?.id,
|
|
227
|
+
mentions: exMentions,
|
|
181
228
|
};
|
|
182
229
|
|
|
183
230
|
await dispatchSend(
|
|
@@ -330,6 +377,18 @@ export const createUrlTextMessage = async (text: string, urls: string[]) => {
|
|
|
330
377
|
return urlTextMessage;
|
|
331
378
|
};
|
|
332
379
|
|
|
380
|
+
export const createTextAtMessage = async (params: AtMsgParams) => {
|
|
381
|
+
let atMessage = await OpenIMSDK.createTextAtMessage(params, uuid.v4())
|
|
382
|
+
.then((data) => {
|
|
383
|
+
return data;
|
|
384
|
+
})
|
|
385
|
+
.catch(({ errCode, errMsg }) => {
|
|
386
|
+
console.error('createTextAtMessage', errCode, errMsg);
|
|
387
|
+
return null;
|
|
388
|
+
});
|
|
389
|
+
return atMessage;
|
|
390
|
+
};
|
|
391
|
+
|
|
333
392
|
export const createCustomMessage = async (params: CustomMsgParams) => {
|
|
334
393
|
let customMessage = await OpenIMSDK.createCustomMessage(params, uuid.v4())
|
|
335
394
|
.then((data) => {
|
package/src/hooks/query-keys.ts
CHANGED
|
@@ -62,6 +62,12 @@ export const messageQueryKeys = {
|
|
|
62
62
|
[...messageQueryKeys.all, 'search', query, conversationId] as const,
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
export const mentionQueryKeys = {
|
|
66
|
+
all: ['mentionSearch'] as const,
|
|
67
|
+
search: (groupID: string, keyword: string) =>
|
|
68
|
+
[...mentionQueryKeys.all, groupID, keyword] as const,
|
|
69
|
+
};
|
|
70
|
+
|
|
65
71
|
export const contactQueryKeys = {
|
|
66
72
|
all: ['contacts'] as const,
|
|
67
73
|
search: (query: string, type?: string) =>
|
|
@@ -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
|