@droppii-org/chat-mobile 0.2.42 → 0.2.43
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/components/messages/quotedMessage/index.js +6 -4
- package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
- package/lib/module/context/ChatContext.js +5 -0
- package/lib/module/context/ChatContext.js.map +1 -1
- package/lib/module/core/useChatListener.js +12 -3
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/message/useMessages.js +65 -0
- package/lib/module/hooks/message/useMessages.js.map +1 -0
- package/lib/module/hooks/message/useSendMessage.js +10 -2
- package/lib/module/hooks/message/useSendMessage.js.map +1 -1
- package/lib/module/index.js +0 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +35 -30
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +83 -84
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +9 -9
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +9 -3
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/services/attachmentOrchestrator.js +4 -0
- package/lib/module/services/attachmentOrchestrator.js.map +1 -1
- package/lib/module/store/message.js +148 -6
- package/lib/module/store/message.js.map +1 -1
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/types/events.js +8 -0
- package/lib/module/types/events.js.map +1 -0
- package/lib/module/utils/conversation.js +13 -1
- package/lib/module/utils/conversation.js.map +1 -1
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts +0 -1
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useMessages.d.ts +16 -0
- package/lib/typescript/src/hooks/message/useMessages.d.ts.map +1 -0
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +0 -1
- package/lib/typescript/src/index.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/ChatListLegend.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts +7 -7
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
- package/lib/typescript/src/store/message.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +18 -2
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +8 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -0
- package/lib/typescript/src/utils/conversation.d.ts +1 -0
- package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/messages/quotedMessage/index.tsx +8 -8
- package/src/context/ChatContext.tsx +5 -0
- package/src/core/useChatListener.ts +18 -2
- package/src/hooks/message/useMessages.ts +82 -0
- package/src/hooks/message/useSendMessage.ts +13 -2
- package/src/index.tsx +0 -1
- package/src/screens/chat-detail/ChatDetail.tsx +37 -28
- package/src/screens/chat-detail/ChatListLegend.tsx +96 -90
- package/src/screens/chat-detail/PinnedMessagesSheet.tsx +12 -7
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +11 -3
- package/src/screens/chat-detail/types.ts +7 -7
- package/src/services/attachmentOrchestrator.ts +5 -0
- package/src/store/message.ts +174 -8
- package/src/types/chat.ts +20 -2
- package/src/types/events.ts +8 -0
- package/src/utils/conversation.ts +14 -0
- package/lib/module/hooks/useChatMessages.js +0 -155
- package/lib/module/hooks/useChatMessages.js.map +0 -1
- package/lib/typescript/src/hooks/useChatMessages.d.ts +0 -21
- package/lib/typescript/src/hooks/useChatMessages.d.ts.map +0 -1
- package/src/hooks/useChatMessages.ts +0 -246
|
@@ -17,6 +17,7 @@ import { useChatContext } from '../context';
|
|
|
17
17
|
import isEmpty from 'lodash/isEmpty';
|
|
18
18
|
import { markMessageRevoked, useMessageStore } from '../store/message';
|
|
19
19
|
import { syncPinnedMessagesOnEvent } from '../hooks/message/usePinMessage';
|
|
20
|
+
import { belongsToConversation } from '../utils/message';
|
|
20
21
|
|
|
21
22
|
export const useChatListener = () => {
|
|
22
23
|
const updateOneMessage = useMessageStore((state) => state.updateOneMessage);
|
|
@@ -119,11 +120,24 @@ export const useChatListener = () => {
|
|
|
119
120
|
[updateOneMessage]
|
|
120
121
|
);
|
|
121
122
|
|
|
123
|
+
const onRecvNewMessages = useCallback((messages: MessageItem[]) => {
|
|
124
|
+
console.log('[Listener] OnRecvNewMessages', messages);
|
|
125
|
+
const conversationId =
|
|
126
|
+
useConversationStore.getState().currentConversationId;
|
|
127
|
+
if (!conversationId) return;
|
|
128
|
+
const relevant = messages.filter((message) =>
|
|
129
|
+
belongsToConversation(message, conversationId)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!relevant.length) return;
|
|
133
|
+
|
|
134
|
+
relevant.forEach((msg) => useMessageStore.getState().pushNewMessage(msg));
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
122
137
|
useEffect(() => {
|
|
123
138
|
// Conversation
|
|
124
139
|
OpenIMSDK.on(OpenIMEvent.OnConversationChanged, onConversationChanged);
|
|
125
140
|
OpenIMSDK.on(OpenIMEvent.OnNewConversation, onNewConversation);
|
|
126
|
-
// OpenIMSDK.on(OpenIMEvent.OnRecvNewMessages, onRecvNewMessages);
|
|
127
141
|
|
|
128
142
|
// Group members
|
|
129
143
|
OpenIMSDK.on(OpenIMEvent.OnGroupMemberAdded, onGroupMemberAdded);
|
|
@@ -133,6 +147,7 @@ export const useChatListener = () => {
|
|
|
133
147
|
OpenIMSDK.on(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
|
|
134
148
|
OpenIMSDK.on(OpenIMEvent.OnNewRecvMessageRevoked, onMessageRevoked);
|
|
135
149
|
OpenIMSDK.on(OpenIMEvent.OnRecvMessagePinned, onMessagePinned);
|
|
150
|
+
OpenIMSDK.on(OpenIMEvent.OnRecvNewMessages, onRecvNewMessages);
|
|
136
151
|
|
|
137
152
|
return () => {
|
|
138
153
|
// Conversation
|
|
@@ -145,7 +160,7 @@ export const useChatListener = () => {
|
|
|
145
160
|
|
|
146
161
|
// Message status
|
|
147
162
|
OpenIMSDK.off(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
|
|
148
|
-
|
|
163
|
+
OpenIMSDK.off(OpenIMEvent.OnRecvNewMessages, onRecvNewMessages);
|
|
149
164
|
OpenIMSDK.off(OpenIMEvent.OnNewRecvMessageRevoked, onMessageRevoked);
|
|
150
165
|
OpenIMSDK.off(OpenIMEvent.OnRecvMessagePinned, onMessagePinned);
|
|
151
166
|
};
|
|
@@ -157,5 +172,6 @@ export const useChatListener = () => {
|
|
|
157
172
|
onMessageRevoked,
|
|
158
173
|
onNewConversation,
|
|
159
174
|
onSendMessageProgress,
|
|
175
|
+
onRecvNewMessages,
|
|
160
176
|
]);
|
|
161
177
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
import { useMessageStore } from '../../store/message';
|
|
3
|
+
import { useChatContext } from '../../context';
|
|
4
|
+
|
|
5
|
+
interface IUseMessagesProps {
|
|
6
|
+
conversationId?: string;
|
|
7
|
+
startClientMsgId?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useMessages = ({
|
|
11
|
+
conversationId,
|
|
12
|
+
startClientMsgId,
|
|
13
|
+
}: IUseMessagesProps) => {
|
|
14
|
+
const messages = useMessageStore((s) => s.messages);
|
|
15
|
+
const { applicationType } = useChatContext();
|
|
16
|
+
const getMoreOldMessages = useMessageStore((s) => s.getMoreOldMessages);
|
|
17
|
+
const getMoreNewMessages = useMessageStore((s) => s.getMoreNewMessages);
|
|
18
|
+
const searchMessages = useMessageStore((s) => s.searchMessages);
|
|
19
|
+
const initLoading = useMessageStore((s) => s.initLoading);
|
|
20
|
+
const moreOldLoading = useMessageStore((s) => s.moreOldLoading);
|
|
21
|
+
const moreNewLoading = useMessageStore((s) => s.moreNewLoading);
|
|
22
|
+
const hasMoreOld = useMessageStore((s) => s.hasMoreOld);
|
|
23
|
+
const hasMoreNew = useMessageStore((s) => s.hasMoreNew);
|
|
24
|
+
|
|
25
|
+
const handleLoadMoreOld = useCallback(
|
|
26
|
+
(loadMore: boolean) => {
|
|
27
|
+
if (!conversationId || !applicationType) return;
|
|
28
|
+
getMoreOldMessages({
|
|
29
|
+
loadMore,
|
|
30
|
+
applicationType: applicationType,
|
|
31
|
+
conversationId: conversationId,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
[getMoreOldMessages, applicationType, conversationId]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const handleLoadMoreNew = useCallback(
|
|
38
|
+
(loadMore: boolean) => {
|
|
39
|
+
if (!conversationId || !applicationType) return;
|
|
40
|
+
getMoreNewMessages({
|
|
41
|
+
loadMore,
|
|
42
|
+
applicationType: applicationType,
|
|
43
|
+
conversationId: conversationId,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
[getMoreNewMessages, applicationType, conversationId]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!conversationId || !applicationType) return;
|
|
51
|
+
if (startClientMsgId) {
|
|
52
|
+
searchMessages({
|
|
53
|
+
applicationType,
|
|
54
|
+
conversationId,
|
|
55
|
+
startClientMsgId,
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
getMoreOldMessages({
|
|
59
|
+
loadMore: false,
|
|
60
|
+
applicationType,
|
|
61
|
+
conversationId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}, [
|
|
65
|
+
conversationId,
|
|
66
|
+
applicationType,
|
|
67
|
+
startClientMsgId,
|
|
68
|
+
getMoreOldMessages,
|
|
69
|
+
searchMessages,
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
messages,
|
|
74
|
+
getMoreNewMessages: handleLoadMoreNew,
|
|
75
|
+
getMoreOldMessages: handleLoadMoreOld,
|
|
76
|
+
initLoading,
|
|
77
|
+
moreOldLoading,
|
|
78
|
+
moreNewLoading,
|
|
79
|
+
hasMoreOld,
|
|
80
|
+
hasMoreNew,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -16,12 +16,17 @@ import {
|
|
|
16
16
|
type IMessageItemEx,
|
|
17
17
|
} from '../../types/chat';
|
|
18
18
|
import { useChatContext, useChatDetailContext } from '../../context';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
generateContentBasedOnMessageType,
|
|
21
|
+
getBotId,
|
|
22
|
+
} from '../../utils/conversation';
|
|
20
23
|
import { markMessageRevoked, useMessageStore } from '../../store/message';
|
|
21
24
|
import uuid from 'react-native-uuid';
|
|
22
25
|
|
|
23
26
|
import type { IUrlMetadata } from '../../types/common';
|
|
24
27
|
import type { IRunConversationFlowPayload } from '../../types/flows';
|
|
28
|
+
import { DeviceEventEmitter } from 'react-native';
|
|
29
|
+
import { ChatEvent } from '../../types/events';
|
|
25
30
|
|
|
26
31
|
const buildSendParams = (
|
|
27
32
|
message: MessageItem,
|
|
@@ -68,6 +73,8 @@ export const useSendMessage = () => {
|
|
|
68
73
|
} catch {
|
|
69
74
|
updateOneMessage({ ...message, status: MessageStatus.Failed });
|
|
70
75
|
}
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
77
|
+
DeviceEventEmitter.emit(ChatEvent.ScrollToBottom);
|
|
71
78
|
},
|
|
72
79
|
[pushNewMessage, updateOneMessage, deleteOneMessage]
|
|
73
80
|
);
|
|
@@ -192,6 +199,8 @@ export const useSendMessage = () => {
|
|
|
192
199
|
} catch {
|
|
193
200
|
updateOneMessage({ ...message, status: MessageStatus.Failed });
|
|
194
201
|
}
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
203
|
+
DeviceEventEmitter.emit(ChatEvent.ScrollToBottom);
|
|
195
204
|
},
|
|
196
205
|
[pushNewMessage, updateOneMessage, deleteOneMessage]
|
|
197
206
|
);
|
|
@@ -233,7 +242,9 @@ export const generateOfflinePushInfo = (
|
|
|
233
242
|
ex: JSON.stringify({
|
|
234
243
|
icon: conversationData?.faceURL || '',
|
|
235
244
|
conversationId: conversationData?.conversationID || '',
|
|
236
|
-
operatorUserID,
|
|
245
|
+
userId: operatorUserID,
|
|
246
|
+
groupId: conversationData?.groupID || '',
|
|
247
|
+
botId: getBotId(conversationData) || '',
|
|
237
248
|
title,
|
|
238
249
|
desc,
|
|
239
250
|
}),
|
package/src/index.tsx
CHANGED
|
@@ -13,7 +13,6 @@ export * from './core';
|
|
|
13
13
|
export * from './store';
|
|
14
14
|
export * from './context';
|
|
15
15
|
export * from './hooks/useConversationList';
|
|
16
|
-
export * from './hooks/useChatMessages';
|
|
17
16
|
export * from './hooks/message/useSendMessage';
|
|
18
17
|
export { useImageAttachment } from './hooks/useImageAttachment';
|
|
19
18
|
export { useMediaViewer } from './hooks/useMediaViewer';
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import { Platform, StyleSheet } from 'react-native';
|
|
2
|
+
import { DeviceEventEmitter, Platform, StyleSheet } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
|
|
5
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
6
|
import { KContainer } from '@droppii/libs';
|
|
7
7
|
import { useNavigation } from '@react-navigation/native';
|
|
8
|
-
import { useChatMessages } from '../../hooks/useChatMessages';
|
|
9
8
|
import { usePinMessage } from '../../hooks/message/usePinMessage';
|
|
10
9
|
import { PinnedMessageBar } from './PinnedMessageBar';
|
|
11
10
|
import { openPinnedMessagesSheet } from './PinnedMessagesSheet';
|
|
@@ -13,7 +12,11 @@ import {
|
|
|
13
12
|
MediaViewerModal,
|
|
14
13
|
type MediaViewerModalHandle,
|
|
15
14
|
} from '../../components/MediaViewerModal';
|
|
16
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
useConversation,
|
|
17
|
+
useConversationStore,
|
|
18
|
+
useUserStore,
|
|
19
|
+
} from '../../store';
|
|
17
20
|
import ChatDetailHeader from './ChatDetailHeader';
|
|
18
21
|
import { ChatListLegend } from './ChatListLegend';
|
|
19
22
|
import { ChatComposer } from './ChatComposer';
|
|
@@ -29,6 +32,9 @@ import {
|
|
|
29
32
|
} from '../../hooks/conversation/useGetConversationDetail';
|
|
30
33
|
import { PeerType } from '@droppii/openim-rn-client-sdk';
|
|
31
34
|
import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
35
|
+
import { useMessages } from '../../hooks/message/useMessages';
|
|
36
|
+
import { getBotId } from '../../utils/conversation';
|
|
37
|
+
import { ChatEvent } from '../../types/events';
|
|
32
38
|
|
|
33
39
|
const ChatDetail = memo(
|
|
34
40
|
({
|
|
@@ -36,7 +42,6 @@ const ChatDetail = memo(
|
|
|
36
42
|
targetUserId = '',
|
|
37
43
|
targetGroupId = '',
|
|
38
44
|
targetBotId = '',
|
|
39
|
-
enabled = true,
|
|
40
45
|
showAddMember,
|
|
41
46
|
onBack,
|
|
42
47
|
onPressSearch,
|
|
@@ -50,6 +55,7 @@ const ChatDetail = memo(
|
|
|
50
55
|
const navigation = useNavigation();
|
|
51
56
|
const mediaViewerRef = useRef<MediaViewerModalHandle>(null);
|
|
52
57
|
const messageToolboxRef = useRef<MessageToolboxRef>(null);
|
|
58
|
+
const currentUser = useUserStore((s) => s.user);
|
|
53
59
|
|
|
54
60
|
const { conversationId, isLoading: isLoadingConversation } =
|
|
55
61
|
useGetConversationDetail({
|
|
@@ -66,17 +72,10 @@ const ChatDetail = memo(
|
|
|
66
72
|
if (targetBotId) {
|
|
67
73
|
return targetBotId;
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
const id = JSON.parse(conversation?.ex ?? '{}')?.sessionInfo?.data
|
|
71
|
-
?.botId;
|
|
72
|
-
return id;
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
74
|
-
} catch (_: any) {
|
|
75
|
-
return undefined;
|
|
76
|
-
}
|
|
75
|
+
return getBotId(conversation);
|
|
77
76
|
}
|
|
78
77
|
return undefined;
|
|
79
|
-
}, [conversation
|
|
78
|
+
}, [conversation, targetBotId]);
|
|
80
79
|
|
|
81
80
|
const { data: botInfo } = useGetStatusConversationBot(botId);
|
|
82
81
|
|
|
@@ -98,12 +97,14 @@ const ChatDetail = memo(
|
|
|
98
97
|
|
|
99
98
|
const {
|
|
100
99
|
messages,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
initLoading,
|
|
101
|
+
moreOldLoading,
|
|
102
|
+
moreNewLoading,
|
|
103
|
+
getMoreOldMessages,
|
|
104
|
+
getMoreNewMessages,
|
|
105
|
+
hasMoreOld,
|
|
106
|
+
hasMoreNew,
|
|
107
|
+
} = useMessages({ conversationId });
|
|
107
108
|
|
|
108
109
|
const { pinnedMessages, pinMessage, unpinMessage } =
|
|
109
110
|
usePinMessage(conversationId);
|
|
@@ -143,11 +144,16 @@ const ChatDetail = memo(
|
|
|
143
144
|
|
|
144
145
|
const handlePressPinnedMessageBar = useCallback(() => {
|
|
145
146
|
openPinnedMessagesSheet({
|
|
146
|
-
currentUserId,
|
|
147
|
+
currentUserId: currentUser?.userID,
|
|
147
148
|
unpinMessage,
|
|
148
149
|
customMessageItemFactory,
|
|
150
|
+
onPressMessage: (message: MessageItem) => {
|
|
151
|
+
DeviceEventEmitter.emit(ChatEvent.ScrollToMessage, {
|
|
152
|
+
clientMessageId: message?.clientMsgID,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
149
155
|
});
|
|
150
|
-
}, [
|
|
156
|
+
}, [currentUser, unpinMessage, customMessageItemFactory]);
|
|
151
157
|
|
|
152
158
|
return (
|
|
153
159
|
<KContainer.Page flex edges={['bottom']}>
|
|
@@ -163,7 +169,7 @@ const ChatDetail = memo(
|
|
|
163
169
|
|
|
164
170
|
<PinnedMessageBar
|
|
165
171
|
messages={pinnedMessages}
|
|
166
|
-
currentUserId={
|
|
172
|
+
currentUserId={currentUser?.userID}
|
|
167
173
|
onPress={handlePressPinnedMessageBar}
|
|
168
174
|
/>
|
|
169
175
|
|
|
@@ -174,12 +180,15 @@ const ChatDetail = memo(
|
|
|
174
180
|
>
|
|
175
181
|
<ChatListLegend
|
|
176
182
|
messages={messages}
|
|
177
|
-
|
|
183
|
+
conversationId={conversationId}
|
|
178
184
|
customMessageItemFactory={customMessageItemFactory}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
isLoading={initLoading || isLoadingConversation}
|
|
186
|
+
onLoadMoreOld={getMoreOldMessages}
|
|
187
|
+
hasMoreOld={hasMoreOld}
|
|
188
|
+
moreOldLoading={moreOldLoading}
|
|
189
|
+
onLoadMoreNew={getMoreNewMessages}
|
|
190
|
+
hasMoreNew={hasMoreNew}
|
|
191
|
+
moreNewLoading={moreNewLoading}
|
|
183
192
|
onMediaPress={handleMediaPress}
|
|
184
193
|
onPressUrl={handlePressUrl}
|
|
185
194
|
onLongPress={handleMessageLongPress}
|
|
@@ -195,7 +204,7 @@ const ChatDetail = memo(
|
|
|
195
204
|
<MediaViewerModal ref={mediaViewerRef} />
|
|
196
205
|
<MessageToolboxOverlay
|
|
197
206
|
ref={messageToolboxRef}
|
|
198
|
-
currentUserId={
|
|
207
|
+
currentUserId={currentUser?.userID}
|
|
199
208
|
customMessageItemFactory={customMessageItemFactory}
|
|
200
209
|
onPinMessage={pinMessage}
|
|
201
210
|
onUnpinMessage={unpinMessage}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memo, useCallback, useMemo, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { StyleSheet, type View } from 'react-native';
|
|
2
|
+
import { DeviceEventEmitter, StyleSheet, type View } from 'react-native';
|
|
3
3
|
import Animated, {
|
|
4
4
|
useSharedValue,
|
|
5
5
|
useAnimatedStyle,
|
|
@@ -18,24 +18,30 @@ import { InputButtonFlow } from '../../components/flows/inputButtons';
|
|
|
18
18
|
import { useResetFlow, useRunFlow } from '../../hooks/flows/useFlow';
|
|
19
19
|
import type { IRunConversationFlowPayload } from '../../types/flows';
|
|
20
20
|
import { useSendMessage } from '../../hooks/message/useSendMessage';
|
|
21
|
+
import { ChatEvent, type ScrollToMessagePayload } from '../../types/events';
|
|
22
|
+
import { useMessageStore } from '../../store/message';
|
|
23
|
+
import { useChatContext } from '../../context';
|
|
21
24
|
import { messageStyles } from './legend/messageTypes';
|
|
22
25
|
import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
26
|
+
import { useUserStore } from '../../store';
|
|
27
|
+
import { ChatMessageAPI } from '../../services';
|
|
23
28
|
|
|
24
29
|
export const ChatListLegend = memo(
|
|
25
30
|
({
|
|
26
31
|
messages = [],
|
|
27
|
-
|
|
32
|
+
conversationId,
|
|
28
33
|
customMessageItemFactory,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
onLoadMoreOld,
|
|
35
|
+
moreOldLoading,
|
|
36
|
+
hasMoreOld,
|
|
37
|
+
onLoadMoreNew,
|
|
38
|
+
moreNewLoading,
|
|
39
|
+
hasMoreNew,
|
|
35
40
|
onMediaPress,
|
|
36
41
|
onPressUrl,
|
|
37
42
|
onLongPress,
|
|
38
43
|
}: ChatListProps) => {
|
|
44
|
+
const currentUserId = useUserStore((s) => s.user?.userID);
|
|
39
45
|
// Precompute all message data in single pass (O(n))
|
|
40
46
|
const messageDataMap = useMemo(
|
|
41
47
|
() => precomputeLegendListData(messages),
|
|
@@ -55,9 +61,6 @@ export const ChatListLegend = memo(
|
|
|
55
61
|
const listRef = useRef<LegendListRef>(null);
|
|
56
62
|
const buttonOpacity = useSharedValue(0);
|
|
57
63
|
const isAtBottomRef = useRef(true);
|
|
58
|
-
const lastMessageCountRef = useRef(messages.length);
|
|
59
|
-
const lastFirstMessageIdRef = useRef<string | null>(null);
|
|
60
|
-
const lastLastMessageIdRef = useRef<string | null>(null);
|
|
61
64
|
const scrollDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
62
65
|
const itemHeightsRef = useRef<Map<string, number>>(new Map());
|
|
63
66
|
const avgItemHeightRef = useRef(120);
|
|
@@ -65,6 +68,9 @@ export const ChatListLegend = memo(
|
|
|
65
68
|
const rowRefsMap = useRef<Map<string, View | null>>(new Map());
|
|
66
69
|
const [showBadge, setShowBadge] = useState(false);
|
|
67
70
|
|
|
71
|
+
const { applicationType } = useChatContext();
|
|
72
|
+
const searchMessages = useMessageStore((s) => s.searchMessages);
|
|
73
|
+
|
|
68
74
|
const handleItemLayout = useCallback(
|
|
69
75
|
(messageId: string, height: number) => {
|
|
70
76
|
itemHeightsRef.current.set(messageId, height);
|
|
@@ -214,14 +220,14 @@ export const ChatListLegend = memo(
|
|
|
214
220
|
() => ({
|
|
215
221
|
animated: false,
|
|
216
222
|
on: {
|
|
217
|
-
dataChange: !
|
|
218
|
-
layout: !
|
|
223
|
+
dataChange: !moreOldLoading,
|
|
224
|
+
layout: !moreOldLoading,
|
|
219
225
|
// Must be disabled while prepending older history; otherwise re-measuring
|
|
220
226
|
// the freshly prepended items snaps the list back to the bottom.
|
|
221
|
-
itemLayout: !
|
|
227
|
+
itemLayout: !moreOldLoading,
|
|
222
228
|
},
|
|
223
229
|
}),
|
|
224
|
-
[
|
|
230
|
+
[moreOldLoading]
|
|
225
231
|
);
|
|
226
232
|
|
|
227
233
|
// Stable config so the native maintainVisibleContentPosition prop isn't toggled
|
|
@@ -232,21 +238,21 @@ export const ChatListLegend = memo(
|
|
|
232
238
|
const handleStartReached = useCallback(() => {
|
|
233
239
|
if (__DEV__) {
|
|
234
240
|
console.log('[LegendList] onStartReached triggered', {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
willLoad:
|
|
241
|
+
hasMoreOld,
|
|
242
|
+
moreOldLoading,
|
|
243
|
+
willLoad: hasMoreOld && !moreOldLoading,
|
|
238
244
|
});
|
|
239
245
|
}
|
|
240
|
-
if (
|
|
241
|
-
|
|
246
|
+
if (hasMoreOld && !moreOldLoading) {
|
|
247
|
+
onLoadMoreOld?.(true);
|
|
242
248
|
}
|
|
243
|
-
}, [
|
|
249
|
+
}, [hasMoreOld, moreOldLoading, onLoadMoreOld]);
|
|
244
250
|
|
|
245
251
|
const handleEndReached = useCallback(() => {
|
|
246
|
-
if (
|
|
247
|
-
|
|
252
|
+
if (hasMoreNew && !moreNewLoading) {
|
|
253
|
+
onLoadMoreNew?.(true);
|
|
248
254
|
}
|
|
249
|
-
}, [
|
|
255
|
+
}, [hasMoreNew, moreNewLoading, onLoadMoreNew]);
|
|
250
256
|
|
|
251
257
|
const handleScroll = useCallback(
|
|
252
258
|
(e: any) => {
|
|
@@ -267,82 +273,30 @@ export const ChatListLegend = memo(
|
|
|
267
273
|
duration: 300,
|
|
268
274
|
});
|
|
269
275
|
}
|
|
276
|
+
if (isAtBottom) {
|
|
277
|
+
ChatMessageAPI.markConversationAsRead(conversationId || '');
|
|
278
|
+
}
|
|
270
279
|
}, 50);
|
|
271
280
|
},
|
|
272
|
-
[buttonOpacity]
|
|
281
|
+
[buttonOpacity, conversationId]
|
|
273
282
|
);
|
|
274
283
|
|
|
275
284
|
const handleScrollToBottom = useCallback(() => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
if (hasMoreNew) {
|
|
286
|
+
onLoadMoreOld?.(false);
|
|
287
|
+
} else {
|
|
288
|
+
listRef.current?.scrollToEnd({ animated: true });
|
|
289
|
+
buttonOpacity.value = withSpring(0, { duration: 300 });
|
|
290
|
+
isAtBottomRef.current = true;
|
|
291
|
+
setShowBadge(false);
|
|
292
|
+
}
|
|
293
|
+
}, [buttonOpacity, onLoadMoreOld, hasMoreNew]);
|
|
281
294
|
|
|
282
295
|
const animatedButtonStyle = useAnimatedStyle(() => ({
|
|
283
296
|
opacity: buttonOpacity.value,
|
|
284
297
|
pointerEvents: buttonOpacity.value === 0 ? 'none' : 'auto',
|
|
285
298
|
}));
|
|
286
299
|
|
|
287
|
-
// Auto-scroll to bottom when new messages arrive
|
|
288
|
-
useEffect(() => {
|
|
289
|
-
const currentCount = messages.length;
|
|
290
|
-
const previousCount = lastMessageCountRef.current;
|
|
291
|
-
let timeoutId: NodeJS.Timeout | undefined;
|
|
292
|
-
|
|
293
|
-
if (currentCount > previousCount) {
|
|
294
|
-
// Detect if messages were prepended (added to top) or appended (added to bottom)
|
|
295
|
-
const firstMsgId = messages[0]?.clientMsgID || messages[0]?.serverMsgID;
|
|
296
|
-
const lastMsgId =
|
|
297
|
-
messages[currentCount - 1]?.clientMsgID ||
|
|
298
|
-
messages[currentCount - 1]?.serverMsgID;
|
|
299
|
-
const prevFirstMsgId = lastFirstMessageIdRef.current;
|
|
300
|
-
const prevLastMsgId = lastLastMessageIdRef.current;
|
|
301
|
-
|
|
302
|
-
// Messages were prepended if last message ID stayed same but first changed
|
|
303
|
-
const wasPrepended =
|
|
304
|
-
prevLastMsgId === lastMsgId && firstMsgId !== prevFirstMsgId;
|
|
305
|
-
|
|
306
|
-
// Check if new message is from current user
|
|
307
|
-
const newMessage = messages[currentCount - 1];
|
|
308
|
-
const isOwnMessage = newMessage?.sendID === currentUserId;
|
|
309
|
-
|
|
310
|
-
// Auto-scroll conditions:
|
|
311
|
-
// 1. Own message: ALWAYS scroll to bottom (user needs to see what they sent)
|
|
312
|
-
// 2. Received message: Only scroll if already at bottom (don't interrupt reading history)
|
|
313
|
-
const shouldScroll =
|
|
314
|
-
!wasPrepended &&
|
|
315
|
-
!isLoadingEarlier &&
|
|
316
|
-
(isOwnMessage || isAtBottomRef.current);
|
|
317
|
-
|
|
318
|
-
if (shouldScroll) {
|
|
319
|
-
timeoutId = setTimeout(() => {
|
|
320
|
-
listRef.current?.scrollToEnd({ animated: true });
|
|
321
|
-
buttonOpacity.value = withSpring(0, { duration: 300 });
|
|
322
|
-
setShowBadge(false);
|
|
323
|
-
}, 100);
|
|
324
|
-
} else if (!wasPrepended && !isLoadingEarlier && !isOwnMessage) {
|
|
325
|
-
// Show badge for received messages while scrolled up
|
|
326
|
-
setShowBadge(true);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
lastMessageCountRef.current = currentCount;
|
|
331
|
-
if (messages.length > 0) {
|
|
332
|
-
// Use same ID logic as precomputeLegendListData (with fallback)
|
|
333
|
-
lastFirstMessageIdRef.current =
|
|
334
|
-
messages[0]?.clientMsgID || messages[0]?.serverMsgID || null;
|
|
335
|
-
lastLastMessageIdRef.current =
|
|
336
|
-
messages[messages.length - 1]?.clientMsgID ||
|
|
337
|
-
messages[messages.length - 1]?.serverMsgID ||
|
|
338
|
-
null;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return () => {
|
|
342
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
343
|
-
};
|
|
344
|
-
}, [messages, isLoadingEarlier, buttonOpacity, currentUserId]);
|
|
345
|
-
|
|
346
300
|
// Cleanup debounce timeouts on unmount
|
|
347
301
|
useEffect(() => {
|
|
348
302
|
return () => {
|
|
@@ -355,6 +309,58 @@ export const ChatListLegend = memo(
|
|
|
355
309
|
};
|
|
356
310
|
}, []);
|
|
357
311
|
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
const bottomSubscription = DeviceEventEmitter.addListener(
|
|
314
|
+
ChatEvent.ScrollToBottom,
|
|
315
|
+
handleScrollToBottom
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const subscription = DeviceEventEmitter.addListener(
|
|
319
|
+
ChatEvent.ScrollToMessage,
|
|
320
|
+
({ clientMessageId }: ScrollToMessagePayload) => {
|
|
321
|
+
const index = messages.findIndex(
|
|
322
|
+
(m) => m.clientMsgID === clientMessageId
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (index >= 0) {
|
|
326
|
+
try {
|
|
327
|
+
listRef.current?.scrollToIndex({
|
|
328
|
+
index,
|
|
329
|
+
animated: true,
|
|
330
|
+
viewPosition: 0.5,
|
|
331
|
+
});
|
|
332
|
+
} catch {
|
|
333
|
+
// scrollToIndex throws if item not rendered yet — force refetch messages
|
|
334
|
+
searchMessages({
|
|
335
|
+
applicationType,
|
|
336
|
+
conversationId,
|
|
337
|
+
startClientMsgId: clientMessageId,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
searchMessages({
|
|
344
|
+
applicationType,
|
|
345
|
+
conversationId,
|
|
346
|
+
startClientMsgId: clientMessageId,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return () => {
|
|
352
|
+
bottomSubscription.remove();
|
|
353
|
+
subscription.remove();
|
|
354
|
+
};
|
|
355
|
+
}, [
|
|
356
|
+
messages,
|
|
357
|
+
applicationType,
|
|
358
|
+
searchMessages,
|
|
359
|
+
conversationId,
|
|
360
|
+
buttonOpacity,
|
|
361
|
+
handleScrollToBottom,
|
|
362
|
+
]);
|
|
363
|
+
|
|
358
364
|
// if (isLoading) {
|
|
359
365
|
// return (
|
|
360
366
|
// <KContainer.View flex center background={KColors.white}>
|
|
@@ -384,7 +390,7 @@ export const ChatListLegend = memo(
|
|
|
384
390
|
contentContainerStyle={styles.content}
|
|
385
391
|
scrollIndicatorInsets={{ right: 1 }}
|
|
386
392
|
ListHeaderComponent={
|
|
387
|
-
<LegendChatLoadEarlier isLoading={
|
|
393
|
+
<LegendChatLoadEarlier isLoading={moreOldLoading} />
|
|
388
394
|
}
|
|
389
395
|
onStartReached={handleStartReached}
|
|
390
396
|
onStartReachedThreshold={0.5}
|
|
@@ -29,6 +29,7 @@ interface PinnedMessagesSheetContentProps {
|
|
|
29
29
|
currentUserId?: string;
|
|
30
30
|
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
31
31
|
customMessageItemFactory?: (item: MessageItem) => ReactNode;
|
|
32
|
+
onPressMessage?: (message: MessageItem) => void;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
const PinnedMessagesSheetContent = memo(
|
|
@@ -36,6 +37,7 @@ const PinnedMessagesSheetContent = memo(
|
|
|
36
37
|
currentUserId,
|
|
37
38
|
unpinMessage,
|
|
38
39
|
customMessageItemFactory,
|
|
40
|
+
onPressMessage,
|
|
39
41
|
}: PinnedMessagesSheetContentProps) => {
|
|
40
42
|
const messages = usePinnedMessageStore((state) => state.messages);
|
|
41
43
|
|
|
@@ -49,13 +51,13 @@ const PinnedMessagesSheetContent = memo(
|
|
|
49
51
|
[unpinMessage]
|
|
50
52
|
);
|
|
51
53
|
|
|
52
|
-
const handlePressMessage = useCallback(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
const handlePressMessage = useCallback(
|
|
55
|
+
(message: MessageItem) => {
|
|
56
|
+
UIUtils.bottomSheet.dismiss();
|
|
57
|
+
onPressMessage?.(message);
|
|
58
|
+
},
|
|
59
|
+
[onPressMessage]
|
|
60
|
+
);
|
|
59
61
|
|
|
60
62
|
return (
|
|
61
63
|
<KContainer.View paddingV="0.5rem">
|
|
@@ -181,12 +183,14 @@ export interface OpenPinnedMessagesSheetParams {
|
|
|
181
183
|
currentUserId?: string;
|
|
182
184
|
unpinMessage: (message: MessageItem) => void | Promise<void>;
|
|
183
185
|
customMessageItemFactory?: (item: MessageItem) => ReactNode;
|
|
186
|
+
onPressMessage?: (message: MessageItem) => void;
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
export const openPinnedMessagesSheet = ({
|
|
187
190
|
currentUserId,
|
|
188
191
|
unpinMessage,
|
|
189
192
|
customMessageItemFactory,
|
|
193
|
+
onPressMessage,
|
|
190
194
|
}: OpenPinnedMessagesSheetParams) => {
|
|
191
195
|
UIUtils.bottomSheet.open({
|
|
192
196
|
header: {
|
|
@@ -201,6 +205,7 @@ export const openPinnedMessagesSheet = ({
|
|
|
201
205
|
currentUserId={currentUserId}
|
|
202
206
|
unpinMessage={unpinMessage}
|
|
203
207
|
customMessageItemFactory={customMessageItemFactory}
|
|
208
|
+
onPressMessage={onPressMessage}
|
|
204
209
|
/>
|
|
205
210
|
),
|
|
206
211
|
},
|