@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.
- package/lib/module/config/feature-flags.js +38 -0
- package/lib/module/config/feature-flags.js.map +1 -0
- package/lib/module/hooks/query-keys.js +4 -0
- package/lib/module/hooks/query-keys.js.map +1 -1
- package/lib/module/hooks/useChatMessages.js +45 -0
- package/lib/module/hooks/useChatMessages.js.map +1 -1
- package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js +17 -0
- package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js.map +1 -0
- package/lib/module/hooks/useLinkPreview/useLinkPreview.js +34 -0
- package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +18 -2
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +110 -20
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatLinkPreview.js +79 -0
- package/lib/module/screens/chat-detail/ChatLinkPreview.js.map +1 -0
- package/lib/module/screens/chat-detail/ChatList.js +2 -0
- package/lib/module/screens/chat-detail/ChatList.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +352 -0
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -0
- package/lib/module/screens/chat-detail/ChatQuickActions.js +12 -2
- package/lib/module/screens/chat-detail/ChatQuickActions.js.map +1 -1
- package/lib/module/screens/chat-detail/conversationHeader.utils.js +31 -0
- package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -0
- package/lib/module/screens/chat-detail/index.js +1 -0
- package/lib/module/screens/chat-detail/index.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatDay.js +57 -0
- package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js +21 -0
- package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +47 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js +58 -0
- package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/message-types.js +122 -0
- package/lib/module/screens/chat-detail/legend/message-types.js.map +1 -0
- package/lib/module/screens/chat-detail/messages/ChatMessageBubble.js.map +1 -1
- package/lib/module/services/apis.js +1 -1
- package/lib/module/services/apis.js.map +1 -1
- package/lib/module/services/endpoints.js +8 -0
- package/lib/module/services/endpoints.js.map +1 -0
- package/lib/module/types/common.js +2 -0
- package/lib/module/types/common.js.map +1 -0
- package/lib/module/utils/legendListMessage.js +80 -0
- package/lib/module/utils/legendListMessage.js.map +1 -0
- package/lib/module/utils/url.js +7 -0
- package/lib/module/utils/url.js.map +1 -0
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +1 -1
- package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts +1 -1
- package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts.map +1 -1
- package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts +1 -1
- package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts.map +1 -1
- package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts +1 -1
- package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts.map +1 -1
- package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +1 -1
- package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
- package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +1 -1
- package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
- package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts +1 -1
- package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
- package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts +1 -1
- package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts.map +1 -1
- package/lib/typescript/src/config/feature-flags.d.ts +12 -0
- package/lib/typescript/src/config/feature-flags.d.ts.map +1 -0
- package/lib/typescript/src/context/ChatContext.d.ts +1 -1
- package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/query-keys.d.ts +4 -0
- package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useChatMessages.d.ts +3 -0
- package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts +3 -0
- package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts +7 -0
- package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts +9 -0
- package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/ChatList.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatList.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +3 -0
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +6 -0
- package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/index.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts +6 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +15 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts +6 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts +12 -0
- package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts +30 -3
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/screens/inbox/Inbox.d.ts +1 -1
- package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
- package/lib/typescript/src/screens/inbox/MessagesTab.d.ts +1 -1
- package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
- package/lib/typescript/src/services/apis.d.ts +1 -0
- package/lib/typescript/src/services/apis.d.ts.map +1 -1
- package/lib/typescript/src/services/endpoints.d.ts +6 -0
- package/lib/typescript/src/services/endpoints.d.ts.map +1 -0
- package/lib/typescript/src/types/common.d.ts +6 -0
- package/lib/typescript/src/types/common.d.ts.map +1 -0
- package/lib/typescript/src/utils/legendListMessage.d.ts +25 -0
- package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -0
- package/lib/typescript/src/utils/url.d.ts +2 -0
- package/lib/typescript/src/utils/url.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/config/feature-flags.ts +49 -0
- package/src/hooks/query-keys.ts +5 -0
- package/src/hooks/useChatMessages.ts +60 -0
- package/src/hooks/useLinkPreview/useFetchUrlMetadata.ts +18 -0
- package/src/hooks/useLinkPreview/useLinkPreview.ts +30 -0
- package/src/index.tsx +1 -0
- package/src/screens/chat-detail/ChatComposer.tsx +21 -0
- package/src/screens/chat-detail/ChatDetail.tsx +154 -28
- package/src/screens/chat-detail/ChatLinkPreview.tsx +86 -0
- package/src/screens/chat-detail/ChatList.tsx +3 -0
- package/src/screens/chat-detail/ChatListLegend.tsx +404 -0
- package/src/screens/chat-detail/ChatQuickActions.tsx +19 -2
- package/src/screens/chat-detail/conversationHeader.utils.ts +52 -0
- package/src/screens/chat-detail/index.ts +7 -0
- package/src/screens/chat-detail/legend/LegendChatDay.tsx +70 -0
- package/src/screens/chat-detail/legend/LegendChatLoadEarlier.tsx +21 -0
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +66 -0
- package/src/screens/chat-detail/legend/LegendChatScrollToBottom.tsx +56 -0
- package/src/screens/chat-detail/legend/message-types.tsx +149 -0
- package/src/screens/chat-detail/messages/ChatMessageBubble.tsx +0 -1
- package/src/screens/chat-detail/types.ts +43 -3
- package/src/services/apis.ts +1 -1
- package/src/services/endpoints.ts +5 -0
- package/src/types/common.ts +5 -0
- package/src/utils/legendListMessage.ts +102 -0
- 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
|
-
({
|
|
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';
|