@droppii-org/chat-mobile 0.2.3 → 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 (167) hide show
  1. package/lib/module/components/ThreadCard/NamePrefixIcon.js +2 -3
  2. package/lib/module/components/ThreadCard/NamePrefixIcon.js.map +1 -1
  3. package/lib/module/config/feature-flags.js +38 -0
  4. package/lib/module/config/feature-flags.js.map +1 -0
  5. package/lib/module/hooks/query-keys.js +4 -0
  6. package/lib/module/hooks/query-keys.js.map +1 -1
  7. package/lib/module/hooks/useChatMessages.js +45 -0
  8. package/lib/module/hooks/useChatMessages.js.map +1 -1
  9. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js +17 -0
  10. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js.map +1 -0
  11. package/lib/module/hooks/useLinkPreview/useLinkPreview.js +34 -0
  12. package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -0
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/module/screens/chat-detail/ChatComposer.js +24 -7
  15. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  16. package/lib/module/screens/chat-detail/ChatDetail.js +119 -19
  17. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  18. package/lib/module/screens/chat-detail/ChatDetailHeader.js +43 -20
  19. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  20. package/lib/module/screens/chat-detail/ChatLinkPreview.js +79 -0
  21. package/lib/module/screens/chat-detail/ChatLinkPreview.js.map +1 -0
  22. package/lib/module/screens/chat-detail/ChatList.js +2 -0
  23. package/lib/module/screens/chat-detail/ChatList.js.map +1 -1
  24. package/lib/module/screens/chat-detail/ChatListLegend.js +352 -0
  25. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -0
  26. package/lib/module/screens/chat-detail/ChatQuickActions.js +12 -2
  27. package/lib/module/screens/chat-detail/ChatQuickActions.js.map +1 -1
  28. package/lib/module/screens/chat-detail/conversationHeader.utils.js +31 -0
  29. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -0
  30. package/lib/module/screens/chat-detail/index.js +1 -0
  31. package/lib/module/screens/chat-detail/index.js.map +1 -1
  32. package/lib/module/screens/chat-detail/legend/LegendChatDay.js +57 -0
  33. package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -0
  34. package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js +21 -0
  35. package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js.map +1 -0
  36. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +47 -0
  37. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -0
  38. package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js +58 -0
  39. package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js.map +1 -0
  40. package/lib/module/screens/chat-detail/legend/message-types.js +122 -0
  41. package/lib/module/screens/chat-detail/legend/message-types.js.map +1 -0
  42. package/lib/module/screens/chat-detail/messages/ChatMessageBubble.js.map +1 -1
  43. package/lib/module/services/apis.js +1 -1
  44. package/lib/module/services/apis.js.map +1 -1
  45. package/lib/module/services/endpoints.js +8 -0
  46. package/lib/module/services/endpoints.js.map +1 -0
  47. package/lib/module/types/common.js +2 -0
  48. package/lib/module/types/common.js.map +1 -0
  49. package/lib/module/utils/legendListMessage.js +80 -0
  50. package/lib/module/utils/legendListMessage.js.map +1 -0
  51. package/lib/module/utils/url.js +7 -0
  52. package/lib/module/utils/url.js.map +1 -0
  53. package/lib/typescript/src/components/Avatar/Avatar.d.ts +1 -1
  54. package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -1
  55. package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts +1 -1
  56. package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts.map +1 -1
  57. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts +1 -1
  58. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts.map +1 -1
  59. package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts +1 -1
  60. package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts.map +1 -1
  61. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +1 -1
  62. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  63. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +1 -1
  64. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
  65. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts +1 -1
  66. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  67. package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts +1 -1
  68. package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts.map +1 -1
  69. package/lib/typescript/src/config/feature-flags.d.ts +12 -0
  70. package/lib/typescript/src/config/feature-flags.d.ts.map +1 -0
  71. package/lib/typescript/src/context/ChatContext.d.ts +1 -1
  72. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  73. package/lib/typescript/src/hooks/query-keys.d.ts +4 -0
  74. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  75. package/lib/typescript/src/hooks/useChatMessages.d.ts +3 -0
  76. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  77. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts +3 -0
  78. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts.map +1 -0
  79. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts +7 -0
  80. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -0
  81. package/lib/typescript/src/index.d.ts +1 -1
  82. package/lib/typescript/src/index.d.ts.map +1 -1
  83. package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts +1 -1
  84. package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts.map +1 -1
  85. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +1 -1
  86. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  87. package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts +1 -1
  88. package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts.map +1 -1
  89. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  90. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  91. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
  92. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  93. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts +9 -0
  94. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts.map +1 -0
  95. package/lib/typescript/src/screens/chat-detail/ChatList.d.ts +1 -1
  96. package/lib/typescript/src/screens/chat-detail/ChatList.d.ts.map +1 -1
  97. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +3 -0
  98. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -0
  99. package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts +1 -1
  100. package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts.map +1 -1
  101. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts +1 -1
  102. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts +1 -1
  104. package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts.map +1 -1
  105. package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts +1 -1
  106. package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts.map +1 -1
  107. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +6 -0
  108. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -0
  109. package/lib/typescript/src/screens/chat-detail/index.d.ts +2 -1
  110. package/lib/typescript/src/screens/chat-detail/index.d.ts.map +1 -1
  111. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -0
  112. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -0
  113. package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts +6 -0
  114. package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts.map +1 -0
  115. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +15 -0
  116. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -0
  117. package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts +6 -0
  118. package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts.map +1 -0
  119. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts +12 -0
  120. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts.map +1 -0
  121. package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts +1 -1
  122. package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts.map +1 -1
  123. package/lib/typescript/src/screens/chat-detail/types.d.ts +34 -5
  124. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  125. package/lib/typescript/src/screens/inbox/Inbox.d.ts +1 -1
  126. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  127. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts +1 -1
  128. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  129. package/lib/typescript/src/services/apis.d.ts +1 -0
  130. package/lib/typescript/src/services/apis.d.ts.map +1 -1
  131. package/lib/typescript/src/services/endpoints.d.ts +6 -0
  132. package/lib/typescript/src/services/endpoints.d.ts.map +1 -0
  133. package/lib/typescript/src/types/common.d.ts +6 -0
  134. package/lib/typescript/src/types/common.d.ts.map +1 -0
  135. package/lib/typescript/src/utils/legendListMessage.d.ts +25 -0
  136. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -0
  137. package/lib/typescript/src/utils/url.d.ts +2 -0
  138. package/lib/typescript/src/utils/url.d.ts.map +1 -0
  139. package/package.json +4 -2
  140. package/src/components/ThreadCard/NamePrefixIcon.tsx +2 -3
  141. package/src/config/feature-flags.ts +49 -0
  142. package/src/hooks/query-keys.ts +5 -0
  143. package/src/hooks/useChatMessages.ts +60 -0
  144. package/src/hooks/useLinkPreview/useFetchUrlMetadata.ts +18 -0
  145. package/src/hooks/useLinkPreview/useLinkPreview.ts +30 -0
  146. package/src/index.tsx +1 -0
  147. package/src/screens/chat-detail/ChatComposer.tsx +30 -9
  148. package/src/screens/chat-detail/ChatDetail.tsx +163 -27
  149. package/src/screens/chat-detail/ChatDetailHeader.tsx +58 -26
  150. package/src/screens/chat-detail/ChatLinkPreview.tsx +86 -0
  151. package/src/screens/chat-detail/ChatList.tsx +3 -0
  152. package/src/screens/chat-detail/ChatListLegend.tsx +404 -0
  153. package/src/screens/chat-detail/ChatQuickActions.tsx +19 -2
  154. package/src/screens/chat-detail/conversationHeader.utils.ts +52 -0
  155. package/src/screens/chat-detail/index.ts +7 -0
  156. package/src/screens/chat-detail/legend/LegendChatDay.tsx +70 -0
  157. package/src/screens/chat-detail/legend/LegendChatLoadEarlier.tsx +21 -0
  158. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +66 -0
  159. package/src/screens/chat-detail/legend/LegendChatScrollToBottom.tsx +56 -0
  160. package/src/screens/chat-detail/legend/message-types.tsx +149 -0
  161. package/src/screens/chat-detail/messages/ChatMessageBubble.tsx +0 -1
  162. package/src/screens/chat-detail/types.ts +47 -5
  163. package/src/services/apis.ts +1 -1
  164. package/src/services/endpoints.ts +5 -0
  165. package/src/types/common.ts +5 -0
  166. package/src/utils/legendListMessage.ts +102 -0
  167. package/src/utils/url.ts +5 -0
