@droppii-org/chat-mobile 0.2.6 → 0.2.7

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 (116) hide show
  1. package/lib/module/components/ThreadCard/AvatarSection.js +4 -4
  2. package/lib/module/components/ThreadCard/AvatarSection.js.map +1 -1
  3. package/lib/module/components/ThreadCard/NamePrefixIcon.js +13 -16
  4. package/lib/module/components/ThreadCard/NamePrefixIcon.js.map +1 -1
  5. package/lib/module/components/ThreadCard/ThreadCard.js +13 -33
  6. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  7. package/lib/module/context/ChatContext.js +7 -6
  8. package/lib/module/context/ChatContext.js.map +1 -1
  9. package/lib/module/hooks/message/useSendMessage.js +101 -0
  10. package/lib/module/hooks/message/useSendMessage.js.map +1 -0
  11. package/lib/module/hooks/useChatMessages.js +37 -119
  12. package/lib/module/hooks/useChatMessages.js.map +1 -1
  13. package/lib/module/hooks/useConversationList.js +29 -17
  14. package/lib/module/hooks/useConversationList.js.map +1 -1
  15. package/lib/module/hooks/useLinkPreview/useLinkPreview.js +3 -2
  16. package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -1
  17. package/lib/module/screens/chat-detail/ChatComposer.js +2 -2
  18. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  19. package/lib/module/screens/chat-detail/ChatDetail.js +14 -10
  20. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  21. package/lib/module/screens/chat-detail/ChatDetailHeader.js +5 -8
  22. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  23. package/lib/module/screens/chat-detail/ChatLinkPreview.js +1 -1
  24. package/lib/module/screens/chat-detail/ChatLinkPreview.js.map +1 -1
  25. package/lib/module/screens/chat-detail/ChatListLegend.js +0 -2
  26. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  27. package/lib/module/screens/chat-detail/conversationHeader.utils.js +7 -9
  28. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -1
  29. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +10 -23
  30. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  31. package/lib/module/screens/chat-detail/legend/message-types.js +128 -6
  32. package/lib/module/screens/chat-detail/legend/message-types.js.map +1 -1
  33. package/lib/module/store/conversation.js +1 -1
  34. package/lib/module/store/conversation.js.map +1 -1
  35. package/lib/module/store/message.js +45 -0
  36. package/lib/module/store/message.js.map +1 -0
  37. package/lib/module/translation/resources/i18n.js +7 -1
  38. package/lib/module/translation/resources/i18n.js.map +1 -1
  39. package/lib/module/types/chat.js +2 -7
  40. package/lib/module/types/chat.js.map +1 -1
  41. package/lib/module/utils/conversation.js +34 -13
  42. package/lib/module/utils/conversation.js.map +1 -1
  43. package/lib/module/utils/legendListMessage.js +0 -3
  44. package/lib/module/utils/legendListMessage.js.map +1 -1
  45. package/lib/module/utils/message.js +5 -8
  46. package/lib/module/utils/message.js.map +1 -1
  47. package/lib/module/utils/url.js +3 -3
  48. package/lib/module/utils/url.js.map +1 -1
  49. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +2 -2
  50. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  51. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +3 -4
  52. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
  53. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  54. package/lib/typescript/src/context/ChatContext.d.ts +1 -1
  55. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  56. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +12 -0
  57. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -0
  58. package/lib/typescript/src/hooks/useChatMessages.d.ts +0 -1
  59. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  60. package/lib/typescript/src/hooks/useConversationList.d.ts +2 -1
  61. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  62. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -1
  63. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  64. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  65. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
  66. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  67. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  68. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +1 -1
  69. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -1
  70. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +1 -3
  71. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  72. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts +1 -0
  73. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts.map +1 -1
  74. package/lib/typescript/src/screens/chat-detail/types.d.ts +6 -7
  75. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  76. package/lib/typescript/src/store/message.d.ts +3 -0
  77. package/lib/typescript/src/store/message.d.ts.map +1 -0
  78. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  79. package/lib/typescript/src/types/chat.d.ts +28 -27
  80. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  81. package/lib/typescript/src/types/common.d.ts +1 -0
  82. package/lib/typescript/src/types/common.d.ts.map +1 -1
  83. package/lib/typescript/src/utils/conversation.d.ts +3 -2
  84. package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
  85. package/lib/typescript/src/utils/legendListMessage.d.ts +0 -2
  86. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
  87. package/lib/typescript/src/utils/message.d.ts.map +1 -1
  88. package/lib/typescript/src/utils/url.d.ts +1 -1
  89. package/lib/typescript/src/utils/url.d.ts.map +1 -1
  90. package/package.json +3 -3
  91. package/src/components/ThreadCard/AvatarSection.tsx +5 -8
  92. package/src/components/ThreadCard/NamePrefixIcon.tsx +27 -38
  93. package/src/components/ThreadCard/ThreadCard.tsx +16 -30
  94. package/src/context/ChatContext.tsx +12 -4
  95. package/src/hooks/message/useSendMessage.ts +136 -0
  96. package/src/hooks/useChatMessages.ts +70 -158
  97. package/src/hooks/useConversationList.ts +34 -16
  98. package/src/hooks/useLinkPreview/useLinkPreview.ts +3 -2
  99. package/src/screens/chat-detail/ChatComposer.tsx +2 -2
  100. package/src/screens/chat-detail/ChatDetail.tsx +29 -22
  101. package/src/screens/chat-detail/ChatDetailHeader.tsx +4 -10
  102. package/src/screens/chat-detail/ChatLinkPreview.tsx +1 -1
  103. package/src/screens/chat-detail/ChatListLegend.tsx +1 -2
  104. package/src/screens/chat-detail/conversationHeader.utils.ts +11 -14
  105. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +15 -33
  106. package/src/screens/chat-detail/legend/message-types.tsx +167 -12
  107. package/src/screens/chat-detail/types.ts +6 -8
  108. package/src/store/conversation.ts +1 -1
  109. package/src/store/message.ts +44 -0
  110. package/src/translation/resources/i18n.ts +6 -0
  111. package/src/types/chat.ts +31 -30
  112. package/src/types/common.ts +1 -0
  113. package/src/utils/conversation.ts +44 -17
  114. package/src/utils/legendListMessage.ts +0 -5
  115. package/src/utils/message.ts +10 -12
  116. package/src/utils/url.ts +3 -3
