@droppii-org/chat-mobile 0.2.80 → 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.
Files changed (228) hide show
  1. package/lib/module/components/ThreadCard/MentionBadge.js +24 -0
  2. package/lib/module/components/ThreadCard/MentionBadge.js.map +1 -0
  3. package/lib/module/components/ThreadCard/ThreadCard.js +5 -1
  4. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  5. package/lib/module/components/messages/linkMessage/LinkPreviewCard.js +67 -0
  6. package/lib/module/components/messages/linkMessage/LinkPreviewCard.js.map +1 -0
  7. package/lib/module/components/messages/linkMessage/index.js +21 -68
  8. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  9. package/lib/module/components/messages/quotedMessage/QuotedMessagePreviewStrip.js +152 -0
  10. package/lib/module/components/messages/quotedMessage/QuotedMessagePreviewStrip.js.map +1 -0
  11. package/lib/module/components/messages/textMessage/index.js +102 -8
  12. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  13. package/lib/module/config/api-endpoints.js +2 -1
  14. package/lib/module/config/api-endpoints.js.map +1 -1
  15. package/lib/module/core/useChatListener.js +1 -1
  16. package/lib/module/core/useChatListener.js.map +1 -1
  17. package/lib/module/hooks/contacts/useSearchContacts.js +50 -0
  18. package/lib/module/hooks/contacts/useSearchContacts.js.map +1 -0
  19. package/lib/module/hooks/message/useSendMessage.js +64 -3
  20. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  21. package/lib/module/hooks/query-keys.js +8 -0
  22. package/lib/module/hooks/query-keys.js.map +1 -1
  23. package/lib/module/hooks/useChatCompose.js +3 -2
  24. package/lib/module/hooks/useChatCompose.js.map +1 -1
  25. package/lib/module/screens/SearchConversation/ContactResultItem.js +119 -0
  26. package/lib/module/screens/SearchConversation/ContactResultItem.js.map +1 -0
  27. package/lib/module/screens/SearchConversation/SearchAllResult.js +26 -20
  28. package/lib/module/screens/SearchConversation/SearchAllResult.js.map +1 -1
  29. package/lib/module/screens/SearchConversation/SearchCommunityResult.js +79 -0
  30. package/lib/module/screens/SearchConversation/SearchCommunityResult.js.map +1 -0
  31. package/lib/module/screens/SearchConversation/SearchContactsResult.js +117 -0
  32. package/lib/module/screens/SearchConversation/SearchContactsResult.js.map +1 -0
  33. package/lib/module/screens/SearchConversation/SearchEmpty.js +2 -0
  34. package/lib/module/screens/SearchConversation/SearchEmpty.js.map +1 -1
  35. package/lib/module/screens/SearchConversation/SearchMessagesResult.js +1 -8
  36. package/lib/module/screens/SearchConversation/SearchMessagesResult.js.map +1 -1
  37. package/lib/module/screens/SearchConversation/SearchResultItem.js +2 -0
  38. package/lib/module/screens/SearchConversation/SearchResultItem.js.map +1 -1
  39. package/lib/module/screens/SearchConversation/TabResult.js +24 -7
  40. package/lib/module/screens/SearchConversation/TabResult.js.map +1 -1
  41. package/lib/module/screens/SearchConversation/index.js +6 -2
  42. package/lib/module/screens/SearchConversation/index.js.map +1 -1
  43. package/lib/module/screens/SearchConversation/types.js +1 -0
  44. package/lib/module/screens/SearchConversation/types.js.map +1 -1
  45. package/lib/module/screens/chat-detail/ChatComposer.js +170 -48
  46. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  47. package/lib/module/screens/chat-detail/ChatListLegend.js +1 -0
  48. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  49. package/lib/module/screens/chat-detail/components/ChatMessageInput.js +23 -2
  50. package/lib/module/screens/chat-detail/components/ChatMessageInput.js.map +1 -1
  51. package/lib/module/screens/chat-detail/components/MentionHighlightAdapter.js +51 -0
  52. package/lib/module/screens/chat-detail/components/MentionHighlightAdapter.js.map +1 -0
  53. package/lib/module/screens/chat-detail/components/MentionSuggestionItem.js +49 -0
  54. package/lib/module/screens/chat-detail/components/MentionSuggestionItem.js.map +1 -0
  55. package/lib/module/screens/chat-detail/components/MentionSuggestionList.js +114 -0
  56. package/lib/module/screens/chat-detail/components/MentionSuggestionList.js.map +1 -0
  57. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +8 -0
  58. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
  59. package/lib/module/screens/chat-detail/hooks/useMentionComposer.js +251 -0
  60. package/lib/module/screens/chat-detail/hooks/useMentionComposer.js.map +1 -0
  61. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +5 -1
  62. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  63. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +11 -1
  64. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
  65. package/lib/module/services/endpoints.js +2 -1
  66. package/lib/module/services/endpoints.js.map +1 -1
  67. package/lib/module/services/mentionSearch.js +49 -0
  68. package/lib/module/services/mentionSearch.js.map +1 -0
  69. package/lib/module/services/message.js +6 -1
  70. package/lib/module/services/message.js.map +1 -1
  71. package/lib/module/translation/resources/i18n.js +11 -0
  72. package/lib/module/translation/resources/i18n.js.map +1 -1
  73. package/lib/module/types/chat.js +8 -0
  74. package/lib/module/types/chat.js.map +1 -1
  75. package/lib/module/types/mention.js +2 -0
  76. package/lib/module/types/mention.js.map +1 -0
  77. package/lib/module/utils/dateTimeUtils.js +10 -0
  78. package/lib/module/utils/dateTimeUtils.js.map +1 -1
  79. package/lib/module/utils/highlightSearch.js +6 -2
  80. package/lib/module/utils/highlightSearch.js.map +1 -1
  81. package/lib/module/utils/mentionSerializer.js +113 -0
  82. package/lib/module/utils/mentionSerializer.js.map +1 -0
  83. package/lib/module/utils/mentions/index.js +7 -0
  84. package/lib/module/utils/mentions/index.js.map +1 -0
  85. package/lib/module/utils/mentions/mentionDetector.js +49 -0
  86. package/lib/module/utils/mentions/mentionDetector.js.map +1 -0
  87. package/lib/module/utils/mentions/mentionDiff.js +66 -0
  88. package/lib/module/utils/mentions/mentionDiff.js.map +1 -0
  89. package/lib/module/utils/mentions/mentionRange.js +188 -0
  90. package/lib/module/utils/mentions/mentionRange.js.map +1 -0
  91. package/lib/module/utils/mentions/mentionTokenize.js +50 -0
  92. package/lib/module/utils/mentions/mentionTokenize.js.map +1 -0
  93. package/lib/module/utils/messageUtils.js +16 -3
  94. package/lib/module/utils/messageUtils.js.map +1 -1
  95. package/lib/module/utils/url.js +33 -0
  96. package/lib/module/utils/url.js.map +1 -1
  97. package/lib/typescript/src/components/ThreadCard/MentionBadge.d.ts +7 -0
  98. package/lib/typescript/src/components/ThreadCard/MentionBadge.d.ts.map +1 -0
  99. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  100. package/lib/typescript/src/components/messages/linkMessage/LinkPreviewCard.d.ts +16 -0
  101. package/lib/typescript/src/components/messages/linkMessage/LinkPreviewCard.d.ts.map +1 -0
  102. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.d.ts +13 -0
  104. package/lib/typescript/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.d.ts.map +1 -0
  105. package/lib/typescript/src/components/messages/textMessage/index.d.ts +3 -1
  106. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  107. package/lib/typescript/src/config/api-endpoints.d.ts +1 -0
  108. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  109. package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts +221 -0
  110. package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts.map +1 -0
  111. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +5 -2
  112. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  113. package/lib/typescript/src/hooks/query-keys.d.ts +8 -0
  114. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  115. package/lib/typescript/src/hooks/useChatCompose.d.ts +2 -1
  116. package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
  117. package/lib/typescript/src/screens/SearchConversation/ContactResultItem.d.ts +9 -0
  118. package/lib/typescript/src/screens/SearchConversation/ContactResultItem.d.ts.map +1 -0
  119. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts +3 -2
  120. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts.map +1 -1
  121. package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts +7 -0
  122. package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts.map +1 -0
  123. package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts +8 -0
  124. package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts.map +1 -0
  125. package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts.map +1 -1
  126. package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts.map +1 -1
  127. package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts.map +1 -1
  128. package/lib/typescript/src/screens/SearchConversation/TabResult.d.ts +4 -2
  129. package/lib/typescript/src/screens/SearchConversation/TabResult.d.ts.map +1 -1
  130. package/lib/typescript/src/screens/SearchConversation/index.d.ts +4 -2
  131. package/lib/typescript/src/screens/SearchConversation/index.d.ts.map +1 -1
  132. package/lib/typescript/src/screens/SearchConversation/types.d.ts +2 -1
  133. package/lib/typescript/src/screens/SearchConversation/types.d.ts.map +1 -1
  134. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  135. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  136. package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts +10 -2
  137. package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts.map +1 -1
  138. package/lib/typescript/src/screens/chat-detail/components/MentionHighlightAdapter.d.ts +29 -0
  139. package/lib/typescript/src/screens/chat-detail/components/MentionHighlightAdapter.d.ts.map +1 -0
  140. package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionItem.d.ts +8 -0
  141. package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionItem.d.ts.map +1 -0
  142. package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionList.d.ts +12 -0
  143. package/lib/typescript/src/screens/chat-detail/components/MentionSuggestionList.d.ts.map +1 -0
  144. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +1 -1
  145. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
  146. package/lib/typescript/src/screens/chat-detail/hooks/useMentionComposer.d.ts +48 -0
  147. package/lib/typescript/src/screens/chat-detail/hooks/useMentionComposer.d.ts.map +1 -0
  148. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  149. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
  150. package/lib/typescript/src/services/endpoints.d.ts +1 -0
  151. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  152. package/lib/typescript/src/services/mentionSearch.d.ts +15 -0
  153. package/lib/typescript/src/services/mentionSearch.d.ts.map +1 -0
  154. package/lib/typescript/src/services/message.d.ts +1 -1
  155. package/lib/typescript/src/services/message.d.ts.map +1 -1
  156. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  157. package/lib/typescript/src/types/chat.d.ts +50 -0
  158. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  159. package/lib/typescript/src/types/mention.d.ts +65 -0
  160. package/lib/typescript/src/types/mention.d.ts.map +1 -0
  161. package/lib/typescript/src/utils/dateTimeUtils.d.ts +2 -0
  162. package/lib/typescript/src/utils/dateTimeUtils.d.ts.map +1 -1
  163. package/lib/typescript/src/utils/highlightSearch.d.ts.map +1 -1
  164. package/lib/typescript/src/utils/mentionSerializer.d.ts +78 -0
  165. package/lib/typescript/src/utils/mentionSerializer.d.ts.map +1 -0
  166. package/lib/typescript/src/utils/mentions/index.d.ts +5 -0
  167. package/lib/typescript/src/utils/mentions/index.d.ts.map +1 -0
  168. package/lib/typescript/src/utils/mentions/mentionDetector.d.ts +18 -0
  169. package/lib/typescript/src/utils/mentions/mentionDetector.d.ts.map +1 -0
  170. package/lib/typescript/src/utils/mentions/mentionDiff.d.ts +20 -0
  171. package/lib/typescript/src/utils/mentions/mentionDiff.d.ts.map +1 -0
  172. package/lib/typescript/src/utils/mentions/mentionRange.d.ts +86 -0
  173. package/lib/typescript/src/utils/mentions/mentionRange.d.ts.map +1 -0
  174. package/lib/typescript/src/utils/mentions/mentionTokenize.d.ts +29 -0
  175. package/lib/typescript/src/utils/mentions/mentionTokenize.d.ts.map +1 -0
  176. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  177. package/lib/typescript/src/utils/url.d.ts +12 -0
  178. package/lib/typescript/src/utils/url.d.ts.map +1 -1
  179. package/package.json +3 -1
  180. package/src/build-ignore.d.ts +2 -0
  181. package/src/components/ThreadCard/MentionBadge.tsx +28 -0
  182. package/src/components/ThreadCard/ThreadCard.tsx +3 -0
  183. package/src/components/messages/linkMessage/LinkPreviewCard.tsx +92 -0
  184. package/src/components/messages/linkMessage/index.tsx +25 -89
  185. package/src/components/messages/quotedMessage/QuotedMessagePreviewStrip.tsx +168 -0
  186. package/src/components/messages/textMessage/index.tsx +124 -4
  187. package/src/config/api-endpoints.ts +2 -0
  188. package/src/core/useChatListener.ts +1 -1
  189. package/src/hooks/contacts/useSearchContacts.ts +66 -0
  190. package/src/hooks/message/useSendMessage.ts +60 -1
  191. package/src/hooks/query-keys.ts +12 -0
  192. package/src/hooks/useChatCompose.ts +115 -107
  193. package/src/screens/SearchConversation/ContactResultItem.tsx +128 -0
  194. package/src/screens/SearchConversation/SearchAllResult.tsx +38 -24
  195. package/src/screens/SearchConversation/SearchCommunityResult.tsx +96 -0
  196. package/src/screens/SearchConversation/SearchContactsResult.tsx +155 -0
  197. package/src/screens/SearchConversation/SearchEmpty.tsx +2 -0
  198. package/src/screens/SearchConversation/SearchMessagesResult.tsx +1 -6
  199. package/src/screens/SearchConversation/SearchResultItem.tsx +6 -1
  200. package/src/screens/SearchConversation/TabResult.tsx +40 -5
  201. package/src/screens/SearchConversation/index.tsx +10 -2
  202. package/src/screens/SearchConversation/types.tsx +1 -0
  203. package/src/screens/chat-detail/ChatComposer.tsx +201 -67
  204. package/src/screens/chat-detail/ChatListLegend.tsx +1 -0
  205. package/src/screens/chat-detail/components/ChatMessageInput.tsx +35 -2
  206. package/src/screens/chat-detail/components/MentionHighlightAdapter.tsx +57 -0
  207. package/src/screens/chat-detail/components/MentionSuggestionItem.tsx +53 -0
  208. package/src/screens/chat-detail/components/MentionSuggestionList.tsx +133 -0
  209. package/src/screens/chat-detail/hooks/useChatComposerState.ts +7 -1
  210. package/src/screens/chat-detail/hooks/useMentionComposer.ts +344 -0
  211. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +7 -1
  212. package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +22 -4
  213. package/src/services/endpoints.ts +1 -0
  214. package/src/services/mentionSearch.ts +57 -0
  215. package/src/services/message.ts +9 -1
  216. package/src/translation/resources/i18n.ts +12 -0
  217. package/src/types/chat.ts +52 -0
  218. package/src/types/mention.ts +70 -0
  219. package/src/utils/dateTimeUtils.ts +15 -0
  220. package/src/utils/highlightSearch.ts +12 -2
  221. package/src/utils/mentionSerializer.ts +155 -0
  222. package/src/utils/mentions/index.ts +16 -0
  223. package/src/utils/mentions/mentionDetector.ts +58 -0
  224. package/src/utils/mentions/mentionDiff.ts +95 -0
  225. package/src/utils/mentions/mentionRange.ts +207 -0
  226. package/src/utils/mentions/mentionTokenize.ts +53 -0
  227. package/src/utils/messageUtils.ts +21 -10
  228. package/src/utils/url.ts +30 -0
