@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
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { memo } from 'react';
|
|
2
2
|
import { StyleSheet } from 'react-native';
|
|
3
|
-
import {
|
|
4
|
-
KContainer,
|
|
5
|
-
KImage,
|
|
6
|
-
KLabel,
|
|
7
|
-
KColors,
|
|
8
|
-
KSpacingValue,
|
|
9
|
-
} from '@droppii/libs';
|
|
3
|
+
import { KContainer, KLabel, KColors, KSpacingValue } from '@droppii/libs';
|
|
10
4
|
import type { BaseLegendMessageProps } from '../types';
|
|
11
5
|
import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
|
|
12
6
|
import { parseUrlMetadata } from '../../../utils/messageUtils';
|
|
7
|
+
import { splitTextByUrls } from '../../../utils/url';
|
|
13
8
|
import { messageStyles } from '../styles';
|
|
9
|
+
import { LinkPreviewCard } from './LinkPreviewCard';
|
|
14
10
|
import { getTailCornerStyle } from '../bubbleTail.utils';
|
|
15
11
|
import { BubbleTailRow } from '../BubbleTailRow';
|
|
16
12
|
|
|
@@ -18,45 +14,26 @@ const renderTextWithLinks = (
|
|
|
18
14
|
content: string,
|
|
19
15
|
urls: string[],
|
|
20
16
|
onPressUrl?: (url: string) => void
|
|
21
|
-
) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{parts.map((part, i) =>
|
|
42
|
-
part.isUrl ? (
|
|
43
|
-
<KLabel.Text
|
|
44
|
-
key={i}
|
|
45
|
-
onPress={() => onPressUrl?.(part.text)}
|
|
46
|
-
color={KColors.primary.normal}
|
|
47
|
-
typo="TextMdMedium"
|
|
48
|
-
>
|
|
49
|
-
{part.text}
|
|
50
|
-
</KLabel.Text>
|
|
51
|
-
) : (
|
|
52
|
-
<KLabel.Text typo="TextMdNormal" key={i}>
|
|
53
|
-
{part.text}
|
|
54
|
-
</KLabel.Text>
|
|
55
|
-
)
|
|
56
|
-
)}
|
|
57
|
-
</KLabel.Text>
|
|
58
|
-
);
|
|
59
|
-
};
|
|
17
|
+
) => (
|
|
18
|
+
<KLabel.Text typo="TextMdNormal">
|
|
19
|
+
{splitTextByUrls(content, urls).map((part, i) =>
|
|
20
|
+
part.isUrl ? (
|
|
21
|
+
<KLabel.Text
|
|
22
|
+
key={i}
|
|
23
|
+
onPress={() => onPressUrl?.(part.text)}
|
|
24
|
+
color={KColors.primary.normal}
|
|
25
|
+
typo="TextMdMedium"
|
|
26
|
+
>
|
|
27
|
+
{part.text}
|
|
28
|
+
</KLabel.Text>
|
|
29
|
+
) : (
|
|
30
|
+
<KLabel.Text typo="TextMdNormal" key={i}>
|
|
31
|
+
{part.text}
|
|
32
|
+
</KLabel.Text>
|
|
33
|
+
)
|
|
34
|
+
)}
|
|
35
|
+
</KLabel.Text>
|
|
36
|
+
);
|
|
60
37
|
|
|
61
38
|
export const LinkMessage = memo(
|
|
62
39
|
({
|
|
@@ -87,49 +64,7 @@ export const LinkMessage = memo(
|
|
|
87
64
|
)}
|
|
88
65
|
|
|
89
66
|
{!!metadata && (
|
|
90
|
-
<
|
|
91
|
-
style={styles.card}
|
|
92
|
-
onPress={() => metadata.url && onPressUrl?.(metadata.url)}
|
|
93
|
-
>
|
|
94
|
-
{!!metadata.image && (
|
|
95
|
-
<KContainer.View style={styles.cardThumb}>
|
|
96
|
-
<KImage.Base
|
|
97
|
-
uri={metadata.image}
|
|
98
|
-
style={StyleSheet.absoluteFill}
|
|
99
|
-
resizeMode="cover"
|
|
100
|
-
/>
|
|
101
|
-
</KContainer.View>
|
|
102
|
-
)}
|
|
103
|
-
<KContainer.View style={styles.cardBody}>
|
|
104
|
-
{!!metadata.title && (
|
|
105
|
-
<KLabel.Text
|
|
106
|
-
typo="TextNmMedium"
|
|
107
|
-
color={KColors.palette.gray.w900}
|
|
108
|
-
numberOfLines={1}
|
|
109
|
-
>
|
|
110
|
-
{metadata.title}
|
|
111
|
-
</KLabel.Text>
|
|
112
|
-
)}
|
|
113
|
-
{!!metadata.url && (
|
|
114
|
-
<KLabel.Text
|
|
115
|
-
typo="TextXsMedium"
|
|
116
|
-
color={KColors.palette.primary.w400}
|
|
117
|
-
numberOfLines={1}
|
|
118
|
-
>
|
|
119
|
-
{metadata.url}
|
|
120
|
-
</KLabel.Text>
|
|
121
|
-
)}
|
|
122
|
-
{!!metadata.description && (
|
|
123
|
-
<KLabel.Text
|
|
124
|
-
typo="TextXsNormal"
|
|
125
|
-
color={KColors.gray.normal}
|
|
126
|
-
numberOfLines={2}
|
|
127
|
-
>
|
|
128
|
-
{metadata.description}
|
|
129
|
-
</KLabel.Text>
|
|
130
|
-
)}
|
|
131
|
-
</KContainer.View>
|
|
132
|
-
</KContainer.Touchable>
|
|
67
|
+
<LinkPreviewCard metadata={metadata} onPressUrl={onPressUrl} />
|
|
133
68
|
)}
|
|
134
69
|
</KContainer.View>
|
|
135
70
|
</BubbleTailRow>
|
|
@@ -157,6 +92,7 @@ const styles = StyleSheet.create({
|
|
|
157
92
|
textRow: {
|
|
158
93
|
paddingHorizontal: KSpacingValue['0.25rem'],
|
|
159
94
|
},
|
|
95
|
+
timeLabel: { gap: 4, marginTop: 2 },
|
|
160
96
|
card: {
|
|
161
97
|
borderRadius: KSpacingValue['0.75rem'],
|
|
162
98
|
borderWidth: 1,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
2
|
+
import { DeviceEventEmitter, StyleSheet } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
KColors,
|
|
5
|
+
KContainer,
|
|
6
|
+
KImage,
|
|
7
|
+
KLabel,
|
|
8
|
+
KRadiusValue,
|
|
9
|
+
KSpacingValue,
|
|
10
|
+
} from '@droppii/libs';
|
|
11
|
+
import { MessageType, type MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
12
|
+
import {
|
|
13
|
+
getMessagePreviewParts,
|
|
14
|
+
getReplySenderName,
|
|
15
|
+
parseUrlMetadata,
|
|
16
|
+
} from '../../../utils/messageUtils';
|
|
17
|
+
import { getFileIcon, getMimeType } from '../../../utils/imageUtils';
|
|
18
|
+
import { useUserStore } from '../../../store';
|
|
19
|
+
import { useChatDetailContext } from '../../../context';
|
|
20
|
+
import { ChatEvent } from '../../../types/events';
|
|
21
|
+
|
|
22
|
+
interface QuotedMessagePreviewStripProps {
|
|
23
|
+
quotedMessage?: MessageItem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The "replying to ..." strip — shared by QuotedMessage (message.quoteElem)
|
|
28
|
+
* and TextMessage (message.atTextElem?.quoteMessage, for the mention+quote
|
|
29
|
+
* combo). Extracted so both content types render the exact same preview
|
|
30
|
+
* instead of two copies drifting apart.
|
|
31
|
+
*/
|
|
32
|
+
export const QuotedMessagePreviewStrip = memo(
|
|
33
|
+
({ quotedMessage }: QuotedMessagePreviewStripProps) => {
|
|
34
|
+
const { conversation } = useChatDetailContext();
|
|
35
|
+
const currentUserId = useUserStore((s) => s.user?.userID);
|
|
36
|
+
const messagePreview = getMessagePreviewParts(quotedMessage);
|
|
37
|
+
|
|
38
|
+
const imageUrl = useMemo(() => {
|
|
39
|
+
switch (quotedMessage?.contentType) {
|
|
40
|
+
case MessageType.PictureMessage:
|
|
41
|
+
return quotedMessage?.pictureElem?.snapshotPicture?.url || '';
|
|
42
|
+
case MessageType.VideoMessage:
|
|
43
|
+
return quotedMessage?.videoElem?.snapshotUrl || '';
|
|
44
|
+
case MessageType.UrlTextMessage: {
|
|
45
|
+
const metadata = parseUrlMetadata(quotedMessage?.ex);
|
|
46
|
+
return metadata?.image || '';
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}, [quotedMessage]);
|
|
52
|
+
|
|
53
|
+
const iconSource = useMemo(() => {
|
|
54
|
+
if (quotedMessage?.contentType !== MessageType.FileMessage) return '';
|
|
55
|
+
const mimeType = getMimeType(quotedMessage?.fileElem?.fileName || '');
|
|
56
|
+
return getFileIcon(mimeType);
|
|
57
|
+
}, [quotedMessage]);
|
|
58
|
+
|
|
59
|
+
const handlePress = useCallback(() => {
|
|
60
|
+
DeviceEventEmitter.emit(ChatEvent.ScrollToMessage, {
|
|
61
|
+
clientMessageId: quotedMessage?.clientMsgID || '',
|
|
62
|
+
});
|
|
63
|
+
}, [quotedMessage]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<KContainer.Touchable
|
|
67
|
+
style={styles.quotedContainer}
|
|
68
|
+
onPress={handlePress}
|
|
69
|
+
>
|
|
70
|
+
<KContainer.View style={styles.dot} />
|
|
71
|
+
<KContainer.VisibleView visible={!!imageUrl}>
|
|
72
|
+
<KContainer.View style={styles.imageContainer}>
|
|
73
|
+
<KImage.Base uri={imageUrl} style={styles.imageContainer} />
|
|
74
|
+
<KContainer.VisibleView
|
|
75
|
+
visible={quotedMessage?.contentType === MessageType.VideoMessage}
|
|
76
|
+
>
|
|
77
|
+
<KContainer.View style={styles.playIconContainer}>
|
|
78
|
+
<KContainer.View style={styles.playIcon}>
|
|
79
|
+
<KImage.VectorIcons
|
|
80
|
+
name="play-b"
|
|
81
|
+
size={12}
|
|
82
|
+
color={KColors.white}
|
|
83
|
+
/>
|
|
84
|
+
</KContainer.View>
|
|
85
|
+
</KContainer.View>
|
|
86
|
+
</KContainer.VisibleView>
|
|
87
|
+
</KContainer.View>
|
|
88
|
+
</KContainer.VisibleView>
|
|
89
|
+
<KContainer.VisibleView visible={!!iconSource}>
|
|
90
|
+
<KImage.Base
|
|
91
|
+
uri={iconSource}
|
|
92
|
+
style={styles.icon}
|
|
93
|
+
resizeMode="contain"
|
|
94
|
+
/>
|
|
95
|
+
</KContainer.VisibleView>
|
|
96
|
+
<KContainer.View flexS={1} style={styles.content}>
|
|
97
|
+
<KLabel.Text typo="TextNmMedium" numberOfLines={1}>
|
|
98
|
+
{getReplySenderName(
|
|
99
|
+
quotedMessage,
|
|
100
|
+
currentUserId,
|
|
101
|
+
conversation,
|
|
102
|
+
true
|
|
103
|
+
)}
|
|
104
|
+
</KLabel.Text>
|
|
105
|
+
<KLabel.Text
|
|
106
|
+
typo="TextSmNormal"
|
|
107
|
+
color={KColors.gray.light}
|
|
108
|
+
numberOfLines={1}
|
|
109
|
+
{...messagePreview.textProps}
|
|
110
|
+
>
|
|
111
|
+
{messagePreview.text}
|
|
112
|
+
</KLabel.Text>
|
|
113
|
+
</KContainer.View>
|
|
114
|
+
</KContainer.Touchable>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
QuotedMessagePreviewStrip.displayName = 'QuotedMessagePreviewStrip';
|
|
120
|
+
|
|
121
|
+
const styles = StyleSheet.create({
|
|
122
|
+
quotedContainer: {
|
|
123
|
+
backgroundColor: KColors.white,
|
|
124
|
+
flexDirection: 'row',
|
|
125
|
+
alignItems: 'center',
|
|
126
|
+
gap: KSpacingValue['0.5rem'],
|
|
127
|
+
borderTopRightRadius: KRadiusValue['3x'],
|
|
128
|
+
borderBottomRightRadius: KRadiusValue['3x'],
|
|
129
|
+
paddingRight: KSpacingValue['0.5rem'],
|
|
130
|
+
maxWidth: '100%',
|
|
131
|
+
},
|
|
132
|
+
dot: {
|
|
133
|
+
width: 3,
|
|
134
|
+
height: '100%',
|
|
135
|
+
backgroundColor: KColors.palette.grayCool.w200,
|
|
136
|
+
borderRadius: KRadiusValue.x,
|
|
137
|
+
marginRight: -2,
|
|
138
|
+
},
|
|
139
|
+
content: {
|
|
140
|
+
paddingHorizontal: KSpacingValue['0.25rem'],
|
|
141
|
+
paddingVertical: KSpacingValue['0.25rem'],
|
|
142
|
+
},
|
|
143
|
+
icon: {
|
|
144
|
+
width: 24,
|
|
145
|
+
height: 24,
|
|
146
|
+
},
|
|
147
|
+
imageContainer: {
|
|
148
|
+
width: 42,
|
|
149
|
+
height: 42,
|
|
150
|
+
borderRadius: KRadiusValue['2x'],
|
|
151
|
+
},
|
|
152
|
+
playIconContainer: {
|
|
153
|
+
position: 'absolute',
|
|
154
|
+
top: 0,
|
|
155
|
+
left: 0,
|
|
156
|
+
right: 0,
|
|
157
|
+
bottom: 0,
|
|
158
|
+
justifyContent: 'center',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
},
|
|
161
|
+
playIcon: {
|
|
162
|
+
borderRadius: 8,
|
|
163
|
+
padding: KSpacingValue['0.25rem'],
|
|
164
|
+
backgroundColor: KColors.hexToRgba(KColors.black, 0.6),
|
|
165
|
+
justifyContent: 'center',
|
|
166
|
+
alignItems: 'center',
|
|
167
|
+
},
|
|
168
|
+
});
|
|
@@ -1,29 +1,116 @@
|
|
|
1
|
-
import { memo } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { memo, useMemo } from 'react';
|
|
2
|
+
import { Text } 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
10
|
import type { BaseLegendMessageProps } from '../types';
|
|
11
|
+
import { StyleSheet } from 'react-native';
|
|
12
|
+
import { tokenize } from '../../../utils/mentions';
|
|
13
|
+
import { deserializeMentionRanges } from '../../../utils/mentionSerializer';
|
|
14
|
+
import type { IMessageItemEx } from '../../../types/chat';
|
|
15
|
+
import { QuotedMessagePreviewStrip } from '../quotedMessage/QuotedMessagePreviewStrip';
|
|
16
|
+
import { LinkPreviewCard } from '../linkMessage/LinkPreviewCard';
|
|
17
|
+
import { extractUrls, splitTextByUrls } from '../../../utils/url';
|
|
18
|
+
|
|
19
|
+
const parseMessageEx = (ex?: string): IMessageItemEx | undefined => {
|
|
20
|
+
if (!ex) return undefined;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(ex) as IMessageItemEx;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
9
28
|
export const TextMessage = memo(
|
|
10
29
|
({
|
|
11
30
|
message,
|
|
12
31
|
isOutgoing,
|
|
13
32
|
isHighlighted,
|
|
33
|
+
onPressUrl,
|
|
14
34
|
groupPosition,
|
|
15
|
-
}: BaseLegendMessageProps) => {
|
|
35
|
+
}: BaseLegendMessageProps & { onPressUrl?: (url: string) => void }) => {
|
|
16
36
|
const messageText = getMessageText(message);
|
|
17
37
|
|
|
38
|
+
const isAtTextMessage = message.contentType === MessageType.AtTextMessage;
|
|
39
|
+
|
|
40
|
+
// Both the reply-preview strip and the link-preview card below piggyback
|
|
41
|
+
// on an AtTextMessage instead of getting their own content type — OpenIM
|
|
42
|
+
// content types are mutually exclusive (AtText vs Quote vs UrlText), so
|
|
43
|
+
// once mention picks AtText (to keep native tag/notify semantics — see
|
|
44
|
+
// useSendMessage.ts), quote rides along via the native
|
|
45
|
+
// AtTextElem.quoteMessage field and the URL card rides along via our own
|
|
46
|
+
// ex.urlMetadata channel (already attached unconditionally on send).
|
|
47
|
+
const quotedMessage = isAtTextMessage
|
|
48
|
+
? message.atTextElem?.quoteMessage
|
|
49
|
+
: undefined;
|
|
50
|
+
|
|
51
|
+
const parsedEx = useMemo(
|
|
52
|
+
() => (isAtTextMessage ? parseMessageEx(message.ex) : undefined),
|
|
53
|
+
[isAtTextMessage, message.ex]
|
|
54
|
+
);
|
|
55
|
+
const urlMetadata = parsedEx?.urlMetadata;
|
|
56
|
+
|
|
57
|
+
const mentions = useMemo(() => {
|
|
58
|
+
if (!isAtTextMessage || !message.atTextElem) return [];
|
|
59
|
+
// Wrapped under `offsets` — matches the wire shape other Droppii
|
|
60
|
+
// clients (e.g. the web/CRM app) already send (see
|
|
61
|
+
// IMessageItemEx.mentions doc comment for how this was confirmed).
|
|
62
|
+
return deserializeMentionRanges(
|
|
63
|
+
message.atTextElem,
|
|
64
|
+
parsedEx?.mentions?.offsets
|
|
65
|
+
);
|
|
66
|
+
}, [isAtTextMessage, message.atTextElem, parsedEx]);
|
|
67
|
+
|
|
68
|
+
const segments = useMemo(
|
|
69
|
+
() => (mentions.length > 0 ? tokenize(messageText, mentions) : null),
|
|
70
|
+
[mentions, messageText]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Scoped to the mention+URL combo only (`segments` implies mentions are
|
|
74
|
+
// present) — a plain TextMessage with no mention falls through to
|
|
75
|
+
// `messageText` unchanged below, same as before this feature existed.
|
|
76
|
+
// Widening this to auto-linkify every TextMessage is a separate,
|
|
77
|
+
// deliberate product decision, not a side effect of this fix.
|
|
78
|
+
const urls = useMemo(
|
|
79
|
+
() => (segments ? extractUrls(messageText) : []),
|
|
80
|
+
[segments, messageText]
|
|
81
|
+
);
|
|
82
|
+
|
|
18
83
|
if (!messageText?.trim()) {
|
|
19
84
|
return null;
|
|
20
85
|
}
|
|
21
86
|
|
|
87
|
+
// A plain-text run (the space between two mentions) may itself contain
|
|
88
|
+
// a URL — split further so it gets the same tappable/colored treatment
|
|
89
|
+
// LinkMessage gives a pure URL message, instead of sitting there as
|
|
90
|
+
// unstyled black text next to a colored mention.
|
|
91
|
+
const renderTextRun = (text: string, keyPrefix: string) => {
|
|
92
|
+
if (!urls.length) return <Text key={keyPrefix}>{text}</Text>;
|
|
93
|
+
return splitTextByUrls(text, urls).map((part, i) =>
|
|
94
|
+
part.isUrl ? (
|
|
95
|
+
<Text
|
|
96
|
+
key={`${keyPrefix}-url-${i}`}
|
|
97
|
+
style={urlTextStyle}
|
|
98
|
+
onPress={() => onPressUrl?.(part.text)}
|
|
99
|
+
>
|
|
100
|
+
{part.text}
|
|
101
|
+
</Text>
|
|
102
|
+
) : (
|
|
103
|
+
<Text key={`${keyPrefix}-text-${i}`}>{part.text}</Text>
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
22
108
|
return (
|
|
23
109
|
<BubbleTailRow isOutgoing={isOutgoing} groupPosition={groupPosition}>
|
|
24
110
|
<KContainer.View
|
|
25
111
|
style={[
|
|
26
112
|
messageStyles.bubble,
|
|
113
|
+
styles.bubbleContent,
|
|
27
114
|
isOutgoing
|
|
28
115
|
? messageStyles.bubbleSent
|
|
29
116
|
: messageStyles.bubbleReceived,
|
|
@@ -31,9 +118,25 @@ export const TextMessage = memo(
|
|
|
31
118
|
getTailCornerStyle(isOutgoing, groupPosition),
|
|
32
119
|
]}
|
|
33
120
|
>
|
|
121
|
+
{quotedMessage && (
|
|
122
|
+
<QuotedMessagePreviewStrip quotedMessage={quotedMessage} />
|
|
123
|
+
)}
|
|
34
124
|
<KLabel.Text typo="TextMdNormal" color={CHAT_BUBBLE_COLORS.text}>
|
|
35
|
-
{
|
|
125
|
+
{segments
|
|
126
|
+
? segments.map((segment, index) =>
|
|
127
|
+
segment.type === 'mention' ? (
|
|
128
|
+
<Text key={segment.mention.id} style={mentionTextStyle}>
|
|
129
|
+
{`@${segment.mention.displayName}`}
|
|
130
|
+
</Text>
|
|
131
|
+
) : (
|
|
132
|
+
renderTextRun(segment.text, `seg-${index}`)
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
: messageText}
|
|
36
136
|
</KLabel.Text>
|
|
137
|
+
{!!urlMetadata && (
|
|
138
|
+
<LinkPreviewCard metadata={urlMetadata} onPressUrl={onPressUrl} />
|
|
139
|
+
)}
|
|
37
140
|
</KContainer.View>
|
|
38
141
|
</BubbleTailRow>
|
|
39
142
|
);
|
|
@@ -41,3 +144,20 @@ export const TextMessage = memo(
|
|
|
41
144
|
);
|
|
42
145
|
|
|
43
146
|
TextMessage.displayName = 'TextMessage';
|
|
147
|
+
|
|
148
|
+
const styles = StyleSheet.create({
|
|
149
|
+
bubbleContent: { gap: 4 },
|
|
150
|
+
timeLabel: { gap: 4, marginTop: 2 },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Same highlight color as the composer (MentionHighlightAdapter) and the
|
|
154
|
+
// Figma screenshots. This is a read-only <Text>, not an editable
|
|
155
|
+
// TextInput, so none of the nested-Text-in-TextInput bug class applies —
|
|
156
|
+
// nesting styled Text here is unconditionally safe.
|
|
157
|
+
const mentionTextStyle = { color: KColors.palette.primary.w400 };
|
|
158
|
+
|
|
159
|
+
// Same color LinkMessage uses for a URL substring — a mention message's
|
|
160
|
+
// plain-text run can contain a URL too (mention+URL combo, see
|
|
161
|
+
// useSendMessage.ts's doc comment), and it should look tappable/colored
|
|
162
|
+
// the same way there as it does in a pure UrlTextMessage.
|
|
163
|
+
const urlTextStyle = { color: KColors.primary.normal };
|
|
@@ -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) =>
|