@droppii-org/chat-mobile 0.2.4 → 0.2.6

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 (161) hide show
  1. package/lib/module/config/feature-flags.js +38 -0
  2. package/lib/module/config/feature-flags.js.map +1 -0
  3. package/lib/module/hooks/query-keys.js +4 -0
  4. package/lib/module/hooks/query-keys.js.map +1 -1
  5. package/lib/module/hooks/useChatMessages.js +45 -0
  6. package/lib/module/hooks/useChatMessages.js.map +1 -1
  7. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js +17 -0
  8. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js.map +1 -0
  9. package/lib/module/hooks/useLinkPreview/useLinkPreview.js +34 -0
  10. package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -0
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/screens/chat-detail/ChatComposer.js +18 -2
  13. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  14. package/lib/module/screens/chat-detail/ChatDetail.js +110 -20
  15. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  16. package/lib/module/screens/chat-detail/ChatLinkPreview.js +79 -0
  17. package/lib/module/screens/chat-detail/ChatLinkPreview.js.map +1 -0
  18. package/lib/module/screens/chat-detail/ChatList.js +2 -0
  19. package/lib/module/screens/chat-detail/ChatList.js.map +1 -1
  20. package/lib/module/screens/chat-detail/ChatListLegend.js +352 -0
  21. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -0
  22. package/lib/module/screens/chat-detail/ChatQuickActions.js +12 -2
  23. package/lib/module/screens/chat-detail/ChatQuickActions.js.map +1 -1
  24. package/lib/module/screens/chat-detail/conversationHeader.utils.js +31 -0
  25. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -0
  26. package/lib/module/screens/chat-detail/index.js +1 -0
  27. package/lib/module/screens/chat-detail/index.js.map +1 -1
  28. package/lib/module/screens/chat-detail/legend/LegendChatDay.js +57 -0
  29. package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -0
  30. package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js +21 -0
  31. package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js.map +1 -0
  32. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +47 -0
  33. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -0
  34. package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js +58 -0
  35. package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js.map +1 -0
  36. package/lib/module/screens/chat-detail/legend/message-types.js +122 -0
  37. package/lib/module/screens/chat-detail/legend/message-types.js.map +1 -0
  38. package/lib/module/screens/chat-detail/messages/ChatMessageBubble.js.map +1 -1
  39. package/lib/module/services/apis.js +1 -1
  40. package/lib/module/services/apis.js.map +1 -1
  41. package/lib/module/services/endpoints.js +8 -0
  42. package/lib/module/services/endpoints.js.map +1 -0
  43. package/lib/module/types/common.js +2 -0
  44. package/lib/module/types/common.js.map +1 -0
  45. package/lib/module/utils/legendListMessage.js +80 -0
  46. package/lib/module/utils/legendListMessage.js.map +1 -0
  47. package/lib/module/utils/url.js +7 -0
  48. package/lib/module/utils/url.js.map +1 -0
  49. package/lib/typescript/src/components/Avatar/Avatar.d.ts +1 -1
  50. package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -1
  51. package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts +1 -1
  52. package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts.map +1 -1
  53. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts +1 -1
  54. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts.map +1 -1
  55. package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts +1 -1
  56. package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts.map +1 -1
  57. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +1 -1
  58. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  59. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +1 -1
  60. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
  61. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts +1 -1
  62. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  63. package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts +1 -1
  64. package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts.map +1 -1
  65. package/lib/typescript/src/config/feature-flags.d.ts +12 -0
  66. package/lib/typescript/src/config/feature-flags.d.ts.map +1 -0
  67. package/lib/typescript/src/context/ChatContext.d.ts +1 -1
  68. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  69. package/lib/typescript/src/hooks/query-keys.d.ts +4 -0
  70. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  71. package/lib/typescript/src/hooks/useChatMessages.d.ts +3 -0
  72. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  73. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts +3 -0
  74. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts.map +1 -0
  75. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts +7 -0
  76. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -0
  77. package/lib/typescript/src/index.d.ts +1 -1
  78. package/lib/typescript/src/index.d.ts.map +1 -1
  79. package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts +1 -1
  80. package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts.map +1 -1
  81. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +1 -1
  82. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  83. package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts +1 -1
  84. package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts.map +1 -1
  85. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  86. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  87. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
  88. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  89. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts +9 -0
  90. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts.map +1 -0
  91. package/lib/typescript/src/screens/chat-detail/ChatList.d.ts +1 -1
  92. package/lib/typescript/src/screens/chat-detail/ChatList.d.ts.map +1 -1
  93. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +3 -0
  94. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -0
  95. package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts +1 -1
  96. package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts.map +1 -1
  97. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts +1 -1
  98. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts.map +1 -1
  99. package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts +1 -1
  100. package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts.map +1 -1
  101. package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts +1 -1
  102. package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +6 -0
  104. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -0
  105. package/lib/typescript/src/screens/chat-detail/index.d.ts +2 -1
  106. package/lib/typescript/src/screens/chat-detail/index.d.ts.map +1 -1
  107. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -0
  108. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -0
  109. package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts +6 -0
  110. package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts.map +1 -0
  111. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +15 -0
  112. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -0
  113. package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts +6 -0
  114. package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts.map +1 -0
  115. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts +12 -0
  116. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts.map +1 -0
  117. package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts +1 -1
  118. package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts.map +1 -1
  119. package/lib/typescript/src/screens/chat-detail/types.d.ts +30 -3
  120. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  121. package/lib/typescript/src/screens/inbox/Inbox.d.ts +1 -1
  122. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  123. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts +1 -1
  124. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  125. package/lib/typescript/src/services/apis.d.ts +1 -0
  126. package/lib/typescript/src/services/apis.d.ts.map +1 -1
  127. package/lib/typescript/src/services/endpoints.d.ts +6 -0
  128. package/lib/typescript/src/services/endpoints.d.ts.map +1 -0
  129. package/lib/typescript/src/types/common.d.ts +6 -0
  130. package/lib/typescript/src/types/common.d.ts.map +1 -0
  131. package/lib/typescript/src/utils/legendListMessage.d.ts +25 -0
  132. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -0
  133. package/lib/typescript/src/utils/url.d.ts +2 -0
  134. package/lib/typescript/src/utils/url.d.ts.map +1 -0
  135. package/package.json +4 -2
  136. package/src/config/feature-flags.ts +49 -0
  137. package/src/hooks/query-keys.ts +5 -0
  138. package/src/hooks/useChatMessages.ts +60 -0
  139. package/src/hooks/useLinkPreview/useFetchUrlMetadata.ts +18 -0
  140. package/src/hooks/useLinkPreview/useLinkPreview.ts +30 -0
  141. package/src/index.tsx +1 -0
  142. package/src/screens/chat-detail/ChatComposer.tsx +21 -0
  143. package/src/screens/chat-detail/ChatDetail.tsx +154 -28
  144. package/src/screens/chat-detail/ChatLinkPreview.tsx +86 -0
  145. package/src/screens/chat-detail/ChatList.tsx +3 -0
  146. package/src/screens/chat-detail/ChatListLegend.tsx +404 -0
  147. package/src/screens/chat-detail/ChatQuickActions.tsx +19 -2
  148. package/src/screens/chat-detail/conversationHeader.utils.ts +52 -0
  149. package/src/screens/chat-detail/index.ts +7 -0
  150. package/src/screens/chat-detail/legend/LegendChatDay.tsx +70 -0
  151. package/src/screens/chat-detail/legend/LegendChatLoadEarlier.tsx +21 -0
  152. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +66 -0
  153. package/src/screens/chat-detail/legend/LegendChatScrollToBottom.tsx +56 -0
  154. package/src/screens/chat-detail/legend/message-types.tsx +149 -0
  155. package/src/screens/chat-detail/messages/ChatMessageBubble.tsx +0 -1
  156. package/src/screens/chat-detail/types.ts +43 -3
  157. package/src/services/apis.ts +1 -1
  158. package/src/services/endpoints.ts +5 -0
  159. package/src/types/common.ts +5 -0
  160. package/src/utils/legendListMessage.ts +102 -0
  161. package/src/utils/url.ts +5 -0
