@droppii-org/chat-mobile 0.2.42 → 0.2.44

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 (79) hide show
  1. package/lib/module/components/messages/quotedMessage/index.js +6 -4
  2. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  3. package/lib/module/context/ChatContext.js +11 -0
  4. package/lib/module/context/ChatContext.js.map +1 -1
  5. package/lib/module/core/useChatListener.js +12 -3
  6. package/lib/module/core/useChatListener.js.map +1 -1
  7. package/lib/module/hooks/message/useMessages.js +65 -0
  8. package/lib/module/hooks/message/useMessages.js.map +1 -0
  9. package/lib/module/hooks/message/useSendMessage.js +10 -2
  10. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  11. package/lib/module/index.js +0 -1
  12. package/lib/module/index.js.map +1 -1
  13. package/lib/module/screens/chat-detail/ChatDetail.js +35 -30
  14. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  15. package/lib/module/screens/chat-detail/ChatListLegend.js +83 -84
  16. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  17. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +9 -9
  18. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
  19. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +9 -3
  20. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  21. package/lib/module/services/attachmentOrchestrator.js +4 -0
  22. package/lib/module/services/attachmentOrchestrator.js.map +1 -1
  23. package/lib/module/store/message.js +148 -6
  24. package/lib/module/store/message.js.map +1 -1
  25. package/lib/module/types/chat.js.map +1 -1
  26. package/lib/module/types/events.js +8 -0
  27. package/lib/module/types/events.js.map +1 -0
  28. package/lib/module/utils/conversation.js +13 -1
  29. package/lib/module/utils/conversation.js.map +1 -1
  30. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts +0 -1
  31. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  32. package/lib/typescript/src/context/ChatContext.d.ts +1 -1
  33. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  34. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  35. package/lib/typescript/src/hooks/message/useMessages.d.ts +16 -0
  36. package/lib/typescript/src/hooks/message/useMessages.d.ts.map +1 -0
  37. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  38. package/lib/typescript/src/index.d.ts +0 -1
  39. package/lib/typescript/src/index.d.ts.map +1 -1
  40. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  41. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  42. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
  43. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  44. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts +2 -1
  45. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
  46. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +1 -1
  47. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  48. package/lib/typescript/src/screens/chat-detail/types.d.ts +7 -7
  49. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  50. package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
  51. package/lib/typescript/src/store/message.d.ts.map +1 -1
  52. package/lib/typescript/src/types/chat.d.ts +19 -2
  53. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  54. package/lib/typescript/src/types/events.d.ts +8 -0
  55. package/lib/typescript/src/types/events.d.ts.map +1 -0
  56. package/lib/typescript/src/utils/conversation.d.ts +1 -0
  57. package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
  58. package/package.json +2 -2
  59. package/src/components/messages/quotedMessage/index.tsx +8 -8
  60. package/src/context/ChatContext.tsx +11 -0
  61. package/src/core/useChatListener.ts +18 -2
  62. package/src/hooks/message/useMessages.ts +82 -0
  63. package/src/hooks/message/useSendMessage.ts +13 -2
  64. package/src/index.tsx +0 -1
  65. package/src/screens/chat-detail/ChatDetail.tsx +37 -28
  66. package/src/screens/chat-detail/ChatListLegend.tsx +96 -90
  67. package/src/screens/chat-detail/PinnedMessagesSheet.tsx +12 -7
  68. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +11 -3
  69. package/src/screens/chat-detail/types.ts +7 -7
  70. package/src/services/attachmentOrchestrator.ts +5 -0
  71. package/src/store/message.ts +174 -8
  72. package/src/types/chat.ts +21 -2
  73. package/src/types/events.ts +8 -0
  74. package/src/utils/conversation.ts +14 -0
  75. package/lib/module/hooks/useChatMessages.js +0 -155
  76. package/lib/module/hooks/useChatMessages.js.map +0 -1
  77. package/lib/typescript/src/hooks/useChatMessages.d.ts +0 -21
  78. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +0 -1
  79. package/src/hooks/useChatMessages.ts +0 -246
@@ -9,7 +9,7 @@ import {
9
9
  KRadiusValue,
10
10
  KSpacingValue,
11
11
  } from '@droppii/libs';
12
- import { StyleSheet } from 'react-native';
12
+ import { DeviceEventEmitter, StyleSheet } from 'react-native';
13
13
  import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
14
14
  import { MessageType } from '@droppii/openim-rn-client-sdk';
