@droppii-org/chat-mobile 0.2.24 → 0.2.25

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 (133) hide show
  1. package/lib/module/assets/images/btn_resend.png +0 -0
  2. package/lib/module/assets/images/index.js +2 -1
  3. package/lib/module/assets/images/index.js.map +1 -1
  4. package/lib/module/components/AttachmentPreview/FilePreview.js +1 -1
  5. package/lib/module/components/AttachmentPreview/FilePreview.js.map +1 -1
  6. package/lib/module/components/messages/MessageStatusIndicator.js +70 -0
  7. package/lib/module/components/messages/MessageStatusIndicator.js.map +1 -0
  8. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  9. package/lib/module/components/messages/imageMessage/index.js +59 -50
  10. package/lib/module/components/messages/imageMessage/index.js.map +1 -1
  11. package/lib/module/components/messages/linkMessage/index.js +14 -6
  12. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  13. package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
  14. package/lib/module/components/messages/textMessage/index.js +14 -6
  15. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  16. package/lib/module/components/messages/videoMessage/index.js.map +1 -1
  17. package/lib/module/config/uploadLimits.js +1 -1
  18. package/lib/module/config/uploadLimits.js.map +1 -1
  19. package/lib/module/core/useChatListener.js +35 -4
  20. package/lib/module/core/useChatListener.js.map +1 -1
  21. package/lib/module/core/useUserListener.js +17 -7
  22. package/lib/module/core/useUserListener.js.map +1 -1
  23. package/lib/module/hooks/message/useSendMessage.js +62 -15
  24. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  25. package/lib/module/hooks/useChatCompose.js +39 -28
  26. package/lib/module/hooks/useChatCompose.js.map +1 -1
  27. package/lib/module/hooks/useChatMessages.js +39 -8
  28. package/lib/module/hooks/useChatMessages.js.map +1 -1
  29. package/lib/module/hooks/useFileAttachment.js +1 -1
  30. package/lib/module/hooks/useFileAttachment.js.map +1 -1
  31. package/lib/module/hooks/useFileDownload.js +1 -1
  32. package/lib/module/hooks/useFileDownload.js.map +1 -1
  33. package/lib/module/hooks/useImageAttachment.js +12 -3
  34. package/lib/module/hooks/useImageAttachment.js.map +1 -1
  35. package/lib/module/index.js +1 -0
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/screens/chat-detail/ChatDetail.js +13 -0
  38. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  39. package/lib/module/screens/chat-detail/ChatListLegend.js +20 -15
  40. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  41. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +18 -13
  42. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  43. package/lib/module/services/attachmentOrchestrator.js +34 -12
  44. package/lib/module/services/attachmentOrchestrator.js.map +1 -1
  45. package/lib/module/services/message.js +13 -5
  46. package/lib/module/services/message.js.map +1 -1
  47. package/lib/module/translation/resources/i18n.js +4 -3
  48. package/lib/module/translation/resources/i18n.js.map +1 -1
  49. package/lib/module/types/chat.js.map +1 -1
  50. package/lib/module/types/global.d.js +2 -0
  51. package/lib/module/types/global.d.js.map +1 -0
  52. package/lib/module/types/index.d.js +3 -0
  53. package/lib/module/types/index.d.js.map +1 -0
  54. package/lib/module/utils/UIUtils.js +37 -0
  55. package/lib/module/utils/UIUtils.js.map +1 -0
  56. package/lib/module/utils/videoThumbnailUpload.js +1 -1
  57. package/lib/module/utils/videoThumbnailUpload.js.map +1 -1
  58. package/lib/typescript/src/assets/images/index.d.ts +1 -0
  59. package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
  60. package/lib/typescript/src/components/messages/MessageStatusIndicator.d.ts +9 -0
  61. package/lib/typescript/src/components/messages/MessageStatusIndicator.d.ts.map +1 -0
  62. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  63. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  64. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +3 -8
  65. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  66. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  67. package/lib/typescript/src/components/messages/textMessage/index.d.ts +2 -8
  68. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  69. package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
  70. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  71. package/lib/typescript/src/core/useUserListener.d.ts.map +1 -1
  72. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +2 -0
  73. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  74. package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
  75. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  76. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  77. package/lib/typescript/src/index.d.ts +1 -0
  78. package/lib/typescript/src/index.d.ts.map +1 -1
  79. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  80. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
  81. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  82. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +3 -5
  83. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  84. package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
  85. package/lib/typescript/src/services/message.d.ts +7 -0
  86. package/lib/typescript/src/services/message.d.ts.map +1 -1
  87. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  88. package/lib/typescript/src/types/chat.d.ts +11 -0
  89. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  90. package/lib/typescript/src/utils/UIUtils.d.ts +4 -0
  91. package/lib/typescript/src/utils/UIUtils.d.ts.map +1 -0
  92. package/package.json +2 -2
  93. package/src/assets/images/btn_resend.png +0 -0
  94. package/src/assets/images/index.ts +1 -0
  95. package/src/components/AttachmentPreview/FilePreview.tsx +1 -1
  96. package/src/components/messages/MessageStatusIndicator.tsx +82 -0
  97. package/src/components/messages/fileMessage/index.tsx +0 -1
  98. package/src/components/messages/imageMessage/index.tsx +71 -57
  99. package/src/components/messages/linkMessage/index.tsx +13 -19
  100. package/src/components/messages/mergedMessage/index.tsx +0 -1
  101. package/src/components/messages/textMessage/index.tsx +13 -18
  102. package/src/components/messages/videoMessage/index.tsx +0 -1
  103. package/src/config/uploadLimits.ts +1 -1
  104. package/src/core/useChatListener.ts +38 -4
  105. package/src/core/useUserListener.ts +17 -7
  106. package/src/hooks/message/useSendMessage.ts +103 -24
  107. package/src/hooks/useChatCompose.ts +42 -33
  108. package/src/hooks/useChatMessages.ts +54 -10
  109. package/src/hooks/useFileAttachment.ts +1 -1
  110. package/src/hooks/useFileDownload.ts +1 -1
  111. package/src/hooks/useImageAttachment.ts +13 -5
  112. package/src/index.tsx +2 -0
  113. package/src/screens/chat-detail/ChatDetail.tsx +12 -0
  114. package/src/screens/chat-detail/ChatListLegend.tsx +16 -13
  115. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +19 -12
  116. package/src/services/attachmentOrchestrator.ts +45 -21
  117. package/src/services/message.ts +21 -4
  118. package/src/translation/resources/i18n.ts +6 -3
  119. package/src/types/chat.ts +6 -0
  120. package/src/types/global.d.ts +299 -0
  121. package/src/types/index.d.ts +1 -0
  122. package/src/utils/UIUtils.ts +31 -0
  123. package/src/utils/videoThumbnailUpload.ts +1 -1
  124. package/lib/module/hooks/useVideoAttachment.js +0 -251
  125. package/lib/module/hooks/useVideoAttachment.js.map +0 -1
  126. package/lib/module/utils/ui.js +0 -17
  127. package/lib/module/utils/ui.js.map +0 -1
  128. package/lib/typescript/src/hooks/useVideoAttachment.d.ts +0 -29
  129. package/lib/typescript/src/hooks/useVideoAttachment.d.ts.map +0 -1
  130. package/lib/typescript/src/utils/ui.d.ts +0 -13
  131. package/lib/typescript/src/utils/ui.d.ts.map +0 -1
  132. package/src/hooks/useVideoAttachment.ts +0 -334
  133. package/src/utils/ui.ts +0 -19