@@ -54,7 +54,7 @@ export const useChatListener = () => {
54
54
 
55
55
  const onConversationChanged = useCallback(
56
56
  (conversations: ConversationItem[]) => {
57
- // console.log('[Listener] OnConversationChanged', conversations);
57
+ console.log('[Listener] OnConversationChanged', conversations);
58
58
 
59
59
  // return; // TEMPORARILY DISABLED: OnConversationChanged/OnNewConversation can
60
60
  const oldMap = useConversationStore.getState().map;
@@ -0,0 +1,66 @@
1
+ import { useInfiniteQuery } from '@tanstack/react-query';
2
+ import { useMemo } from 'react';
3
+ import { contactQueryKeys } from '../query-keys';
4
+ import { apiInstance } from '../../services/apis';
5
+ import { ENDPOINTS } from '../../services/endpoints';
6
+ import type {
7
+ ContactCategory,
8
+ DChatApplicationType,
9
+ IContactSearchItem,
10
+ IContactSearchRequest,
11
+ } from '../../types/chat';
12
+ import type { BaseResponse } from '../../types/auth';
13
+ import { PAGE_SIZE } from '../../constants';
14
+
15
+ interface IUseSearchContactsProps {
16
+ query: string;
17
+ applicationType: DChatApplicationType;
18
+ type?: ContactCategory;
19
+ pageSize?: number;
20
+ }
21
+
22
+ export const useSearchContacts = ({
23
+ query,
24
+ applicationType,
25
+ type,
26
+ pageSize = PAGE_SIZE,
27
+ }: IUseSearchContactsProps) => {
28
+ const { data, ...rest } = useInfiniteQuery({
29
+ queryKey: [contactQueryKeys.search(query.trim(), type), pageSize],
30
+ queryFn: async ({ pageParam = 1 }) => {
31
+ const params: IContactSearchRequest = {
32
+ page: pageParam,
33
+ pageSize,
34
+ applicationType,
35
+ searchTerm: query.trim(),
36
+ type,
37
+ };
38
+ const res = await apiInstance?.post<BaseResponse<IContactSearchItem[]>>(
39
+ ENDPOINTS.chatService.searchContacts(),
40
+ params
41
+ );
42
+ return res?.data;
43
+ },
44
+ getNextPageParam: (lastPage) => {
45
+ const currentPage = lastPage?.pageable?.pageNumber || 1;
46
+ const dataLength = lastPage?.data?.length || 0;
47
+ return dataLength < pageSize ? undefined : currentPage + 1;
48
+ },
49
+ initialPageParam: 1,
50
+ enabled: !!query.trim() && !!applicationType,
51
+ });
52
+
53
+ const dataFlatten = useMemo<IContactSearchItem[]>(() => {
54
+ if (!query.trim() || !data) return [];
55
+ return (
56
+ data?.pages
57
+ ?.flatMap?.((page) => page?.data)
58
+ ?.filter((item): item is IContactSearchItem => item !== undefined) || []
59
+ );
60
+ }, [data, query]);
61
+
62
+ return {
63
+ dataFlatten,
64
+ ...rest,
65
+ };
66
+ };
@@ -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
- if (quotedMessage) {
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) => {
@@ -62,6 +62,18 @@ 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
+
71
+ export const contactQueryKeys = {
72
+ all: ['contacts'] as const,
73
+ search: (query: string, type?: string) =>
74
+ [...contactQueryKeys.all, 'search', query, type] as const,
75
+ };
76
+
65
77
  export const groupQueryKeys = {
66
78
  all: ['groups'] as const,
67
79
  public: () => [...groupQueryKeys.all, 'public'] as const,
@@ -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(async () => {
49
- const { selectedImages, clearAllImages } = imageAttachment;
50
- const { selectedFiles, clearAllFiles } = fileAttachment;
51
- const urlMeta = linkPreview.isVisible ? linkPreview.metadata : undefined;
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
- // --- Attachments ---
54
- if (selectedImages.length > 0 || selectedFiles.length > 0) {
55
- const actualImages = selectedImages.filter(
56
- (img: ImageWithUploadStatus) => !isVideoFromPicker(img.image)
57
- );
58
- const videos = selectedImages.filter((img: ImageWithUploadStatus) =>
59
- isVideoFromPicker(img.image)
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
- const uploadedImages = actualImages.filter(
63
- (img: ImageWithUploadStatus) => img.uploadedUrl
64
- );
65
- const uploadedVideos = videos.filter(
66
- (v) => v.uploadedUrl
67
- ) as unknown as VideoWithUploadStatus[];
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
- // Thumbnails are generated + uploaded at pick time (useImageAttachment).
70
- // Only fall back to generating here for videos that don't already have one
71
- // (e.g. thumbnail generation failed or hadn't finished before send).
72
- const videosMissingThumbnail = uploadedVideos.filter(
73
- (v) => !v.thumbnailUrl
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
- // Send images + videos
83
- if (uploadedImages.length > 0 || uploadedVideos.length > 0) {
84
- const attachmentItems = [
85
- ...uploadedImages.map((img: ImageWithUploadStatus) => ({
86
- ...img,
87
- type: 'image' as const,
88
- })),
89
- ...uploadedVideos.map(
90
- (vid: VideoWithUploadStatus, vidIdx: number) => {
91
- const videoId = vid.uploadedUrl || `video-${vidIdx}`;
92
- const thumb = videoThumbnails[videoId];
93
- return {
94
- type: 'video' as const,
95
- video: {
96
- path: vid.image.path,
97
- filename: vid.image.filename ?? undefined,
98
- size: vid.image.size ?? undefined,
99
- width: vid.image.width ?? undefined,
100
- height: vid.image.height ?? undefined,
101
- duration: vid.image.duration ?? undefined,
102
- },
103
- uploadProgress: vid.uploadProgress,
104
- error: vid.error,
105
- isUploading: vid.isUploading,
106
- uploadedUrl: vid.uploadedUrl,
107
- thumbnailUrl: vid.thumbnailUrl ?? thumb?.url,
108
- thumbnailSize: vid.thumbnailSize ?? thumb?.size,
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
- // Clear composer immediately — message is already in chat list with Sending status.
115
- // User retries from the bubble, not the composer.
116
- ImagePicker.clean().catch(() => {});
117
- clearAllImages();
118
- // Awaited so files (lower priority per attachment-priority config)
119
- // only start sending after images/videos are done.
120
- await sendAttachments(attachmentItems, undefined);
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
- // Send each file as its own message (never merged), in priority order
124
- // after images/videos, one at a time to preserve pick order.
125
- const uploadedFiles = selectedFiles.filter(
126
- (f) => f.uploadedUrl && !f.error
127
- );
128
- if (uploadedFiles.length > 0) {
129
- clearAllFiles();
130
- for (const file of uploadedFiles) {
131
- await sendAttachments(
132
- [
133
- {
134
- type: 'file' as const,
135
- file: { path: file.uri, filename: file.name, size: file.size },
136
- uploadProgress: 100,
137
- error: null,
138
- isUploading: false,
139
- uploadedUrl: file.uploadedUrl,
140
- },
141
- ],
142
- undefined
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
- // --- Text / link preview ---
149
- if (text.trim() || urlMeta) {
150
- const plainText = text.trim();
151
- // Clear composer immediately — message is already in chat list with Sending status.
152
- // User retries from the bubble, not the composer.
153
- setText('');
154
- await sendMessage({ plainText, urlMetadata: urlMeta });
155
- }
156
- }, [
157
- imageAttachment,
158
- fileAttachment,
159
- linkPreview.isVisible,
160
- linkPreview.metadata,
161
- sendAttachments,
162
- sendMessage,
163
- text,
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
@@ -0,0 +1,128 @@
1
+ import {
2
+ KColors,
3
+ KContainer,
4
+ KImage,
5
+ KLabel,
6
+ KSpacingValue,
7
+ } from '@droppii/libs';
8
+ import { memo, useCallback, useMemo } from 'react';
9
+ import { StyleSheet } from 'react-native';
10
+ import { PeerType } from '@droppii/openim-rn-client-sdk';
11
+ import { ContactCategory, type IContactSearchItem } from '../../types/chat';
12
+ import { AvatarSection, NamePrefixIcon } from '../../components/ThreadCard';
13
+ import { formatLastOnlineTime } from '../../utils/dateTimeUtils';
14
+ import {
15
+ highlightSearch,
16
+ type HighlightResult,
17
+ } from '../../utils/highlightSearch';
18
+ import { trans } from '../../translation';
19
+ import isEmpty from 'lodash/isEmpty';
20
+
21
+ const CONTACT_PEER_TYPE: Record<ContactCategory, PeerType> = {
22
+ [ContactCategory.Customer]: PeerType.Customer,
23
+ [ContactCategory.Seller]: PeerType.User,
24
+ [ContactCategory.Pdp]: PeerType.Bot,
25
+ [ContactCategory.Group]: PeerType.Group,
26
+ };
27
+
28
+ interface IContactResultItemProps {
29
+ item: IContactSearchItem;
30
+ onPressSearchContact?: (item: IContactSearchItem) => void;
31
+ searchTerm?: string;
32
+ }
33
+
34
+ const ContactResultItem = memo((props: IContactResultItemProps) => {
35
+ const { item, onPressSearchContact, searchTerm } = props || {};
36
+ const peerType = CONTACT_PEER_TYPE[item?.type];
37
+
38
+ const subtitle = useMemo(() => {
39
+ if (item?.type === ContactCategory.Group) {
40
+ const description = !isEmpty(item?.description?.trim())
41
+ ? item?.description
42
+ : trans('group:no_description');
43
+ return `${trans('group:member_count', { count: item?.memberCount ?? 0 })} • ${description}`;
44
+ }
45
+ if (item?.type === ContactCategory.Pdp) {
46
+ return '';
47
+ }
48
+ return formatLastOnlineTime(item?.lastOnlineTime ?? null);
49
+ }, [item?.type, item?.description, item?.memberCount, item?.lastOnlineTime]);
50
+
51
+ const highlightResult: HighlightResult | null = useMemo(() => {
52
+ if (isEmpty(searchTerm?.trim())) return null;
53
+ return highlightSearch(item?.targetName || '', searchTerm || '');
54
+ }, [item?.targetName, searchTerm]);
55
+
56
+ const handlePressItem = useCallback(() => {
57
+ onPressSearchContact?.(item);
58
+ }, [onPressSearchContact, item]);
59
+
60
+ return (
61
+ <KContainer.Touchable
62
+ row
63
+ justifyContent="space-between"
64
+ alignItems
65
+ paddingV={'0.75rem'}
66
+ style={styles.container}
67
+ onPress={handlePressItem}
68
+ brBW={1}
69
+ brBC={KColors.border.light}
70
+ >
71
+ <KContainer.View row alignItems flex>
72
+ <AvatarSection
73
+ avatar={item?.targetAvatar ?? null}
74
+ fullName={item?.targetName}
75
+ peerType={peerType}
76
+ size="nm"
77
+ />
78
+ <KContainer.View flex marginL={'0.5rem'}>
79
+ <KContainer.View row alignItems>
80
+ <NamePrefixIcon peerType={peerType} />
81
+ {highlightResult != null ? (
82
+ <KLabel.Text typo="TextMdMedium" numberOfLines={1} flex>
83
+ {highlightResult?.before || ''}
84
+ <KLabel.Text typo="TextMdMedium" color={KColors.primary.normal}>
85
+ {highlightResult?.match || ''}
86
+ </KLabel.Text>
87
+ <KLabel.Text typo="TextMdMedium">
88
+ {highlightResult?.after || ''}
89
+ </KLabel.Text>
90
+ </KLabel.Text>
91
+ ) : (
92
+ <KLabel.Text typo="TextMdMedium" numberOfLines={1} flex>
93
+ {item?.targetName || ''}
94
+ </KLabel.Text>
95
+ )}
96
+ </KContainer.View>
97
+ <KContainer.VisibleView visible={!!subtitle}>
98
+ <KLabel.Text
99
+ typo="TextXsNormal"
100
+ color={KColors.gray.light}
101
+ numberOfLines={1}
102
+ >
103
+ {subtitle}
104
+ </KLabel.Text>
105
+ </KContainer.VisibleView>
106
+ </KContainer.View>
107
+ </KContainer.View>
108
+
109
+ <KContainer.VisibleView visible={false}>
110
+ <KContainer.View center>
111
+ <KImage.VectorIcons
112
+ name="more-horizontal-b"
113
+ size={24}
114
+ color={KColors.gray.normal}
115
+ />
116
+ </KContainer.View>
117
+ </KContainer.VisibleView>
118
+ </KContainer.Touchable>
119
+ );
120
+ });
121
+
122
+ export default ContactResultItem;
123
+
124
+ const styles = StyleSheet.create({
125
+ container: {
126
+ gap: KSpacingValue['1rem'],
127
+ },
128
+ });