15
15
  import {
@@ -20,14 +20,12 @@ import {
20
20
  import { getFileIcon, getMimeType } from '../../../utils/imageUtils';
21
21
  import { useUserStore } from '../../../store';
22
22
  import { useChatDetailContext } from '../../../context';
23
+ import { ChatEvent } from '../../../types/events';
23
24
 
24
- interface QuotedMessageProps extends BaseLegendMessageProps {
25
- onPressQuotedMessage?: (clientMsgId: string) => void;
26
- }
25
+ interface QuotedMessageProps extends BaseLegendMessageProps {}
27
26
 
28
27
  const QuotedMessage = memo((props: QuotedMessageProps) => {
29
- const { message, isOutgoing, onPressQuotedMessage, createdAtTime } =
30
- props || {};
28
+ const { message, isOutgoing, createdAtTime } = props || {};
31
29
 
32
30
  const { conversation } = useChatDetailContext();
33
31
 
@@ -60,8 +58,10 @@ const QuotedMessage = memo((props: QuotedMessageProps) => {
60
58
  }, [quotedMessage]);
61
59
 
62
60
  const handlePressQuotedMessage = useCallback(() => {
63
- onPressQuotedMessage?.(quotedMessage?.clientMsgID || '');
64
- }, [quotedMessage, onPressQuotedMessage]);
61
+ DeviceEventEmitter.emit(ChatEvent.ScrollToMessage, {
62
+ clientMessageId: quotedMessage?.clientMsgID || '',
63
+ });
64
+ }, [quotedMessage]);
65
65
 
66
66
  return (
67
67
  <>
@@ -55,6 +55,11 @@ const ChatProviderInner = ({
55
55
  if (userInfo) {
56
56
  useUserStore.getState().setUser(userInfo);
57
57
  }
58
+ } else if (loginStatus === LoginStatus.Logged) {
59
+ const userInfo = await OpenIMSDK.getSelfUserInfo();
60
+ if (userInfo) {
61
+ useUserStore.getState().setUser(userInfo);
62
+ }
58
63
  }
59
64
  } catch (error) {
60
65
  console.error('Failed to login chat sdk', error);
@@ -121,5 +126,11 @@ const ChatSdkBridge = () => {
121
126
  };
122
127
 
123
128
  export const ChatProvider = ({ ...props }: ChatProviderProps) => {
129
+ const { enabled = false } = props;
130
+
131
+ if (!enabled) {
132
+ return props.children;
133
+ }
134
+
124
135
  return <ChatProviderInner {...props} />;
125
136
  };
@@ -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
- // OpenIMSDK.off(OpenIMEvent.OnRecvNewMessages, onRecvNewMessages);
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 { generateContentBasedOnMessageType } from '../../utils/conversation';
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 { useConversation, useConversationStore } from '../../store';
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
- try {
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?.peerType, targetBotId, conversation?.ex]);
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
- currentUserId,
102
- onLoadEarlier,
103
- isLoading,
104
- isLoadingEarlier,
105
- hasMoreEarlier,
106
- } = useChatMessages({ conversationId, enabled });
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
- }, [currentUserId, unpinMessage, customMessageItemFactory]);
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={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
- currentUserId={currentUserId}
183
+ conversationId={conversationId}
178
184
  customMessageItemFactory={customMessageItemFactory}
179
- onLoadEarlier={onLoadEarlier}
180
- isLoading={isLoading || isLoadingConversation}
181
- isLoadingEarlier={isLoadingEarlier}
182
- hasMoreEarlier={hasMoreEarlier}
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={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
- currentUserId = '',
32
+ conversationId,
28
33
  customMessageItemFactory,
29
- onLoadEarlier,
30
- isLoadingEarlier,
31
- hasMoreEarlier,
32
- onLoadNewer,
33
- isLoadingNewer,
34
- hasMoreNewer,
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: !isLoadingEarlier,
218
- layout: !isLoadingEarlier,
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: !isLoadingEarlier,
227
+ itemLayout: !moreOldLoading,
222
228
  },
223
229
  }),
224
- [isLoadingEarlier]
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
- hasMoreEarlier,
236
- isLoadingEarlier,
237
- willLoad: hasMoreEarlier && !isLoadingEarlier,
241
+ hasMoreOld,
242
+ moreOldLoading,
243
+ willLoad: hasMoreOld && !moreOldLoading,
238
244
  });
239
245
  }
240
- if (hasMoreEarlier && !isLoadingEarlier) {
241
- onLoadEarlier?.();
246
+ if (hasMoreOld && !moreOldLoading) {
247
+ onLoadMoreOld?.(true);
242
248
  }
243
- }, [hasMoreEarlier, isLoadingEarlier, onLoadEarlier]);
249
+ }, [hasMoreOld, moreOldLoading, onLoadMoreOld]);
244
250
 
245
251
  const handleEndReached = useCallback(() => {
246
- if (hasMoreNewer && !isLoadingNewer) {
247
- onLoadNewer?.();
252
+ if (hasMoreNew && !moreNewLoading) {
253
+ onLoadMoreNew?.(true);
248
254
  }
249
- }, [hasMoreNewer, isLoadingNewer, onLoadNewer]);
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
- listRef.current?.scrollToEnd({ animated: true });
277
- buttonOpacity.value = withSpring(0, { duration: 300 });
278
- isAtBottomRef.current = true;
279
- setShowBadge(false);
280
- }, [buttonOpacity]);
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={isLoadingEarlier} />
393
+ <LegendChatLoadEarlier isLoading={moreOldLoading} />
388
394
  }
389
395
  onStartReached={handleStartReached}
390
396
  onStartReachedThreshold={0.5}