@@ -66,11 +66,17 @@ export function useChatCompose() {
66
66
  (v) => v.uploadedUrl
67
67
  ) as unknown as VideoWithUploadStatus[];
68
68
 
69
- // Generate + upload thumbnails for gallery videos
69
+ // Thumbnails are generated + uploaded at pick time (useImageAttachment).
70
+ // Only fall back to generating here for videos that don't already have one
71
+ // (e.g. thumbnail generation failed or hadn't finished before send).
72
+ const videosMissingThumbnail = uploadedVideos.filter(
73
+ (v) => !v.thumbnailUrl
74
+ );
70
75
  let videoThumbnails: Record<string, { url: string; size: number }> = {};
71
- if (uploadedVideos.length > 0) {
72
- videoThumbnails =
73
- await generateAndUploadVideoThumbnails(uploadedVideos);
76
+ if (videosMissingThumbnail.length > 0) {
77
+ videoThumbnails = await generateAndUploadVideoThumbnails(
78
+ videosMissingThumbnail
79
+ );
74
80
  }
75
81
 
76
82
  // Send images + videos
@@ -98,51 +104,54 @@ export function useChatCompose() {
98
104
  error: vid.error,
99
105
  isUploading: vid.isUploading,
100
106
  uploadedUrl: vid.uploadedUrl,
101
- thumbnailUrl: thumb?.url,
102
- thumbnailSize: thumb?.size,
107
+ thumbnailUrl: vid.thumbnailUrl ?? thumb?.url,
108
+ thumbnailSize: vid.thumbnailSize ?? thumb?.size,
103
109
  };
104
110
  }