@@ -1,10 +1,11 @@
1
1
  import { useEffect } from 'react';
2
2
  import { useQuery } from '@tanstack/react-query';
3
- import OpenIMSDK from '@droppii/openim-rn-client-sdk';
4
- import { ChatAPI } from '../services';
3
+ import OpenIMSDK, {
4
+ type ConversationItem,
5
+ } from '@droppii/openim-rn-client-sdk';
5
6
  import { conversationQueryKeys } from './query-keys';
6
7
  import { useConversationStore } from '../store';
7
- import { mergeOpenIMIntoConversation } from '../utils/conversation';
8
+ import type { DConversationItem, DMessageItem } from '../types/chat';
8
9
 
9
10
  type Params = {
10
11
  applicationType: string;
@@ -12,6 +13,29 @@ type Params = {
12
13
  enabled?: boolean;
13
14
  };
14
15
 
16
+ function toConversationItem(raw: ConversationItem): DConversationItem {
17
+ let lastMessage: DMessageItem | undefined;
18
+ if (raw.latestMsg) {
19
+ try {
20
+ lastMessage = JSON.parse(raw.latestMsg) as DMessageItem;
21
+ } catch {
22
+ // leave undefined
23
+ }
24
+ }
25
+
26
+ let applicationType: string | undefined;
27
+ if (raw.ex) {
28
+ try {
29
+ applicationType = (JSON.parse(raw.ex) as { applicationType?: string })
30
+ .applicationType;
31
+ } catch {
32
+ // leave undefined
33
+ }
34
+ }
35
+
36
+ return { ...(raw as DConversationItem), lastMessage, applicationType };
37
+ }
38
+
15
39
  export function useConversationList({
16
40
  applicationType,
17
41
  page = 1,
@@ -26,21 +50,15 @@ export function useConversationList({
26
50
  enabled: enabled !== false,
27
51
  queryFn: async () => {
28
52
  const offset = (page - 1) * 20;
29
- const [{ data: droppiiList }, openimList] = await Promise.all([
30
- ChatAPI.queryConversations({ applicationType, page, pageSize: 20 }),
31
- OpenIMSDK.getConversationListSplit({ offset, count: 20 }),
32
- ]);
33
- const openimMap = new Map(openimList.map((c) => [c.conversationID, c]));
34
-
35
- const merged = droppiiList.map((conv) => {
36
- const openimConv = openimMap.get(conv.conversationId);
37
- return openimConv
38
- ? mergeOpenIMIntoConversation(conv, openimConv)
39
- : conv;
53
+ const rawList = await OpenIMSDK.getConversationListSplitApp({
54
+ offset,
55
+ count: 20,
56
+ applicationType,
40
57
  });
41
58
 
42
- useConversationStore.getState().updateConversations(merged);
43
- return merged;
59
+ const conversations = rawList.map(toConversationItem);
60
+ useConversationStore.getState().updateConversations(conversations);
61
+ return conversations;
44
62
  },
45
63
  });
46
64
 
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { useFetchUrlMetadata } from './useFetchUrlMetadata';
3
- import { extractFirstUrl } from '../../utils/url';
3
+ import { extractUrls } from '../../utils/url';
4
4
 
5
5
  export function useLinkPreview(value: string | undefined) {
6
6
  const [detectedUrl, setDetectedUrl] = useState<string | undefined>(undefined);
@@ -10,7 +10,8 @@ export function useLinkPreview(value: string | undefined) {
10
10
  useEffect(() => {
11
11
  if (timerRef.current) clearTimeout(timerRef.current);
12
12
  timerRef.current = setTimeout(() => {
13
- const url = value ? extractFirstUrl(value) : undefined;
13
+ const urls = value ? extractUrls(value) : [];
14
+ const url = urls.length === 1 ? urls[0] : undefined;
14
15
  setDetectedUrl(url);
15
16
  if (url) setIsDismissed(false);
16
17
  }, 500);
@@ -181,8 +181,8 @@ export const ChatComposer = memo(
181
181
  });
182
182
 
183
183
  const handleSend = useCallback(() => {
184
- onSend?.();
185
- }, [onSend]);
184
+ onSend?.(showLinkPreview ? urlMetadata : undefined);
185
+ }, [onSend, urlMetadata, showLinkPreview]);
186
186
 
187
187
  const handleToggleAttachment = useCallback(() => {
188
188
  if (!hasAttachmentActions) {
@@ -22,6 +22,8 @@ import {
22
22
  } from './conversationHeader.utils';
23
23
  import { getFeatureFlag, onFlagsChange } from '../../config/feature-flags';
24
24
  import type { ChatDetailProps } from './types';
25
+ import { useSendMessage } from '../../hooks/message/useSendMessage';
26
+ import type { IUrlMetadata } from '../../types/common';
25
27
 
26
28
  const ChatDetail = memo(
27
29
  ({
@@ -31,8 +33,7 @@ const ChatDetail = memo(
31
33
  subtitle,
32
34
  avatarUri,
33
35
  avatarFullName,
34
- chatType,
35
- chatCategory,
36
+ peerType,
36
37
  applicationType,
37
38
  showAddMember,
38
39
  getSubtitle,
@@ -62,11 +63,12 @@ const ChatDetail = memo(
62
63
  const [internalInput, setInternalInput] = useState('');
63
64
  const conversation = useConversation(conversationId);
64
65
 
66
+ const { sendMessage } = useSendMessage();
67
+
65
68
  const {
66
69
  messages,
67
70
  currentUserId,
68
71
  onLoadEarlier,
69
- sendTextMessage,
70
72
  isLoading,
71
73
  isLoadingEarlier,
72
74
  hasMoreEarlier,
@@ -83,8 +85,7 @@ const ChatDetail = memo(
83
85
  const resolvedAvatarUri =
84
86
  avatarUri ?? getConversationAvatarUri(conversation);
85
87
  const resolvedAvatarFullName = avatarFullName ?? resolvedTitle;
86
- const resolvedChatType = chatType ?? conversation?.chatType;
87
- const resolvedChatCategory = chatCategory ?? conversation?.chatCategory;
88
+ const resolvedPeerType = peerType ?? conversation?.peerType;
88
89
  const resolvedApplicationType =
89
90
  applicationType ?? conversation?.applicationType;
90
91
  const resolvedShowAddMember =
@@ -151,24 +152,31 @@ const ChatDetail = memo(
151
152
 
152
153
  const currentInput = inputValue ?? internalInput;
153
154
 
154
- const handleSend = useCallback(async () => {
155
- const text = currentInput.trim();
156
- if (!text) {
157
- return;
158
- }
155
+ const handleSend = useCallback(
156
+ async (urlMetadata?: IUrlMetadata) => {
157
+ const text = currentInput.trim();
158
+ if (!text) {
159
+ return;
160
+ }
159
161
 
160
- try {
161
- await sendTextMessage(text);
162
+ try {
163
+ await sendMessage({
164
+ files: [],
165
+ plainText: text,
166
+ urlMetadata,
167
+ });
162
168
 
163
- if (onChangeInput) {
164
- onChangeInput('');
165
- } else {
166
- setInternalInput('');
169
+ if (onChangeInput) {
170
+ onChangeInput('');
171
+ } else {
172
+ setInternalInput('');
173
+ }
174
+ } catch (error) {
175
+ console.error('[send-message]', error);
167
176
  }
168
- } catch (error) {
169
- console.error('[send-message]', error);
170
- }
171
- }, [currentInput, onChangeInput, sendTextMessage]);
177
+ },
178
+ [currentInput, onChangeInput, sendMessage]
179
+ );
172
180
 
173
181
  const insets = useSafeAreaInsets();
174
182
 
@@ -179,8 +187,7 @@ const ChatDetail = memo(
179
187
  subtitle={resolvedSubtitle}
180
188
  avatarUri={resolvedAvatarUri}
181
189
  avatarFullName={resolvedAvatarFullName}
182
- chatType={resolvedChatType}
183
- chatCategory={resolvedChatCategory}
190
+ peerType={resolvedPeerType}
184
191
  applicationType={resolvedApplicationType}
185
192
  showAddMember={resolvedShowAddMember}
186
193
  onBack={onBack}
@@ -16,8 +16,7 @@ const ChatDetailHeader = memo(
16
16
  subtitle,
17
17
  avatarUri,
18
18
  avatarFullName,
19
- chatType,
20
- chatCategory,
19
+ peerType,
21
20
  applicationType,
22
21
  showAddMember = false,
23
22
  onBack,
@@ -81,17 +80,13 @@ const ChatDetailHeader = memo(
81
80
  <AvatarSection
82
81
  avatar={avatarUri ?? null}
83
82
  fullName={avatarFullName ?? title}
84
- chatCategory={chatCategory}
83
+ peerType={peerType}
85
84
  applicationType={applicationType}
86
- chatType={chatType}
87
85
  />
88
86
 
89
87
  <KContainer.View flex marginL="0.5rem">
90
88
  <KContainer.View row alignItems>
91
- <NamePrefixIcon
92
- chatCategory={chatCategory}
93
- chatType={chatType}
94
- />
89
+ <NamePrefixIcon peerType={peerType} />
95
90
  <KLabel.Text
96
91
  typo="TextLgBold"
97
92
  color={KColors.gray.dark}
@@ -117,9 +112,8 @@ const ChatDetailHeader = memo(
117
112
  [
118
113
  avatarFullName,
119
114
  avatarUri,
120
- chatCategory,
115
+ peerType,
121
116
  applicationType,
122
- chatType,
123
117
  onBack,
124
118
  onPressAvatar,
125
119
  subtitle,
@@ -47,7 +47,7 @@ export const ChatLinkPreview = memo(
47
47
  </KLabel.Text>
48
48
  </KContainer.View>
49
49
  <KContainer.Touchable onPress={onDismiss} style={styles.right}>
50
- <KImage.VectorIcons name="close" size={16} />
50
+ <KImage.VectorIcons name="close-o" size={16} />
51
51
  </KContainer.Touchable>
52
52
  </KContainer.View>
53
53
  );
@@ -97,7 +97,7 @@ export const ChatListLegend = memo(
97
97
  return null; // Shouldn't happen if logic is consistent
98
98
  }
99
99
 
100
- const { dayStart, messageType, createdAt } = messageData;
100
+ const { dayStart, createdAt } = messageData;
101
101
 
102
102
  return (
103
103
  <>
@@ -116,7 +116,6 @@ export const ChatListLegend = memo(
116
116
  ) : (
117
117
  <LegendChatMessage
118
118
  message={message}
119
- messageType={messageType}
120
119
  isOutgoing={isOutgoing}
121
120
  createdAtTime={createdAt}
122
121
  />
@@ -1,5 +1,5 @@
1
- import { DChatType } from '../../types/chat';
2
- import type { DConversationItem } from '../../types/chat';
1
+ import { PeerType } from '@droppii/openim-rn-client-sdk';
2
+ import { type DConversationItem } from '../../types/chat';
3
3
 
4
4
  export const getConversationTitle = (
5
5
  conversation?: DConversationItem
@@ -8,18 +8,13 @@ export const getConversationTitle = (
8
8
  return 'Tin nhắn';
9
9
  }
10
10
 
11
- return (
12
- conversation.peer?.fullName ??
13
- conversation.peer?.username ??
14
- conversation.showName ??
15
- 'Tin nhắn'
16
- );
11
+ return conversation.showName ?? 'Tin nhắn';
17
12
  };
18
13
 
19
14
  export const getConversationAvatarUri = (
20
15
  conversation?: DConversationItem
21
16
  ): string | null => {
22
- return conversation?.peer?.avatar ?? conversation?.faceURL ?? null;
17
+ return conversation?.faceURL ?? null;
23
18
  };
24
19
 
25
20
  export const getConversationSubtitle = (
@@ -29,24 +24,26 @@ export const getConversationSubtitle = (
29
24
  return undefined;
30
25
  }
31
26
 
32
- if (conversation?.chatType === DChatType.GROUP) {
27
+ if (conversation.peerType === PeerType.Group) {
33
28
  const unreadLabel =
34
- (conversation?.unreadCount ?? 0) > 0
29
+ (conversation.unreadCount ?? 0) > 0
35
30
  ? `${conversation.unreadCount} tin nhắn mới`
36
31
  : 'Không có tin mới';
37
32
  return `20 thành viên • ${unreadLabel}`;
38
33
  }
39
34
 
40
- if (conversation?.applicationType === 'MALL') {
35
+ if (
36
+ conversation.peerType === PeerType.Customer ||
37
+ conversation.applicationType === 'MALL'
38
+ ) {
41
39
  return '338,5K Đã bán • 4,2K Theo dõi';
42
40
  }
43
41
 
44
- // const lastMessageText = getLastMessageText(conversation.lastMessage);
45
42
  return 'Nhấn để xem thông tin';
46
43
  };
47
44
 
48
45
  export const shouldShowAddMember = (
49
46
  conversation?: DConversationItem
50
47
  ): boolean => {
51
- return conversation?.chatType === DChatType.GROUP;
48
+ return conversation?.peerType === PeerType.Group;
52
49
  };
@@ -1,32 +1,29 @@
1
- import { memo, ComponentType } from 'react';
1
+ import { memo } from 'react';
2
+ import type { ComponentType } from 'react';
3
+ import { MessageType } from '@droppii/openim-rn-client-sdk';
2
4
  import type { DMessageItem } from '../../../types/chat';
3
- import type { DChatMessageType } from '../../../types/message';
4
5
  import {
5
6
  LegendTextMessage,
6
7
  LegendImageMessage,
7
8
  LegendVideoMessage,
8
9
  LegendFileMessage,
10
+ LegendLinkMessage,
9
11
  } from './message-types';
10
- import { DChatMessageType as MessageTypeEnum } from '../../../types/message';
11
12
 
12
13
  interface LegendChatMessageProps {
13
14
  message: DMessageItem;
14
- messageType: DChatMessageType;
15
15
  isOutgoing: boolean;
16
16
  createdAtTime: number;
17
17
  }
18
18
 
19
- // Message type to component mapping
20
- const messageComponentMap: Partial<
21
- Record<DChatMessageType, ComponentType<any>>
22
- > = {
23
- [MessageTypeEnum.Text]: LegendTextMessage,
24
- [MessageTypeEnum.Image]: LegendImageMessage,
25
- [MessageTypeEnum.Video]: LegendVideoMessage,
26
- [MessageTypeEnum.File]: LegendFileMessage,
27
- [MessageTypeEnum.Link]: LegendTextMessage, // Link renders as text for now
28
- [MessageTypeEnum.Order]: LegendTextMessage, // Order renders as text for now
29
- [MessageTypeEnum.Unsupported]: LegendTextMessage, // Unsupported renders as text for now
19
+ const messageComponentMap: Partial<Record<MessageType, ComponentType<any>>> = {
20
+ [MessageType.TextMessage]: LegendTextMessage,
21
+ [MessageType.AtTextMessage]: LegendTextMessage,
22
+ [MessageType.QuoteMessage]: LegendTextMessage,
23
+ [MessageType.UrlTextMessage]: LegendLinkMessage,
24
+ [MessageType.PictureMessage]: LegendImageMessage,
25
+ [MessageType.VideoMessage]: LegendVideoMessage,
26
+ [MessageType.FileMessage]: LegendFileMessage,
30
27
  };
31
28
 
32
29
  /**
@@ -34,24 +31,9 @@ const messageComponentMap: Partial<
34
31
  * based on message type. Extensible for new message types.
35
32
  */
36
33
  export const LegendChatMessage = memo(
37
- ({
38
- message,
39
- messageType,
40
- isOutgoing,
41
- createdAtTime,
42
- }: LegendChatMessageProps) => {
43
- const MessageComponent = messageComponentMap[messageType];
44
-
45
- // Fallback to text message if type not found
46
- if (!MessageComponent) {
47
- return (
48
- <LegendTextMessage
49
- message={message}
50
- isOutgoing={isOutgoing}
51
- createdAtTime={createdAtTime}
52
- />
53
- );
54
- }
34
+ ({ message, isOutgoing, createdAtTime }: LegendChatMessageProps) => {
35
+ const MessageComponent =
36
+ messageComponentMap[message?.contentType] ?? LegendTextMessage;
55
37
 
56
38
  return (
57
39
  <MessageComponent
@@ -1,7 +1,14 @@
1
1
  import { memo } from 'react';
2
- import { StyleSheet } from 'react-native';
3
- import { KContainer, KLabel, KSpacingValue } from '@droppii/libs';
4
- import type { DMessageItem } from '../../../types/chat';
2
+ import { Linking, StyleSheet, Text } from 'react-native';
3
+ import {
4
+ KContainer,
5
+ KImage,
6
+ KLabel,
7
+ KColors,
8
+ KSpacingValue,
9
+ } from '@droppii/libs';
10
+ import type { DMessageItem, IMessageItemEx } from '../../../types/chat';
11
+ import type { IUrlMetadata } from '../../../types/common';
5
12
  import { getMessageText } from '../../../utils/legendListMessage';
6
13
  import { CHAT_BUBBLE_COLORS } from '../constants';
7
14
 
@@ -13,10 +20,7 @@ interface BaseLegendMessageProps {
13
20
 
14
21
  const formatMessageTime = (createdAt: number) => {
15
22
  const date = new Date(createdAt);
16
- if (Number.isNaN(date.getTime())) {
17
- return '';
18
- }
19
-
23
+ if (Number.isNaN(date.getTime())) return '';
20
24
  return date.toLocaleTimeString('vi-VN', {
21
25
  hour: '2-digit',
22
26
  minute: '2-digit',
@@ -51,7 +55,7 @@ export const LegendTextMessage = memo(
51
55
  <KLabel.Text
52
56
  typo="TextXsNormal"
53
57
  color={CHAT_BUBBLE_COLORS.timestamp}
54
- marginL={'0.25rem'}
58
+ marginL="0.25rem"
55
59
  >
56
60
  {timeLabel}
57
61
  </KLabel.Text>
@@ -60,7 +64,6 @@ export const LegendTextMessage = memo(
60
64
  );
61
65
  }
62
66
  );
63
-
64
67
  LegendTextMessage.displayName = 'LegendTextMessage';
65
68
 
66
69
  // Image Message Component (placeholder)
@@ -82,7 +85,6 @@ export const LegendImageMessage = memo(
82
85
  );
83
86
  }
84
87
  );
85
-
86
88
  LegendImageMessage.displayName = 'LegendImageMessage';
87
89
 
88
90
  // Video Message Component (placeholder)
@@ -104,7 +106,6 @@ export const LegendVideoMessage = memo(
104
106
  );
105
107
  }
106
108
  );
107
-
108
109
  LegendVideoMessage.displayName = 'LegendVideoMessage';
109
110
 
110
111
  // File Message Component
@@ -128,9 +129,140 @@ export const LegendFileMessage = memo(
128
129
  );
129
130
  }
130
131
  );
131
-
132
132
  LegendFileMessage.displayName = 'LegendFileMessage';
133
133
 
134
+ // Link Message Component
135
+ const parseUrlMetadata = (ex?: string): IUrlMetadata | undefined => {
136
+ if (!ex) return undefined;
137
+ try {
138
+ return (JSON.parse(ex) as IMessageItemEx).urlMetadata;
139
+ } catch {
140
+ return undefined;
141
+ }
142
+ };
143
+
144
+ const renderTextWithLinks = (content: string, urls: string[]) => {
145
+ if (!urls.length) {
146
+ return (
147
+ <KLabel.Text typo="TextMdNormal" color={CHAT_BUBBLE_COLORS.text}>
148
+ {content}
149
+ </KLabel.Text>
150
+ );
151
+ }
152
+
153
+ const parts: { text: string; isUrl: boolean }[] = [];
154
+ let remaining = content;
155
+
156
+ for (const url of urls) {
157
+ const idx = remaining.indexOf(url);
158
+ if (idx === -1) continue;
159
+ if (idx > 0) parts.push({ text: remaining.slice(0, idx), isUrl: false });
160
+ parts.push({ text: url, isUrl: true });
161
+ remaining = remaining.slice(idx + url.length);
162
+ }
163
+
164
+ if (remaining) parts.push({ text: remaining, isUrl: false });
165
+
166
+ return (
167
+ <KLabel.Text typo="TextMdNormal" color={CHAT_BUBBLE_COLORS.text}>
168
+ {parts.map((part, i) =>
169
+ part.isUrl ? (
170
+ <Text
171
+ key={i}
172
+ style={styles.urlText}
173
+ onPress={() => Linking.openURL(part.text)}
174
+ >
175
+ {part.text}
176
+ </Text>
177
+ ) : (
178
+ <Text key={i}>{part.text}</Text>
179
+ )
180
+ )}
181
+ </KLabel.Text>
182
+ );
183
+ };
184
+
185
+ export const LegendLinkMessage = memo(
186
+ ({ message, isOutgoing, createdAtTime }: BaseLegendMessageProps) => {
187
+ const content = message?.urlTextElem?.content;
188
+ const urls = message?.urlTextElem?.urls ?? [];
189
+ const metadata = parseUrlMetadata(message.ex);
190
+ const timeLabel = formatMessageTime(createdAtTime);
191
+
192
+ return (
193
+ <KContainer.View style={styles.wrapper}>
194
+ <KContainer.View
195
+ style={[
196
+ styles.bubble,
197
+ isOutgoing ? styles.bubbleSent : styles.bubbleReceived,
198
+ ]}
199
+ >
200
+ {!!content?.trim() && (
201
+ <KContainer.View style={styles.textRow}>
202
+ {renderTextWithLinks(content, urls)}
203
+ </KContainer.View>
204
+ )}
205
+
206
+ {metadata && (
207
+ <KContainer.Touchable
208
+ style={styles.card}
209
+ onPress={() => metadata.url && Linking.openURL(metadata.url)}
210
+ >
211
+ {!!metadata.image && (
212
+ <KImage.Base
213
+ uri={metadata.image}
214
+ style={styles.cardThumb}
215
+ resizeMode="cover"
216
+ />
217
+ )}
218
+ <KContainer.View style={styles.cardBody}>
219
+ {!!metadata.title && (
220
+ <KLabel.Text
221
+ typo="TextNmMedium"
222
+ color={KColors.palette.gray.w900}
223
+ numberOfLines={1}
224
+ >
225
+ {metadata.title}
226
+ </KLabel.Text>
227
+ )}
228
+ {!!metadata.url && (
229
+ <KLabel.Text
230
+ typo="TextXsMedium"
231
+ color={KColors.palette.primary.w400}
232
+ numberOfLines={1}
233
+ >
234
+ {metadata.url}
235
+ </KLabel.Text>
236
+ )}
237
+ {!!metadata.description && (
238
+ <KLabel.Text
239
+ typo="TextXsNormal"
240
+ color={KColors.gray.normal}
241
+ numberOfLines={2}
242
+ >
243
+ {metadata.description}
244
+ </KLabel.Text>
245
+ )}
246
+ </KContainer.View>
247
+ </KContainer.Touchable>
248
+ )}
249
+ </KContainer.View>
250
+
251
+ {!isOutgoing && timeLabel ? (
252
+ <KLabel.Text
253
+ typo="TextXsNormal"
254
+ color={CHAT_BUBBLE_COLORS.timestamp}
255
+ marginL="0.25rem"
256
+ >
257
+ {timeLabel}
258
+ </KLabel.Text>
259
+ ) : null}
260
+ </KContainer.View>
261
+ );
262
+ }
263
+ );
264
+ LegendLinkMessage.displayName = 'LegendLinkMessage';
265
+
134
266
  const styles = StyleSheet.create({
135
267
  wrapper: {
136
268
  maxWidth: '80%',
@@ -146,4 +278,27 @@ const styles = StyleSheet.create({
146
278
  bubbleSent: {
147
279
  backgroundColor: CHAT_BUBBLE_COLORS.sent,
148
280
  },
281
+ textRow: {
282
+ paddingHorizontal: KSpacingValue['0.25rem'],
283
+ },
284
+ urlText: {
285
+ color: KColors.palette.primary.w400,
286
+ fontWeight: '500',
287
+ },
288
+ card: {
289
+ borderRadius: KSpacingValue['0.75rem'],
290
+ borderWidth: 1,
291
+ borderColor: 'rgba(57,62,64,0.1)',
292
+ backgroundColor: KColors.white,
293
+ overflow: 'hidden',
294
+ },
295
+ cardThumb: {
296
+ width: '100%',
297
+ aspectRatio: 16 / 9,
298
+ },
299
+ cardBody: {
300
+ paddingHorizontal: KSpacingValue['0.75rem'],
301
+ paddingVertical: KSpacingValue['0.5rem'],
302
+ gap: 2,
303
+ },
149
304
  });
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { DChatType, DChatCategory } from '../../types/chat';
3
2
  import type { DConversationItem, DMessageItem } from '../../types/chat';
3
+ import type { IUrlMetadata } from '../../types/common';
4
+ import type { PeerType } from '@droppii/openim-rn-client-sdk';
4
5
 
5
6
  export type DChatActionIconProvider = 'MaterialCommunityIcons' | 'DroppiiNew';
6
7
 
@@ -22,8 +23,7 @@ export interface ChatDetailHeaderProps {
22
23
  subtitle?: string;
23
24
  avatarUri?: string | null;
24
25
  avatarFullName?: string;
25
- chatType?: DChatType;
26
- chatCategory?: DChatCategory;
26
+ peerType?: PeerType;
27
27
  applicationType?: DConversationItem['applicationType'];
28
28
  showAddMember?: boolean;
29
29
  onBack?: () => void;
@@ -60,7 +60,7 @@ export interface ChatComposerProps {
60
60
  value?: string;
61
61
  placeholder?: string;
62
62
  onChangeText?: (text: string) => void;
63
- onSend?: () => void;
63
+ onSend?: (urlMetadata?: IUrlMetadata) => void;
64
64
  onPressAttach?: () => void;
65
65
  onPressEmoji?: () => void;
66
66
  showQuickActions?: boolean;
@@ -93,8 +93,7 @@ export interface ChatDetailProps extends Omit<
93
93
  | 'subtitle'
94
94
  | 'avatarUri'
95
95
  | 'avatarFullName'
96
- | 'chatType'
97
- | 'chatCategory'
96
+ | 'peerType'
98
97
  | 'applicationType'
99
98
  | 'showAddMember'
100
99
  > {
@@ -104,8 +103,7 @@ export interface ChatDetailProps extends Omit<
104
103
  subtitle?: string;
105
104
  avatarUri?: string | null;
106
105
  avatarFullName?: string;
107
- chatType?: DChatType;
108
- chatCategory?: DChatCategory;
106
+ peerType?: PeerType;
109
107
  applicationType?: DConversationItem['applicationType'];
110
108
  showAddMember?: boolean;
111
109
  getSubtitle?: (conversation: DConversationItem) => string | undefined;