@@ -0,0 +1,86 @@
1
+ import { memo } from 'react';
2
+ import { ActivityIndicator, StyleSheet } from 'react-native';
3
+ import {
4
+ KContainer,
5
+ KImage,
6
+ KLabel,
7
+ KColors,
8
+ KSpacingValue,
9
+ KDims,
10
+ } from '@droppii/libs';
11
+ import type { IUrlMetadata } from '../../types/common';
12
+
13
+ interface ChatLinkPreviewProps {
14
+ metadata?: IUrlMetadata;
15
+ isLoading?: boolean;
16
+ onDismiss?: () => void;
17
+ }
18
+
19
+ const HEIGHT = 51;
20
+
21
+ export const ChatLinkPreview = memo(
22
+ ({ metadata, isLoading, onDismiss }: ChatLinkPreviewProps) => {
23
+ return (
24
+ <KContainer.View style={styles.container}>
25
+ <KContainer.View style={styles.left}>
26
+ {isLoading ? (
27
+ <ActivityIndicator color={KColors.primary.normal} size={16} />
28
+ ) : metadata?.image ? (
29
+ <KImage.Base uri={metadata?.image} width={HEIGHT} height={HEIGHT} />
30
+ ) : (
31
+ <KImage.VectorIcons
32
+ name="link-alt-o"
33
+ color={KColors.primary.normal}
34
+ />
35
+ )}
36
+ </KContainer.View>
37
+ <KContainer.View style={styles.center}>
38
+ <KLabel.Text typo="TextNmMedium" numberOfLines={1}>
39
+ {metadata?.title}
40
+ </KLabel.Text>
41
+ <KLabel.Text
42
+ typo="TextSmNormal"
43
+ color={KColors.gray.light}
44
+ numberOfLines={1}
45
+ >
46
+ {metadata?.description}
47
+ </KLabel.Text>
48
+ </KContainer.View>
49
+ <KContainer.Touchable onPress={onDismiss} style={styles.right}>
50
+ <KImage.VectorIcons name="close" size={16} />
51
+ </KContainer.Touchable>
52
+ </KContainer.View>
53
+ );
54
+ }
55
+ );
56
+
57
+ ChatLinkPreview.displayName = 'ChatLinkPreview';
58
+
59
+ const styles = StyleSheet.create({
60
+ container: {
61
+ position: 'absolute',
62
+ flexDirection: 'row',
63
+ flex: 1,
64
+ width: KDims.width,
65
+ alignItems: 'center',
66
+ gap: KSpacingValue['0.5rem'],
67
+ backgroundColor: KColors.palette.blue.w25,
68
+ height: HEIGHT,
69
+ top: -HEIGHT,
70
+ zIndex: 10000,
71
+ },
72
+ left: {
73
+ width: HEIGHT,
74
+ height: HEIGHT,
75
+ justifyContent: 'center',
76
+ alignItems: 'center',
77
+ },
78
+ center: {
79
+ flex: 1,
80
+ },
81
+ right: {
82
+ paddingRight: KSpacingValue['0.5rem'],
83
+ justifyContent: 'center',
84
+ alignItems: 'center',
85
+ },
86
+ });
@@ -9,6 +9,7 @@ import {
9
9
  import { KContainer, KColors, KSpacingValue } from '@droppii/libs';
10
10
  import { mapOpenIMMessagesToGiftedChat } from '../../utils/giftedChatMessage';
11
11
  import type { DGiftedChatMessage } from '../../utils/giftedChatMessage';
12
+
12
13
  import type { ChatListProps } from './types';
13
14
  import { ChatDay } from './ChatDay';
14
15
  import { ChatLoadEarlier } from './ChatLoadEarlier';
@@ -24,6 +25,8 @@ export const ChatList = memo(
24
25
  isLoading,
25
26
  isLoadingEarlier,
26
27
  hasMoreEarlier,
28
+ // Note: onLoadNewer, isLoadingNewer, hasMoreNewer are not used in GiftedChat
29
+ // as it has its own pagination mechanism. They're kept for API compatibility.
27
30
  }: ChatListProps) => {
28
31
  const giftedMessages = useMemo(
29
32
  () => mapOpenIMMessagesToGiftedChat(messages),
@@ -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';