105
111
  ),
106
112
  ];
107
113
 
108
- const success = await sendAttachments(attachmentItems, undefined);
109
- if (success) {
110
- // Release all picker temp files to free native memory.
111
- ImagePicker.clean().catch(() => {});
112
- clearAllImages();
113
- }
114
+ // Clear composer immediately message is already in chat list with Sending status.
115
+ // User retries from the bubble, not the composer.
116
+ ImagePicker.clean().catch(() => {});
117
+ clearAllImages();
118
+ // Awaited so files (lower priority per attachment-priority config)
119
+ // only start sending after images/videos are done.
120
+ await sendAttachments(attachmentItems, undefined);
114
121
  }
115
122
 
116
- // Send each file as its own message
123
+ // Send each file as its own message (never merged), in priority order
124
+ // after images/videos, one at a time to preserve pick order.
117
125
  const uploadedFiles = selectedFiles.filter(
118
126
  (f) => f.uploadedUrl && !f.error
119
127
  );
120
- for (const file of uploadedFiles) {
121
- await sendAttachments(
122
- [
123
- {
124
- type: 'file' as const,
125
- file: { path: file.uri, filename: file.name, size: file.size },
126
- uploadProgress: 100,
127
- error: null,
128
- isUploading: false,
129
- uploadedUrl: file.uploadedUrl,
130
- },
131
- ],
132
- undefined
133
- );
128
+ if (uploadedFiles.length > 0) {
129
+ clearAllFiles();
130
+ for (const file of uploadedFiles) {
131
+ await sendAttachments(
132
+ [
133
+ {
134
+ type: 'file' as const,
135
+ file: { path: file.uri, filename: file.name, size: file.size },
136
+ uploadProgress: 100,
137
+ error: null,
138
+ isUploading: false,
139
+ uploadedUrl: file.uploadedUrl,
140
+ },
141
+ ],
142
+ undefined
143
+ );
144
+ }
134
145
  }
135
- if (uploadedFiles.length > 0) clearAllFiles();
136
146
  }
137
147
 
138
148
  // --- Text / link preview ---
139
149
  if (text.trim() || urlMeta) {
140
- await sendMessage({
141
- files: [],
142
- plainText: text.trim(),
143
- urlMetadata: urlMeta,
144
- });
150
+ const plainText = text.trim();
151
+ // Clear composer immediately — message is already in chat list with Sending status.
152
+ // User retries from the bubble, not the composer.
145
153
  setText('');
154
+ await sendMessage({ files: [], plainText, urlMetadata: urlMeta });
146
155
  }