@@ -0,0 +1,404 @@
1
+ import { memo, useCallback, useMemo, useEffect, useRef, useState } from 'react';
2
+ import { ActivityIndicator, StyleSheet } from 'react-native';
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withSpring,
7
+ } from 'react-native-reanimated';
8
+ import { LegendList, type LegendListRef } from '@legendapp/list/react-native';
9
+ import { KContainer, KColors, KSpacingValue, KLabel } from '@droppii/libs';
10
+ import { precomputeLegendListData } from '../../utils/legendListMessage';
11
+ import type { ChatListProps } from './types';
12
+ import type { DMessageItem } from '../../types/chat';
13
+ import { LegendChatMessage } from './legend/LegendChatMessage';
14
+ import { LegendChatDay } from './legend/LegendChatDay';
15
+ import { LegendChatLoadEarlier } from './legend/LegendChatLoadEarlier';
16
+
17
+ export const ChatListLegend = memo(
18
+ ({
19
+ messages = [],
20
+ currentUserId = '',
21
+ renderChat,
22
+ onLoadEarlier,
23
+ isLoading,
24
+ isLoadingEarlier,
25
+ hasMoreEarlier,
26
+ onLoadNewer,
27
+ isLoadingNewer,
28
+ hasMoreNewer,
29
+ }: ChatListProps) => {
30
+ if (__DEV__) {
31
+ console.log('[LegendList] Props:', {
32
+ hasMoreEarlier,
33
+ isLoadingEarlier,
34
+ hasMoreNewer,
35
+ isLoadingNewer,
36
+ messagesCount: messages.length,
37
+ });
38
+ }
39
+
40
+ // Precompute all message data in single pass (O(n))
41
+ const messageDataMap = useMemo(
42
+ () => precomputeLegendListData(messages),
43
+ [messages]
44
+ );
45
+
46
+ const listRef = useRef<LegendListRef>(null);
47
+ const buttonOpacity = useSharedValue(0);
48
+ const isAtBottomRef = useRef(true);
49
+ const lastMessageCountRef = useRef(messages.length);
50
+ const lastFirstMessageIdRef = useRef<string | null>(null);
51
+ const lastLastMessageIdRef = useRef<string | null>(null);
52
+ const scrollDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
53
+ const itemHeightsRef = useRef<Map<string, number>>(new Map());
54
+ const avgItemHeightRef = useRef(120);
55
+ const heightDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
56
+ const [showBadge, setShowBadge] = useState(false);
57
+
58
+ const handleItemLayout = useCallback(
59
+ (messageId: string, height: number) => {
60
+ itemHeightsRef.current.set(messageId, height);
61
+
62
+ // Debounce average height calculation (batch updates)
63
+ if (heightDebounceRef.current) {
64
+ clearTimeout(heightDebounceRef.current);
65
+ }
66
+
67
+ heightDebounceRef.current = setTimeout(() => {
68
+ if (itemHeightsRef.current.size > 0) {
69
+ const total = Array.from(itemHeightsRef.current.values()).reduce(
70
+ (a, b) => a + b,
71
+ 0
72
+ );
73
+ avgItemHeightRef.current = Math.round(
74
+ total / itemHeightsRef.current.size
75
+ );
76
+ }
77
+ }, 100); // Batch updates every 100ms during load
78
+ },
79
+ []
80
+ );
81
+
82
+ const renderItem = useCallback(
83
+ ({ item, index }: any) => {
84
+ try {
85
+ if (!item) return null;
86
+
87
+ const message = item as DMessageItem;
88
+ const isOutgoing = message.sendID === currentUserId;
89
+ // Use same fallback as precomputeLegendListData
90
+ const messageId =
91
+ message.clientMsgID ||
92
+ message.serverMsgID ||
93
+ `temp-${message.sendTime || message.createTime}-${index}`;
94
+
95
+ const messageData = messageDataMap.get(messageId);
96
+ if (!messageData) {
97
+ return null; // Shouldn't happen if logic is consistent
98
+ }
99
+
100
+ const { dayStart, messageType, createdAt } = messageData;
101
+
102
+ return (
103
+ <>
104
+ {dayStart && <LegendChatDay createdAt={createdAt} />}
105
+ <KContainer.View
106
+ style={[
107
+ styles.messageRow,
108
+ isOutgoing ? styles.messageRowRight : styles.messageRowLeft,
109
+ ]}
110
+ onLayout={(e) =>
111
+ handleItemLayout(messageId, e.nativeEvent.layout.height)
112
+ }
113
+ >
114
+ {renderChat ? (
115
+ renderChat(message)
116
+ ) : (
117
+ <LegendChatMessage
118
+ message={message}
119
+ messageType={messageType}
120
+ isOutgoing={isOutgoing}
121
+ createdAtTime={createdAt}
122
+ />
123
+ )}
124
+ </KContainer.View>
125
+ </>
126
+ );
127
+ } catch (error) {
128
+ if (__DEV__)
129
+ console.error('[ChatListLegend] renderItem error:', error);
130
+ return (
131
+ <KContainer.View style={styles.errorRow}>
132
+ <KLabel.Text typo="TextXsNormal">
133
+ Error rendering message
134
+ </KLabel.Text>
135
+ </KContainer.View>
136
+ );
137
+ }
138
+ },
139
+ [currentUserId, renderChat, messageDataMap, handleItemLayout]
140
+ );
141
+
142
+ const maintainScrollConfig = useMemo(
143
+ () => ({
144
+ animated: false,
145
+ on: {
146
+ dataChange: !isLoadingEarlier,
147
+ layout: !isLoadingEarlier,
148
+ itemLayout: true,
149
+ },
150
+ }),
151
+ [isLoadingEarlier]
152
+ );
153
+
154
+ const handleStartReached = useCallback(() => {
155
+ if (__DEV__) {
156
+ console.log('[LegendList] onStartReached triggered', {
157
+ hasMoreEarlier,
158
+ isLoadingEarlier,
159
+ willLoad: hasMoreEarlier && !isLoadingEarlier,
160
+ });
161
+ }
162
+ if (hasMoreEarlier && !isLoadingEarlier) {
163
+ onLoadEarlier?.();
164
+ }
165
+ }, [hasMoreEarlier, isLoadingEarlier, onLoadEarlier]);
166
+
167
+ const handleEndReached = useCallback(() => {
168
+ if (hasMoreNewer && !isLoadingNewer) {
169
+ onLoadNewer?.();
170
+ }
171
+ }, [hasMoreNewer, isLoadingNewer, onLoadNewer]);
172
+
173
+ const handleScroll = useCallback(
174
+ (e: any) => {
175
+ const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
176
+ const isAtBottom =
177
+ contentOffset.y + layoutMeasurement.height >=
178
+ contentSize.height - 100;
179
+
180
+ // Debounce animation updates
181
+ if (scrollDebounceRef.current) {
182
+ clearTimeout(scrollDebounceRef.current);
183
+ }
184
+
185
+ scrollDebounceRef.current = setTimeout(() => {
186
+ if (isAtBottomRef.current !== isAtBottom) {
187
+ isAtBottomRef.current = isAtBottom;
188
+ buttonOpacity.value = withSpring(isAtBottom ? 0 : 1, {
189
+ duration: 300,
190
+ });
191
+ }
192
+ }, 50);
193
+ },
194
+ [buttonOpacity]
195
+ );
196
+
197
+ const handleScrollToBottom = useCallback(() => {
198
+ listRef.current?.scrollToEnd({ animated: true });
199
+ buttonOpacity.value = withSpring(0, { duration: 300 });
200
+ isAtBottomRef.current = true;
201
+ setShowBadge(false);
202
+ }, [buttonOpacity]);
203
+
204
+ const animatedButtonStyle = useAnimatedStyle(() => ({
205
+ opacity: buttonOpacity.value,
206
+ pointerEvents: buttonOpacity.value === 0 ? 'none' : 'auto',
207
+ }));
208
+
209
+ // Auto-scroll to bottom when new messages arrive
210
+ useEffect(() => {
211
+ const currentCount = messages.length;
212
+ const previousCount = lastMessageCountRef.current;
213
+ let timeoutId: NodeJS.Timeout | undefined;
214
+
215
+ if (currentCount > previousCount) {
216
+ // Detect if messages were prepended (added to top) or appended (added to bottom)
217
+ const firstMsgId = messages[0]?.clientMsgID || messages[0]?.serverMsgID;
218
+ const lastMsgId =
219
+ messages[currentCount - 1]?.clientMsgID ||
220
+ messages[currentCount - 1]?.serverMsgID;
221
+ const prevFirstMsgId = lastFirstMessageIdRef.current;
222
+ const prevLastMsgId = lastLastMessageIdRef.current;
223
+
224
+ // Messages were prepended if last message ID stayed same but first changed
225
+ const wasPrepended =
226
+ prevLastMsgId === lastMsgId && firstMsgId !== prevFirstMsgId;
227
+
228
+ // Check if new message is from current user
229
+ const newMessage = messages[currentCount - 1];
230
+ const isOwnMessage = newMessage?.sendID === currentUserId;
231
+
232
+ // Auto-scroll conditions:
233
+ // 1. Own message: ALWAYS scroll to bottom (user needs to see what they sent)
234
+ // 2. Received message: Only scroll if already at bottom (don't interrupt reading history)
235
+ const shouldScroll =
236
+ !wasPrepended &&
237
+ !isLoadingEarlier &&
238
+ (isOwnMessage || isAtBottomRef.current);
239
+
240
+ if (shouldScroll) {
241
+ timeoutId = setTimeout(() => {
242
+ listRef.current?.scrollToEnd({ animated: true });
243
+ buttonOpacity.value = withSpring(0, { duration: 300 });
244
+ setShowBadge(false);
245
+ }, 100);
246
+ } else if (!wasPrepended && !isLoadingEarlier && !isOwnMessage) {
247
+ // Show badge for received messages while scrolled up
248
+ setShowBadge(true);
249
+ }
250
+ }
251
+
252
+ lastMessageCountRef.current = currentCount;
253
+ if (messages.length > 0) {
254
+ // Use same ID logic as precomputeLegendListData (with fallback)
255
+ lastFirstMessageIdRef.current =
256
+ messages[0]?.clientMsgID || messages[0]?.serverMsgID || null;
257
+ lastLastMessageIdRef.current =
258
+ messages[messages.length - 1]?.clientMsgID ||
259
+ messages[messages.length - 1]?.serverMsgID ||
260
+ null;
261
+ }
262
+
263
+ return () => {
264
+ if (timeoutId) clearTimeout(timeoutId);
265
+ };
266
+ }, [messages, isLoadingEarlier, buttonOpacity, currentUserId]);
267
+
268
+ // Cleanup debounce timeouts on unmount
269
+ useEffect(() => {
270
+ return () => {
271
+ if (scrollDebounceRef.current) {
272
+ clearTimeout(scrollDebounceRef.current);
273
+ }
274
+ if (heightDebounceRef.current) {
275
+ clearTimeout(heightDebounceRef.current);
276
+ }
277
+ };
278
+ }, []);
279
+
280
+ if (isLoading) {
281
+ return (
282
+ <KContainer.View flex center background={KColors.white}>
283
+ <ActivityIndicator
284
+ color={KColors.palette.primary.w400}
285
+ size="small"
286
+ />
287
+ </KContainer.View>
288
+ );
289
+ }
290
+
291
+ return (
292
+ <KContainer.View flex background={KColors.white} style={styles.container}>
293
+ <LegendList
294
+ ref={listRef}
295
+ data={messages}
296
+ renderItem={renderItem}
297
+ estimatedItemSize={avgItemHeightRef.current}
298
+ keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
299
+ recycleItems
300
+ alignItemsAtEnd
301
+ maintainVisibleContentPosition={!isLoadingEarlier}
302
+ maintainScrollAtEnd={maintainScrollConfig}
303
+ scrollEventThrottle={16}
304
+ onScroll={handleScroll}
305
+ contentContainerStyle={styles.content}
306
+ scrollIndicatorInsets={{ right: 1 }}
307
+ ListHeaderComponent={
308
+ <LegendChatLoadEarlier isLoading={isLoadingEarlier} />
309
+ }
310
+ onStartReached={handleStartReached}
311
+ onStartReachedThreshold={0.5}
312
+ onEndReached={handleEndReached}
313
+ onEndReachedThreshold={0.2}
314
+ showsVerticalScrollIndicator={false}
315
+ initialScrollAtEnd
316
+ />
317
+ <Animated.View
318
+ style={[styles.scrollToBottomButton, animatedButtonStyle]}
319
+ >
320
+ <KContainer.Touchable
321
+ onPress={handleScrollToBottom}
322
+ activeOpacity={0.7}
323
+ style={styles.buttonInner}
324
+ accessible
325
+ accessibilityLabel="Scroll to latest messages"
326
+ accessibilityRole="button"
327
+ >
328
+ <KLabel.Text
329
+ style={styles.scrollToBottomText}
330
+ accessibilityLabel=""
331
+ >
332
+
333
+ </KLabel.Text>
334
+ {showBadge && <KContainer.View style={styles.badge} />}
335
+ </KContainer.Touchable>
336
+ </Animated.View>
337
+ </KContainer.View>
338
+ );
339
+ }
340
+ );
341
+
342
+ ChatListLegend.displayName = 'ChatListLegend';
343
+
344
+ const styles = StyleSheet.create({
345
+ container: {
346
+ position: 'relative',
347
+ },
348
+ messageRow: {
349
+ width: '100%',
350
+ paddingHorizontal: KSpacingValue['0.75rem'],
351
+ marginBottom: KSpacingValue['0.25rem'],
352
+ },
353
+ messageRowLeft: {
354
+ alignItems: 'flex-start',
355
+ },
356
+ messageRowRight: {
357
+ alignItems: 'flex-end',
358
+ },
359
+ content: {
360
+ paddingVertical: KSpacingValue['0.75rem'],
361
+ flexGrow: 1,
362
+ },
363
+ scrollToBottomButton: {
364
+ position: 'absolute',
365
+ bottom: 20,
366
+ right: 16,
367
+ zIndex: 999,
368
+ },
369
+ buttonInner: {
370
+ position: 'relative',
371
+ width: 40,
372
+ height: 40,
373
+ borderRadius: 20,
374
+ backgroundColor: KColors.white,
375
+ justifyContent: 'center',
376
+ alignItems: 'center',
377
+ shadowColor: KColors.black,
378
+ shadowOffset: { width: 0, height: 1 },
379
+ shadowOpacity: 0.16,
380
+ shadowRadius: 8,
381
+ elevation: 8,
382
+ },
383
+ scrollToBottomText: {
384
+ fontSize: 20,
385
+ fontWeight: 'bold',
386
+ color: KColors.palette.primary.w400,
387
+ },
388
+ errorRow: {
389
+ padding: 16,
390
+ justifyContent: 'center',
391
+ alignItems: 'center',
392
+ },
393
+ badge: {
394
+ position: 'absolute',
395
+ top: -4,
396
+ right: -4,
397
+ width: 12,
398
+ height: 12,
399
+ borderRadius: 6,
400
+ backgroundColor: KColors.palette.primary.w400,
401
+ borderWidth: 2,
402
+ borderColor: KColors.white,
403
+ },
404
+ });
@@ -61,7 +61,13 @@ const QuickActionItem = memo(
61
61
  QuickActionItem.displayName = 'QuickActionItem';
62
62
 
63
63
  export const ChatQuickActions = memo(
64
- ({ actions, onActionPress, renderAction }: ChatQuickActionsProps) => {
64
+ ({
65
+ visible = true,
66
+ actions,
67
+ onActionPress,
68
+ renderAction,
69
+ renderQuickActions,
70
+ }: ChatQuickActionsProps) => {
65
71
  const handleActionPress = useChatActionPress(onActionPress);
66
72
 
67
73
  const renderItem = useCallback<ListRenderItem<DChatQuickAction>>(
@@ -79,10 +85,21 @@ export const ChatQuickActions = memo(
79
85
 
80
86
  const keyExtractor = useCallback((item: DChatQuickAction) => item.id, []);
81
87
 
82
- if (!actions?.length) {
88
+ if (!visible || !actions?.length) {
83
89
  return null;
84
90
  }
85
91
 
92
+ if (renderQuickActions) {
93
+ return (
94
+ <>
95
+ {renderQuickActions({
96
+ actions,
97
+ onActionPress: handleActionPress,
98
+ })}
99
+ </>
100
+ );
101
+ }
102
+
86
103
  return (
87
104
  <KContainer.View paddingT="0.75rem" paddingB="0.25rem">
88
105
  <FlatList
@@ -0,0 +1,52 @@
1
+ import { DChatType } from '../../types/chat';
2
+ import type { DConversationItem } from '../../types/chat';
3
+
4
+ export const getConversationTitle = (
5
+ conversation?: DConversationItem
6
+ ): string => {
7
+ if (!conversation) {
8
+ return 'Tin nhắn';
9
+ }
10
+
11
+ return (
12
+ conversation.peer?.fullName ??
13
+ conversation.peer?.username ??
14
+ conversation.showName ??
15
+ 'Tin nhắn'
16
+ );
17
+ };
18
+
19
+ export const getConversationAvatarUri = (
20
+ conversation?: DConversationItem
21
+ ): string | null => {
22
+ return conversation?.peer?.avatar ?? conversation?.faceURL ?? null;
23
+ };
24
+
25
+ export const getConversationSubtitle = (
26
+ conversation?: DConversationItem
27
+ ): string | undefined => {
28
+ if (!conversation) {
29
+ return undefined;
30
+ }
31
+
32
+ if (conversation?.chatType === DChatType.GROUP) {
33
+ const unreadLabel =
34
+ (conversation?.unreadCount ?? 0) > 0
35
+ ? `${conversation.unreadCount} tin nhắn mới`
36
+ : 'Không có tin mới';
37
+ return `20 thành viên • ${unreadLabel}`;
38
+ }
39
+
40
+ if (conversation?.applicationType === 'MALL') {
41
+ return '338,5K Đã bán • 4,2K Theo dõi';
42
+ }
43
+
44
+ // const lastMessageText = getLastMessageText(conversation.lastMessage);
45
+ return 'Nhấn để xem thông tin';
46
+ };
47
+
48
+ export const shouldShowAddMember = (
49
+ conversation?: DConversationItem
50
+ ): boolean => {
51
+ return conversation?.chatType === DChatType.GROUP;
52
+ };
@@ -9,6 +9,12 @@ export {
9
9
  DEFAULT_ATTACHMENT_ACTIONS,
10
10
  DEFAULT_ATTACHMENT_PANEL_HEIGHT,
11
11
  } from './constants';
12
+ export {
13
+ getConversationTitle,
14
+ getConversationAvatarUri,
15
+ getConversationSubtitle,
16
+ shouldShowAddMember,
17
+ } from './conversationHeader.utils';
12
18
  export type {
13
19
  DChatActionItem,
14
20
  DChatActionIconProvider,
@@ -19,6 +25,7 @@ export type {
19
25
  ChatListProps,
20
26
  ChatComposerProps,
21
27
  ChatQuickActionsProps,
28
+ ChatQuickActionsRenderParams,
22
29
  ChatAttachmentPanelProps,
23
30
  } from './types';
24
31
  export type { DGiftedChatMessage } from '../../utils/giftedChatMessage';
@@ -0,0 +1,70 @@
1
+ import { memo, useMemo } from 'react';
2
+ import { KContainer, KLabel } from '@droppii/libs';
3
+ import { CHAT_BUBBLE_COLORS } from '../constants';
4
+
5
+ const formatTime = (date: Date) =>
6
+ date.toLocaleTimeString('vi-VN', {
7
+ hour: '2-digit',
8
+ minute: '2-digit',
9
+ hour12: false,
10
+ });
11
+
12
+ const formatDayLabel = (createdAt: number): string | null => {
13
+ const date = new Date(createdAt);
14
+ if (Number.isNaN(date.getTime())) return null;
15
+
16
+ const today = new Date();
17
+ today.setHours(0, 0, 0, 0);
18
+
19
+ const messageDay = new Date(date);
20
+ messageDay.setHours(0, 0, 0, 0);
21
+
22
+ if (messageDay.getTime() === today.getTime()) {
23
+ return `${formatTime(date)} Hôm nay`;
24
+ }
25
+
26
+ const yesterday = new Date(today);
27
+ yesterday.setDate(yesterday.getDate() - 1);
28
+
29
+ if (messageDay.getTime() === yesterday.getTime()) {
30
+ return `${formatTime(date)} Hôm qua`;
31
+ }
32
+
33
+ if (messageDay.getFullYear() === today.getFullYear()) {
34
+ return `${formatTime(date)} ${date.toLocaleDateString('vi-VN', {
35
+ day: 'numeric',
36
+ month: 'long',
37
+ })}`;
38
+ }
39
+
40
+ return `${formatTime(date)} ${date.toLocaleDateString('vi-VN', {
41
+ day: 'numeric',
42
+ month: 'long',
43
+ year: 'numeric',
44
+ })}`;
45
+ };
46
+
47
+ interface LegendChatDayProps {
48
+ createdAt: number;
49
+ }
50
+
51
+ export const LegendChatDay = memo(({ createdAt }: LegendChatDayProps) => {
52
+ const dateStr = useMemo(
53
+ () => (createdAt == null ? null : formatDayLabel(createdAt)),
54
+ [createdAt]
55
+ );
56
+
57
+ if (!dateStr) return null;
58
+
59
+ return (
60
+ <KContainer.View center marginV="0.5rem">
61
+ <KContainer.View>
62
+ <KLabel.Text typo="TextXsNormal" color={CHAT_BUBBLE_COLORS.dayLabel}>
63
+ {dateStr}
64
+ </KLabel.Text>
65
+ </KContainer.View>
66
+ </KContainer.View>
67
+ );
68
+ });
69
+
70
+ LegendChatDay.displayName = 'LegendChatDay';
@@ -0,0 +1,21 @@
1
+ import { memo } from 'react';
2
+ import { ActivityIndicator } from 'react-native';
3
+ import { KContainer, KColors } from '@droppii/libs';
4
+
5
+ interface LegendChatLoadEarlierProps {
6
+ isLoading?: boolean;
7
+ }
8
+
9
+ export const LegendChatLoadEarlier = memo(
10
+ ({ isLoading = false }: LegendChatLoadEarlierProps) => {
11
+ if (!isLoading) return null;
12
+
13
+ return (
14
+ <KContainer.View center marginV="0.75rem">
15
+ <ActivityIndicator color={KColors.palette.primary.w400} size="small" />
16
+ </KContainer.View>
17
+ );
18
+ }
19
+ );
20
+
21
+ LegendChatLoadEarlier.displayName = 'LegendChatLoadEarlier';
@@ -0,0 +1,66 @@
1
+ import { memo, ComponentType } from 'react';
2
+ import type { DMessageItem } from '../../../types/chat';
3
+ import type { DChatMessageType } from '../../../types/message';
4
+ import {
5
+ LegendTextMessage,
6
+ LegendImageMessage,
7
+ LegendVideoMessage,
8
+ LegendFileMessage,
9
+ } from './message-types';
10
+ import { DChatMessageType as MessageTypeEnum } from '../../../types/message';
11
+
12
+ interface LegendChatMessageProps {
13
+ message: DMessageItem;
14
+ messageType: DChatMessageType;
15
+ isOutgoing: boolean;
16
+ createdAtTime: number;
17
+ }
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
30
+ };
31
+
32
+ /**
33
+ * Dispatcher component - renders the appropriate message component
34
+ * based on message type. Extensible for new message types.
35
+ */
36
+ 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
+ }
55
+
56
+ return (
57
+ <MessageComponent
58
+ message={message}
59
+ isOutgoing={isOutgoing}
60
+ createdAtTime={createdAtTime}
61
+ />
62
+ );
63
+ }
64
+ );
65
+
66
+ LegendChatMessage.displayName = 'LegendChatMessage';