147
156
  }, [
148
157
  imageAttachment,
@@ -11,6 +11,8 @@ import {
11
11
  mergeMessages,
12
12
  } from '../utils/message';
13
13
  import { useMessageStore } from '../store/message';
14
+ import { useUserStore } from '../store/user';
15
+ import { useChatContext } from '../context';
14
16
 
15
17
  type UseChatMessagesOptions = {
16
18
  conversationId: string;
@@ -30,7 +32,11 @@ export function useChatMessages({
30
32
  const pushNewMessage = useMessageStore((s) => s.pushNewMessage);
31
33
  const reset = useMessageStore((s) => s.reset);
32
34
 
33
- const [currentUserId, setCurrentUserId] = useState<string>();
35
+ const { applicationType } = useChatContext();
36
+ const cachedUserId = useUserStore((s) => s.user?.userID);
37
+ const [currentUserId, setCurrentUserId] = useState<string | undefined>(
38
+ cachedUserId
39
+ );
34
40
  const [isLoading, setIsLoading] = useState(false);
35
41
  const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
36
42
  const [isLoadingNewer, setIsLoadingNewer] = useState(false);
@@ -46,27 +52,60 @@ export function useChatMessages({
46
52
  setError(null);
47
53
 
48
54
  try {
49
- const [history, userId] = await Promise.all([
55
+ // Fetch userId and history independently so an offline SDK state
56
+ // doesn't block reading local SQLite history.
57
+ const [historyResult, userIdResult] = await Promise.allSettled([
50
58
  ChatMessageAPI.fetchHistoryMessages(conversationId, {
51
59
  count: pageSize,
60
+ applicationType,
52
61
  }),
53
62
  ChatMessageAPI.getCurrentUserId(),
54
63
  ]);
55
64
 
56
- const initialMessages = history.messageList as DMessageItem[];
57
-
58
- setCurrentUserId(userId);
59
- setMessages(mergeMessages([], initialMessages), !history.isEnd);
65
+ if (userIdResult.status === 'fulfilled') {
66
+ setCurrentUserId(userIdResult.value);
67
+ }
60
68
 
61
- ChatMessageAPI.markConversationAsRead(conversationId).catch(
62
- () => undefined
63
- );
69
+ if (historyResult.status === 'fulfilled') {
70
+ const initialMessages = historyResult.value
71
+ .messageList as DMessageItem[];
72
+ setMessages(
73
+ mergeMessages([], initialMessages),
74
+ !historyResult.value.isEnd
75
+ );
76
+ ChatMessageAPI.markConversationAsRead(conversationId).catch(
77
+ () => undefined
78
+ );
79
+ } else {
80
+ // Fallback: try searchLocalMessages to verify local SQLite is accessible offline
81
+ console.warn(
82
+ '[useChatMessages] getAdvancedHistoryMessageList failed, trying searchLocalMessages fallback:',
83
+ historyResult.reason
84
+ );
85
+ const localResult = await ChatMessageAPI.searchLocalMessages(
86
+ conversationId,
87
+ { count: pageSize }
88
+ );
89
+ const localMessages = (localResult?.searchResultItems ?? []).flatMap(
90
+ (item: any) => item.messageList ?? []
91
+ ) as DMessageItem[];
92
+ console.log(
93
+ '[useChatMessages] searchLocalMessages fallback result:',
94
+ localMessages.length,
95
+ 'messages'
96
+ );
97
+ if (localMessages.length > 0) {
98
+ setMessages(mergeMessages([], localMessages), false);
99
+ } else {
100
+ throw historyResult.reason;
101
+ }
102
+ }
64
103
  } catch (err) {
65
104
  setError(err instanceof Error ? err : new Error(String(err)));
66
105
  } finally {
67
106
  setIsLoading(false);
68
107
  }
69
- }, [conversationId, pageSize, setMessages]);
108
+ }, [applicationType, conversationId, pageSize, setMessages]);
70
109
 
71
110
  const onLoadEarlier = useCallback(async () => {
72
111
  if (isLoadingEarlier || !hasMoreEarlier || !messages.length) return;
@@ -148,6 +187,11 @@ export function useChatMessages({
148
187
 
149
188
  const appendIncomingMessages = useCallback(
150
189
  (incoming: MessageItem[]) => {
190
+ console.log('[Listener] OnRecvNewMessages', {
191
+ total: incoming.length,
192
+ conversationId: conversationIdRef.current,
193
+ clientMsgIDs: incoming.map((m) => m.clientMsgID),
194
+ });
151
195
  const relevant = incoming.filter((message) =>
152
196
  belongsToConversation(message, conversationIdRef.current)
153
197
  );
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useState } from 'react';
2
2
  import { ImageUploadAPI } from '../services/imageUpload';
3
3
  import DeviceUtils from '../utils/device';
4
- import UIUtils from '../utils/ui';
4
+ import { UIUtils } from '../utils/UIUtils';
5
5
  import { trans } from '../translation';
6
6
  import { MAX_FILE_SIZE } from '../services/attachmentHandlers';
7
7
 
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { Platform, Share } from 'react-native';
3
3
  import RNFS from 'react-native-fs';
4
4
  import FileViewer from 'react-native-file-viewer';
5
- import UIUtils from '../utils/ui';
5
+ import { UIUtils } from '../utils/UIUtils';
6
6
  import { trans } from '../translation';
7
7
 
8
8
  export type DownloadStatus = 'idle' | 'downloading' | 'done' | 'error';
@@ -4,7 +4,7 @@ import type {
4
4
  Video as PickedVideo,
5
5
  } from 'react-native-image-crop-picker';
6
6
  import DeviceUtils from '../utils/device';
7
- import UIUtils from '../utils/ui';
7
+ import { UIUtils } from '../utils/UIUtils';
8
8
  import { trans } from '../translation';
9
9
  import { getMimeType, isVideoFromPicker } from '../utils/imageUtils';
10
10
  import { UPLOAD_CONCURRENCY, UPLOAD_LIMITS } from '../config/uploadLimits';
@@ -86,9 +86,14 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
86
86
  } catch (error: unknown) {
87
87
  const msg =
88
88
  error instanceof Error
89
- ? error.message
89
+ ? error?.message
90
90
  : trans('chat:image_upload_failed');
91
91
  updateImageStatus(index, { isUploading: false, error: msg });
92
+ UIUtils.toast.open({
93
+ title: trans('chat:image_upload_failed'),
94
+ message: trans('chat:generic_error'),
95
+ theme: 'danger',
96
+ });
92
97
  }
93
98
  },
94
99
  [updateImageStatus]
@@ -171,11 +176,14 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
171
176
 
172
177
  const oversizedItems = images.filter((img) => !isWithinLimit(img));
173
178
  if (oversizedItems.length > 0) {
179
+ const isVideo = oversizedItems.some(isVideoFromPicker);
180
+ const maxSize = Math.floor(
181
+ (isVideo ? UPLOAD_LIMITS.video : UPLOAD_LIMITS.image) / 1024 / 1024
182
+ );
174
183
  UIUtils.toast.open({
175
184
  title: trans(
176
- oversizedItems.some(isVideoFromPicker)
177
- ? 'chat:video_too_large'
178
- : 'chat:image_too_large'
185
+ isVideo ? 'chat:video_too_large' : 'chat:image_too_large',
186
+ { maxSize: String(maxSize) }
179
187
  ),
180
188
  theme: 'danger',
181
189
  });
package/src/index.tsx CHANGED
@@ -52,3 +52,5 @@ export type {
52
52
  } from './screens/chat-detail';
53
53
 
54
54
  export type { PeerType } from '@droppii/openim-rn-client-sdk';
55
+
56
+ export { registerUIUtils, UIUtils } from './utils/UIUtils';
@@ -23,6 +23,7 @@ import {
23
23
  import type { ChatDetailProps } from './types';
24
24
  import { ChatDetailProvider } from '../../context/ChatDetailContext';
25
25
  import { normalizeUrl } from '../../utils/url';
26
+ // import { ChatMessageAPI } from '../../services/message';
26
27
 
27
28
  const ChatDetail = memo(
28
29
  ({
@@ -73,6 +74,17 @@ const ChatDetail = memo(
73
74
  useConversationStore.getState().setCurrentConversation(conversationId);
74
75
  }, [conversationId]);
75
76
 
77
+ // useEffect(() => {
78
+ // if (!conversationId) return;
79
+ // ChatMessageAPI.searchLocalMessages(conversationId, { count: 20 })
80
+ // .then((result) => {
81
+ // console.log('[TEST searchLocalMessages] success:', result);
82
+ // })
83
+ // .catch((err) => {
84
+ // console.log('[TEST searchLocalMessages] error:', err);
85
+ // });
86
+ // }, [conversationId]);
87
+
76
88
  const resolvedTitle = title ?? getConversationTitle(conversation);
77
89
  const resolvedAvatarUri =
78
90
  avatarUri ?? getConversationAvatarUri(conversation);
@@ -1,5 +1,5 @@
1
1
  import { memo, useCallback, useMemo, useEffect, useRef, useState } from 'react';
2
- import { ActivityIndicator, StyleSheet } from 'react-native';
2
+ import { StyleSheet } from 'react-native';
3
3
  import Animated, {
4
4
  useSharedValue,
5
5
  useAnimatedStyle,
@@ -18,6 +18,7 @@ import { getFlowInputButtonsNode } from '../../utils/flows';
18
18
  import { InputButtonFlow } from '../../components/flows/inputButtons';
19
19
  import { useResetFlow, useRunFlow } from '../../hooks/flows/useFlow';
20
20
  import type { IRunConversationFlowPayload } from '../../types/flows';
21
+ import { useSendMessage } from '../../hooks/message/useSendMessage';
21
22
 
22
23
  export const ChatListLegend = memo(
23
24
  ({
@@ -25,7 +26,6 @@ export const ChatListLegend = memo(
25
26
  currentUserId = '',
26
27
  customMessageItemFactory,
27
28
  onLoadEarlier,
28
- isLoading,
29
29
  isLoadingEarlier,
30
30
  hasMoreEarlier,
31
31
  onLoadNewer,
@@ -41,6 +41,7 @@ export const ChatListLegend = memo(
41
41
  );
42
42
 
43
43
  const { isBotConversation, conversationFlow } = useChatDetailContext();
44
+ const { retryMessage } = useSendMessage();
44
45
 
45
46
  const lastMessageId = useMemo(() => {
46
47
  const last = messages[messages.length - 1];
@@ -143,6 +144,7 @@ export const ChatListLegend = memo(
143
144
  createdAtTime={createdAt}
144
145
  onMediaPress={onMediaPress}
145
146
  onPressUrl={onPressUrl}
147
+ onRetry={retryMessage}
146
148
  customMessageItemFactory={customMessageItemFactory}
147
149
  />
148
150
  {!!flowInputButtonsNode && (
@@ -179,6 +181,7 @@ export const ChatListLegend = memo(
179
181
  onRunFlow,
180
182
  conversationFlow?.flowCurrentStep,
181
183
  handleItemLayout,
184
+ retryMessage,
182
185
  ]
183
186
  );
184
187
 
@@ -327,16 +330,16 @@ export const ChatListLegend = memo(
327
330
  };
328
331
  }, []);
329
332
 
330
- if (isLoading) {
331
- return (
332
- <KContainer.View flex center background={KColors.white}>
333
- <ActivityIndicator
334
- color={KColors.palette.primary.w400}
335
- size="small"
336
- />
337
- </KContainer.View>
338
- );
339
- }
333
+ // if (isLoading) {
334
+ // return (
335
+ // <KContainer.View flex center background={KColors.white}>
336
+ // <ActivityIndicator
337
+ // color={KColors.palette.primary.w400}
338
+ // size="small"
339
+ // />
340
+ // </KContainer.View>
341
+ // );
342
+ // }
340
343
 
341
344
  return (
342
345
  <KContainer.View flex background={KColors.white} style={styles.container}>
@@ -346,7 +349,7 @@ export const ChatListLegend = memo(
346
349
  renderItem={renderItem}
347
350
  estimatedItemSize={avgItemHeightRef.current}
348
351
  keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
349
- recycleItems={false}
352
+ recycleItems={true}
350
353
  extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`}
351
354
  alignItemsAtEnd
352
355
  maintainVisibleContentPosition={maintainVisiblePositionConfig}
@@ -1,6 +1,8 @@
1
1
  import { memo } from 'react';
2
2
  import type { ComponentType, ReactNode } from 'react';
3
+ import { KContainer } from '@droppii/libs';
3
4
  import { MessageType } from '@droppii/openim-rn-client-sdk';
5
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
4
6
  import type { DMessageItem } from '../../../types/chat';
5
7
  import type { MediaItem } from '../../../hooks/useMediaViewer';
6
8
  import { TextMessage } from '../../../components/messages/textMessage';
@@ -9,6 +11,7 @@ import { VideoMessage } from '../../../components/messages/videoMessage';
9
11
  import { FileMessage } from '../../../components/messages/fileMessage';
10
12
  import { LinkMessage } from '../../../components/messages/linkMessage';
11
13
  import { MergedMessage } from '../../../components/messages/mergedMessage';
14
+ import { MessageStatusIndicator } from '../../../components/messages/MessageStatusIndicator';
12
15
  import type { BaseLegendMessageProps } from '../../../components/messages/types';
13
16
 
14
17
  type MessageProps = BaseLegendMessageProps & {
@@ -21,6 +24,7 @@ interface LegendChatMessageProps {
21
24
  createdAtTime: number;
22
25
  onMediaPress?: (items: MediaItem[], index?: number) => void;
23
26
  onPressUrl?: (url: string) => void;
27
+ onRetry?: (message: MessageItem) => void;
24
28
  customMessageItemFactory?: (m: DMessageItem) => ReactNode;
25
29
  }
26
30
 
@@ -46,10 +50,6 @@ const MEDIA_TYPES: number[] = [
46
50
  MERGED_MESSAGE_TYPE,
47
51
  ];
48
52
 
49
- /**
50
- * Dispatcher component - renders the appropriate message component
51
- * based on message type. Extensible for new message types.
52
- */
53
53
  export const LegendChatMessage = memo(
54
54
  ({
55
55
  message,
@@ -57,9 +57,9 @@ export const LegendChatMessage = memo(
57
57
  createdAtTime,
58
58
  onMediaPress,
59
59
  onPressUrl,
60
+ onRetry,
60
61
  customMessageItemFactory,
61
62
  }: LegendChatMessageProps) => {
62
- console.log('customMessageItemFactory', customMessageItemFactory);
63
63
  const customNode = customMessageItemFactory?.(message);
64
64
  if (customNode) {
65
65
  return customNode;
@@ -74,13 +74,20 @@ export const LegendChatMessage = memo(
74
74
  onPressUrl && message?.contentType === MessageType.UrlTextMessage;
75
75
 
76
76
  return (
77
- <MessageComponent
78
- message={message}
79
- isOutgoing={isOutgoing}
80
- createdAtTime={createdAtTime}
81
- onMediaPress={hasMediaPress ? onMediaPress : undefined}
82
- onPressUrl={hasUrlPress ? onPressUrl : undefined}
83
- />
77
+ <KContainer.View row alignItems>
78
+ <MessageStatusIndicator
79
+ message={message}
80
+ isOutgoing={isOutgoing}
81
+ onRetry={onRetry}
82
+ />
83
+ <MessageComponent
84
+ message={message}
85
+ isOutgoing={isOutgoing}
86
+ createdAtTime={createdAtTime}
87
+ onMediaPress={hasMediaPress ? onMediaPress : undefined}
88
+ onPressUrl={hasUrlPress ? onPressUrl : undefined}
89
+ />
90
+ </KContainer.View>
84
91
  );
85
92
  }
86
93
  );
@@ -1,4 +1,8 @@
1
- import OpenIMSDK, { type MessageItem } from '@droppii/openim-rn-client-sdk';
1
+ import OpenIMSDK, {
2
+ MessageStatus,
3
+ type MessageItem,
4
+ } from '@droppii/openim-rn-client-sdk';
5
+
2
6
  import type {
3
7
  Attachment,
4
8
  AttachmentSendOptions,
@@ -30,7 +34,6 @@ export namespace AttachmentOrchestratorService {
30
34
  }
31
35
 
32
36
  // 1. Validate all attachments
33
- console.log('[AttachmentOrchestrator] Validating prepared attachments...');
34
37
  for (const att of attachments) {
35
38
  try {
36
39
  const handler = getAttachmentHandler(att.type);
@@ -267,32 +270,53 @@ export namespace AttachmentOrchestratorService {
267
270
  }
268
271
  );
269
272
 
270
- for (let i = 0; i < allMessages.length; i++) {
271
- const message = allMessages[i] as MessageItem;
272
- console.log(
273
- '[AttachmentOrchestrator] Sending message',
274
- i + 1,
275
- 'of',
276
- allMessages.length
277
- );
273
+ const recvID = currentConversation.userID || '';
274
+ const groupID = currentConversation.groupID || '';
278
275
 
279
- const result = await OpenIMSDK.sendMessageNotOss({
280
- recvID: currentConversation.userID || '',
281
- groupID: currentConversation.groupID || '',
282
- message,
276
+ // Push all messages as "Sending" immediately so the user sees every
277
+ // bubble right away, instead of waiting for each network call in turn.
278
+ for (const m of allMessages) {
279
+ useMessageStore.getState().pushNewMessage({
280
+ ...(m as MessageItem),
281
+ status: MessageStatus.Sending,
283
282
  });
283
+ }
284
284
 
285
- lastMsgId = result?.clientMsgID || result?.serverMsgID || '';
286
- console.log('[AttachmentOrchestrator] Message sent:', {
287
- messageId: lastMsgId,
288
- index: i + 1,
289
- });
285
+ const sendResults = await Promise.allSettled(
286
+ allMessages.map(async (m) => {
287
+ const message = m as MessageItem;
288
+ const result = await OpenIMSDK.sendMessageNotOss({
289
+ recvID,
290
+ groupID,
291
+ message,
292
+ });
290
293
 
291
- if (result) {
292
- useMessageStore.getState().pushNewMessage(result);
294
+ if (result) {
295
+ useMessageStore.getState().deleteOneMessage(message.clientMsgID);
296
+ useMessageStore.getState().pushNewMessage(result);
297
+ }
298
+ return result?.clientMsgID || result?.serverMsgID || '';
299
+ })
300
+ );
301
+
302
+ let firstSendError: unknown;
303
+ for (let i = 0; i < sendResults.length; i++) {
304
+ const settled = sendResults[i]!;
305
+ if (settled.status === 'fulfilled') {
306
+ lastMsgId = settled.value || lastMsgId;
307
+ } else {
308
+ firstSendError = firstSendError ?? settled.reason;
309
+ useMessageStore.getState().updateOneMessage({
310
+ ...(allMessages[i] as MessageItem),
311
+ status: MessageStatus.Failed,
312
+ });
293
313
  }
294
314
  }
295
315
 
316
+ if (!lastMsgId && firstSendError) {
317
+ throw firstSendError;
318
+ }
319
+
296
320
  return lastMsgId;
297
321
  }
298
322
  }
@@ -20,16 +20,15 @@ export namespace ChatMessageAPI {
20
20
  count?: number;
21
21
  startClientMsgID?: string;
22
22
  lastMinSeq?: number;
23
+ applicationType?: string;
23
24
  }
24
25
  ): Promise<HistoryMessageResult> {
25
- return OpenIMSDK.getAdvancedHistoryMessageList({
26
+ return OpenIMSDK.getAdvancedHistoryMessageListApp({
26
27
  conversationID,
27
28
  count: options?.count ?? DEFAULT_PAGE_SIZE,
28
29
  startClientMsgID: options?.startClientMsgID ?? '',
29
30
  viewType: ViewType.History,
30
- // ...(options?.startClientMsgID
31
- // ? { lastMinSeq: options.lastMinSeq ?? 0 }
32
- // : {}),
31
+ applicationType: options?.applicationType ?? '',
33
32
  }) as Promise<HistoryMessageResult>;
34
33
  }
35
34
 
@@ -50,6 +49,24 @@ export namespace ChatMessageAPI {
50
49
  return sentMessage;
51
50
  }
52
51
 
52
+ export async function searchLocalMessages(
53
+ conversationID: string,
54
+ options?: {
55
+ keywordList?: string[];
56
+ messageTypeList?: number[];
57
+ pageIndex?: number;
58
+ count?: number;
59
+ }
60
+ ) {
61
+ return OpenIMSDK.searchLocalMessages({
62
+ conversationID,
63
+ keywordList: options?.keywordList ?? ['a'],
64
+ messageTypeList: options?.messageTypeList,
65
+ pageIndex: options?.pageIndex ?? 1,
66
+ count: options?.count ?? 20,
67
+ });
68
+ }
69
+
53
70
  export async function markConversationAsRead(conversationID: string) {
54
71
  return OpenIMSDK.markConversationMessageAsRead(conversationID);
55
72
  }
@@ -11,10 +11,13 @@ const i18nKey: Record<string, string> = {
11
11
  'chat:file_download_failed': 'Tải tệp thất bại',
12
12
  'chat:file_open_failed': 'Không thể mở tệp này',
13
13
  'chat:image_preview': 'Xem trước ảnh',
14
- 'chat:image_too_large': 'Ảnh quá lớn. Tối đa 5MB',
15
- 'chat:video_too_large': 'Video quá lớn. Tối đa 200MB',
14
+ 'chat:image_too_large':
15
+ 'Hình ảnh tải lên không hợp lệ. Kích thước hình ảnh không được vượt quá {{maxSize}}MB',
16
+ 'chat:video_too_large':
17
+ 'Video tải lên không hợp lệ. Kích thước video không được vượt quá {{maxSize}}MB',
16
18
  'chat:image_sent': 'Đã gửi ảnh',
17
- 'chat:image_upload_failed': 'Lỗi tải ảnh',
19
+ 'chat:image_upload_failed': ' lỗi xảy ra vui lòng thử lại',
20
+ 'chat:generic_error': 'Có lỗi xảy ra vui lòng thử lại',
18
21
  'chat:failed_to_send_message': 'Không thể gửi tin nhắn. Thử lại?',
19
22
  'chat:filename': 'Tên tệp',
20
23
  'chat:file_size': 'Kích thước',
package/src/types/chat.ts CHANGED
@@ -20,6 +20,12 @@ type LogGA = (
20
20
  provider?: `${keyof typeof EventProvider}`
21
21
  ) => void;
22
22
 
23
+ export interface ChatUIAdapter {
24
+ toast?: { open: (params: any) => void };
25
+ alert?: { open: (params: any) => void };
26
+ popup?: { open: (params: any) => void };
27
+ }
28
+
23
29
  export interface ChatContextType {
24
30
  logGA?: LogGA;
25
31
  applicationType: DChatApplicationType;