@droppii-org/chat-mobile 0.2.24 → 0.2.26

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 (178) 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/flows/inputButtons/index.js +3 -6
  7. package/lib/module/components/flows/inputButtons/index.js.map +1 -1
  8. package/lib/module/components/messages/MessageStatusIndicator.js +70 -0
  9. package/lib/module/components/messages/MessageStatusIndicator.js.map +1 -0
  10. package/lib/module/components/messages/fileMessage/index.js +1 -1
  11. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  12. package/lib/module/components/messages/imageMessage/index.js +60 -51
  13. package/lib/module/components/messages/imageMessage/index.js.map +1 -1
  14. package/lib/module/components/messages/linkMessage/index.js +17 -9
  15. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  16. package/lib/module/components/messages/mergedMessage/index.js +5 -8
  17. package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
  18. package/lib/module/components/messages/styles.js +12 -0
  19. package/lib/module/components/messages/styles.js.map +1 -1
  20. package/lib/module/components/messages/textMessage/index.js +20 -9
  21. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  22. package/lib/module/components/messages/videoMessage/index.js +7 -11
  23. package/lib/module/components/messages/videoMessage/index.js.map +1 -1
  24. package/lib/module/config/uploadLimits.js +1 -1
  25. package/lib/module/config/uploadLimits.js.map +1 -1
  26. package/lib/module/core/useChatListener.js +35 -4
  27. package/lib/module/core/useChatListener.js.map +1 -1
  28. package/lib/module/core/useUserListener.js +17 -7
  29. package/lib/module/core/useUserListener.js.map +1 -1
  30. package/lib/module/hooks/message/useSendMessage.js +62 -15
  31. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  32. package/lib/module/hooks/useChatCompose.js +39 -28
  33. package/lib/module/hooks/useChatCompose.js.map +1 -1
  34. package/lib/module/hooks/useChatMessages.js +39 -8
  35. package/lib/module/hooks/useChatMessages.js.map +1 -1
  36. package/lib/module/hooks/useFileAttachment.js +1 -1
  37. package/lib/module/hooks/useFileAttachment.js.map +1 -1
  38. package/lib/module/hooks/useFileDownload.js +1 -1
  39. package/lib/module/hooks/useFileDownload.js.map +1 -1
  40. package/lib/module/hooks/useImageAttachment.js +12 -3
  41. package/lib/module/hooks/useImageAttachment.js.map +1 -1
  42. package/lib/module/index.js +1 -0
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/screens/chat-detail/ChatDetail.js +24 -1
  45. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  46. package/lib/module/screens/chat-detail/ChatListLegend.js +40 -28
  47. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  48. package/lib/module/screens/chat-detail/constants.js +3 -2
  49. package/lib/module/screens/chat-detail/constants.js.map +1 -1
  50. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +48 -15
  51. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  52. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +98 -0
  53. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -0
  54. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +110 -0
  55. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -0
  56. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +64 -0
  57. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -0
  58. package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js +37 -0
  59. package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js.map +1 -0
  60. package/lib/module/screens/chat-detail/messageToolbox/types.js +4 -0
  61. package/lib/module/screens/chat-detail/messageToolbox/types.js.map +1 -0
  62. package/lib/module/services/attachmentOrchestrator.js +34 -12
  63. package/lib/module/services/attachmentOrchestrator.js.map +1 -1
  64. package/lib/module/services/message.js +13 -5
  65. package/lib/module/services/message.js.map +1 -1
  66. package/lib/module/translation/resources/i18n.js +9 -3
  67. package/lib/module/translation/resources/i18n.js.map +1 -1
  68. package/lib/module/types/chat.js.map +1 -1
  69. package/lib/module/types/global.d.js +2 -0
  70. package/lib/module/types/global.d.js.map +1 -0
  71. package/lib/module/types/index.d.js +3 -0
  72. package/lib/module/types/index.d.js.map +1 -0
  73. package/lib/module/utils/UIUtils.js +37 -0
  74. package/lib/module/utils/UIUtils.js.map +1 -0
  75. package/lib/module/utils/ui.js +15 -13
  76. package/lib/module/utils/ui.js.map +1 -1
  77. package/lib/module/utils/videoThumbnailUpload.js +1 -1
  78. package/lib/module/utils/videoThumbnailUpload.js.map +1 -1
  79. package/lib/typescript/src/assets/images/index.d.ts +1 -0
  80. package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
  81. package/lib/typescript/src/components/flows/inputButtons/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/messages/MessageStatusIndicator.d.ts +9 -0
  83. package/lib/typescript/src/components/messages/MessageStatusIndicator.d.ts.map +1 -0
  84. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +3 -8
  87. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  88. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/messages/styles.d.ts +15 -3
  90. package/lib/typescript/src/components/messages/styles.d.ts.map +1 -1
  91. package/lib/typescript/src/components/messages/textMessage/index.d.ts +2 -8
  92. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
  94. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  95. package/lib/typescript/src/core/useUserListener.d.ts.map +1 -1
  96. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +2 -0
  97. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  98. package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
  99. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  100. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  101. package/lib/typescript/src/index.d.ts +1 -0
  102. package/lib/typescript/src/index.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  104. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
  105. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  106. package/lib/typescript/src/screens/chat-detail/constants.d.ts +2 -2
  107. package/lib/typescript/src/screens/chat-detail/constants.d.ts.map +1 -1
  108. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +4 -5
  109. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  110. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts +11 -0
  111. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -0
  112. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +10 -0
  113. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -0
  114. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts +11 -0
  115. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -0
  116. package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts +18 -0
  117. package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts.map +1 -0
  118. package/lib/typescript/src/screens/chat-detail/messageToolbox/types.d.ts +12 -0
  119. package/lib/typescript/src/screens/chat-detail/messageToolbox/types.d.ts.map +1 -0
  120. package/lib/typescript/src/screens/chat-detail/types.d.ts +2 -0
  121. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  122. package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
  123. package/lib/typescript/src/services/message.d.ts +7 -0
  124. package/lib/typescript/src/services/message.d.ts.map +1 -1
  125. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  126. package/lib/typescript/src/types/chat.d.ts +11 -0
  127. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  128. package/lib/typescript/src/utils/UIUtils.d.ts +4 -0
  129. package/lib/typescript/src/utils/UIUtils.d.ts.map +1 -0
  130. package/lib/typescript/src/utils/ui.d.ts +1 -12
  131. package/lib/typescript/src/utils/ui.d.ts.map +1 -1
  132. package/package.json +4 -2
  133. package/src/assets/images/btn_resend.png +0 -0
  134. package/src/assets/images/index.ts +1 -0
  135. package/src/components/AttachmentPreview/FilePreview.tsx +1 -1
  136. package/src/components/flows/inputButtons/index.tsx +3 -5
  137. package/src/components/messages/MessageStatusIndicator.tsx +82 -0
  138. package/src/components/messages/fileMessage/index.tsx +0 -2
  139. package/src/components/messages/imageMessage/index.tsx +72 -58
  140. package/src/components/messages/linkMessage/index.tsx +16 -21
  141. package/src/components/messages/mergedMessage/index.tsx +13 -18
  142. package/src/components/messages/styles.ts +12 -0
  143. package/src/components/messages/textMessage/index.tsx +20 -20
  144. package/src/components/messages/videoMessage/index.tsx +8 -11
  145. package/src/config/uploadLimits.ts +1 -1
  146. package/src/core/useChatListener.ts +38 -4
  147. package/src/core/useUserListener.ts +17 -7
  148. package/src/hooks/message/useSendMessage.ts +103 -24
  149. package/src/hooks/useChatCompose.ts +42 -33
  150. package/src/hooks/useChatMessages.ts +54 -10
  151. package/src/hooks/useFileAttachment.ts +1 -1
  152. package/src/hooks/useFileDownload.ts +1 -1
  153. package/src/hooks/useImageAttachment.ts +13 -5
  154. package/src/index.tsx +2 -0
  155. package/src/screens/chat-detail/ChatDetail.tsx +29 -0
  156. package/src/screens/chat-detail/ChatListLegend.tsx +45 -28
  157. package/src/screens/chat-detail/constants.ts +3 -2
  158. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +60 -15
  159. package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +114 -0
  160. package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +130 -0
  161. package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +99 -0
  162. package/src/screens/chat-detail/messageToolbox/messageToolbox.utils.ts +56 -0
  163. package/src/screens/chat-detail/messageToolbox/types.ts +13 -0
  164. package/src/screens/chat-detail/types.ts +2 -0
  165. package/src/services/attachmentOrchestrator.ts +45 -21
  166. package/src/services/message.ts +21 -4
  167. package/src/translation/resources/i18n.ts +11 -3
  168. package/src/types/chat.ts +6 -0
  169. package/src/types/global.d.ts +299 -0
  170. package/src/types/index.d.ts +1 -0
  171. package/src/utils/UIUtils.ts +31 -0
  172. package/src/utils/ui.ts +20 -16
  173. package/src/utils/videoThumbnailUpload.ts +1 -1
  174. package/lib/module/hooks/useVideoAttachment.js +0 -251
  175. package/lib/module/hooks/useVideoAttachment.js.map +0 -1
  176. package/lib/typescript/src/hooks/useVideoAttachment.d.ts +0 -29
  177. package/lib/typescript/src/hooks/useVideoAttachment.d.ts.map +0 -1
  178. package/src/hooks/useVideoAttachment.ts +0 -334
@@ -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,10 @@ 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';
27
+ import { MessageToolboxOverlay } from './messageToolbox/MessageToolboxOverlay';
28
+ import type { MeasuredRect, MessageToolboxRef } from './messageToolbox/types';
29
+ import type { DMessageItem } from '../../types/chat';
26
30
 
27
31
  const ChatDetail = memo(
28
32
  ({
@@ -44,6 +48,7 @@ const ChatDetail = memo(
44
48
  }: ChatDetailProps) => {
45
49
  const navigation = useNavigation();
46
50
  const mediaViewerRef = useRef<MediaViewerModalHandle>(null);
51
+ const messageToolboxRef = useRef<MessageToolboxRef>(null);
47
52
  const conversation = useConversation(conversationId);
48
53
 
49
54
  const handleBackDefault = useCallback(() => {
@@ -73,6 +78,17 @@ const ChatDetail = memo(
73
78
  useConversationStore.getState().setCurrentConversation(conversationId);
74
79
  }, [conversationId]);
75
80
 
81
+ // useEffect(() => {
82
+ // if (!conversationId) return;
83
+ // ChatMessageAPI.searchLocalMessages(conversationId, { count: 20 })
84
+ // .then((result) => {
85
+ // console.log('[TEST searchLocalMessages] success:', result);
86
+ // })
87
+ // .catch((err) => {
88
+ // console.log('[TEST searchLocalMessages] error:', err);
89
+ // });
90
+ // }, [conversationId]);
91
+
76
92
  const resolvedTitle = title ?? getConversationTitle(conversation);
77
93
  const resolvedAvatarUri =
78
94
  avatarUri ?? getConversationAvatarUri(conversation);
@@ -99,6 +115,13 @@ const ChatDetail = memo(
99
115
  [onPressUrl]
100
116
  );
101
117
 
118
+ const handleMessageLongPress = useCallback(
119
+ (message: DMessageItem, rect: MeasuredRect) => {
120
+ messageToolboxRef.current?.show(message, rect);
121
+ },
122
+ []
123
+ );
124
+
102
125
  return (
103
126
  <ChatDetailProvider>
104
127
  <KContainer.Page flex edges={['bottom']}>
@@ -131,12 +154,18 @@ const ChatDetail = memo(
131
154
  hasMoreEarlier={hasMoreEarlier}
132
155
  onMediaPress={handleMediaPress}
133
156
  onPressUrl={handlePressUrl}
157
+ onLongPress={handleMessageLongPress}
134
158
  />
135
159
 
136
160
  <ChatComposer isEmptyMessage={messages?.length === 0} />
137
161
  </KeyboardAvoidingView>
138
162
 
139
163
  <MediaViewerModal ref={mediaViewerRef} />
164
+ <MessageToolboxOverlay
165
+ ref={messageToolboxRef}
166
+ currentUserId={currentUserId}
167
+ customMessageItemFactory={customMessageItemFactory}
168
+ />
140
169
  </KContainer.Page>
141
170
  </ChatDetailProvider>
142
171
  );
@@ -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, type View } from 'react-native';
3
3
  import Animated, {
4
4
  useSharedValue,
5
5
  useAnimatedStyle,
@@ -18,6 +18,8 @@ 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';
22
+ import { messageStyles } from './legend/messageTypes';
21
23
 
22
24
  export const ChatListLegend = memo(
23
25
  ({
@@ -25,7 +27,6 @@ export const ChatListLegend = memo(
25
27
  currentUserId = '',
26
28
  customMessageItemFactory,
27
29
  onLoadEarlier,
28
- isLoading,
29
30
  isLoadingEarlier,
30
31
  hasMoreEarlier,
31
32
  onLoadNewer,
@@ -33,6 +34,7 @@ export const ChatListLegend = memo(
33
34
  hasMoreNewer,
34
35
  onMediaPress,
35
36
  onPressUrl,
37
+ onLongPress,
36
38
  }: ChatListProps) => {
37
39
  // Precompute all message data in single pass (O(n))
38
40
  const messageDataMap = useMemo(
@@ -41,6 +43,7 @@ export const ChatListLegend = memo(
41
43
  );
42
44
 
43
45
  const { isBotConversation, conversationFlow } = useChatDetailContext();
46
+ const { retryMessage } = useSendMessage();
44
47
 
45
48
  const lastMessageId = useMemo(() => {
46
49
  const last = messages[messages.length - 1];
@@ -59,6 +62,7 @@ export const ChatListLegend = memo(
59
62
  const itemHeightsRef = useRef<Map<string, number>>(new Map());
60
63
  const avgItemHeightRef = useRef(120);
61
64
  const heightDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
65
+ const rowRefsMap = useRef<Map<string, View | null>>(new Map());
62
66
  const [showBadge, setShowBadge] = useState(false);
63
67
 
64
68
  const handleItemLayout = useCallback(
@@ -85,6 +89,16 @@ export const ChatListLegend = memo(
85
89
  []
86
90
  );
87
91
 
92
+ const handleMessageLongPress = useCallback(
93
+ (messageId: string, message: DMessageItem) => {
94
+ const node = rowRefsMap.current.get(messageId);
95
+ node?.measureInWindow((x, y, width, height) => {
96
+ onLongPress?.(message, { x, y, width, height });
97
+ });
98
+ },
99
+ [onLongPress]
100
+ );
101
+
88
102
  const onRunFlow = useCallback(
89
103
  (payload: IRunConversationFlowPayload) => {
90
104
  runFlow(payload);
@@ -129,10 +143,15 @@ export const ChatListLegend = memo(
129
143
  <>
130
144
  {dayStart && <LegendChatDay createdAt={createdAt} />}
131
145
  <KContainer.View
132
- style={[
133
- styles.messageRow,
134
- isOutgoing ? styles.messageRowRight : styles.messageRowLeft,
135
- ]}
146
+ ref={(node: View | null) => {
147
+ if (node) rowRefsMap.current.set(messageId, node);
148
+ else rowRefsMap.current.delete(messageId);
149
+ }}
150
+ style={
151
+ isOutgoing
152
+ ? messageStyles.messageRowRight
153
+ : messageStyles.messageRowLeft
154
+ }
136
155
  onLayout={(e) =>
137
156
  handleItemLayout(messageId, e.nativeEvent.layout.height)
138
157
  }
@@ -143,6 +162,12 @@ export const ChatListLegend = memo(
143
162
  createdAtTime={createdAt}
144
163
  onMediaPress={onMediaPress}
145
164
  onPressUrl={onPressUrl}
165
+ onRetry={retryMessage}
166
+ onLongPress={
167
+ onLongPress
168
+ ? () => handleMessageLongPress(messageId, message)
169
+ : undefined
170
+ }
146
171
  customMessageItemFactory={customMessageItemFactory}
147
172
  />
148
173
  {!!flowInputButtonsNode && (
@@ -174,11 +199,14 @@ export const ChatListLegend = memo(
174
199
  isBotConversation,
175
200
  onMediaPress,
176
201
  onPressUrl,
202
+ onLongPress,
203
+ handleMessageLongPress,
177
204
  customMessageItemFactory,
178
205
  onResetFlow,
179
206
  onRunFlow,
180
207
  conversationFlow?.flowCurrentStep,
181
208
  handleItemLayout,
209
+ retryMessage,
182
210
  ]
183
211
  );
184
212
 
@@ -327,16 +355,16 @@ export const ChatListLegend = memo(
327
355
  };
328
356
  }, []);
329
357
 
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
- }
358
+ // if (isLoading) {
359
+ // return (
360
+ // <KContainer.View flex center background={KColors.white}>
361
+ // <ActivityIndicator
362
+ // color={KColors.palette.primary.w400}
363
+ // size="small"
364
+ // />
365
+ // </KContainer.View>
366
+ // );
367
+ // }
340
368
 
341
369
  return (
342
370
  <KContainer.View flex background={KColors.white} style={styles.container}>
@@ -346,7 +374,7 @@ export const ChatListLegend = memo(
346
374
  renderItem={renderItem}
347
375
  estimatedItemSize={avgItemHeightRef.current}
348
376
  keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
349
- recycleItems={false}
377
+ recycleItems={true}
350
378
  extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`}
351
379
  alignItemsAtEnd
352
380
  maintainVisibleContentPosition={maintainVisiblePositionConfig}
@@ -396,17 +424,6 @@ const styles = StyleSheet.create({
396
424
  container: {
397
425
  position: 'relative',
398
426
  },
399
- messageRow: {
400
- width: '100%',
401
- paddingHorizontal: KSpacingValue['0.75rem'],
402
- marginBottom: KSpacingValue['0.25rem'],
403
- },
404
- messageRowLeft: {
405
- alignItems: 'flex-start',
406
- },
407
- messageRowRight: {
408
- alignItems: 'flex-end',
409
- },
410
427
  content: {
411
428
  paddingVertical: KSpacingValue['0.75rem'],
412
429
  flexGrow: 1,
@@ -1,9 +1,10 @@
1
1
  import { KColors } from '@droppii/libs';
2
2
  import type { DChatQuickAction, DChatAttachmentAction } from './types';
3
+ import { opacityToSolid } from '../../utils/ui';
3
4
 
4
5
  export const CHAT_BUBBLE_COLORS = {
5
- received: 'rgba(131, 137, 157, 0.1)',
6
- sent: 'rgba(0, 81, 255, 0.1)',
6
+ received: opacityToSolid('#83899d1a', KColors.white, 0.1),
7
+ sent: opacityToSolid('#0051ff1a', KColors.white, 0.1),
7
8
  text: KColors.gray.dark,
8
9
  timestamp: KColors.gray.light,
9
10
  dayLabel: KColors.gray.light,
@@ -1,6 +1,10 @@
1
- import { memo } from 'react';
2
- import type { ComponentType, ReactNode } from 'react';
1
+ import { KContainer } from '@droppii/libs';
2
+ import { memo, useMemo } from 'react';
3
+ import type { ComponentType, PropsWithChildren, ReactNode } from 'react';
3
4
  import { MessageType } from '@droppii/openim-rn-client-sdk';
5
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
6
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
7
+ import { runOnJS } from 'react-native-reanimated';
4
8
  import type { DMessageItem } from '../../../types/chat';
5
9
  import type { MediaItem } from '../../../hooks/useMediaViewer';
6
10
  import { TextMessage } from '../../../components/messages/textMessage';
@@ -9,10 +13,13 @@ import { VideoMessage } from '../../../components/messages/videoMessage';
9
13
  import { FileMessage } from '../../../components/messages/fileMessage';
10
14
  import { LinkMessage } from '../../../components/messages/linkMessage';
11
15
  import { MergedMessage } from '../../../components/messages/mergedMessage';
16
+ import { MessageStatusIndicator } from '../../../components/messages/MessageStatusIndicator';
12
17
  import type { BaseLegendMessageProps } from '../../../components/messages/types';
18
+ import { messageStyles } from './messageTypes';
13
19
 
14
20
  type MessageProps = BaseLegendMessageProps & {
15
21
  onPressUrl?: (url: string) => void;
22
+ onLongPress?: () => void;
16
23
  };
17
24
 
18
25
  interface LegendChatMessageProps {
@@ -21,6 +28,8 @@ interface LegendChatMessageProps {
21
28
  createdAtTime: number;
22
29
  onMediaPress?: (items: MediaItem[], index?: number) => void;
23
30
  onPressUrl?: (url: string) => void;
31
+ onRetry?: (message: MessageItem) => void;
32
+ onLongPress?: () => void;
24
33
  customMessageItemFactory?: (m: DMessageItem) => ReactNode;
25
34
  }
26
35
 
@@ -46,10 +55,6 @@ const MEDIA_TYPES: number[] = [
46
55
  MERGED_MESSAGE_TYPE,
47
56
  ];
48
57
 
49
- /**
50
- * Dispatcher component - renders the appropriate message component
51
- * based on message type. Extensible for new message types.
52
- */
53
58
  export const LegendChatMessage = memo(
54
59
  ({
55
60
  message,
@@ -57,12 +62,18 @@ export const LegendChatMessage = memo(
57
62
  createdAtTime,
58
63
  onMediaPress,
59
64
  onPressUrl,
65
+ onRetry,
66
+ onLongPress,
60
67
  customMessageItemFactory,
61
68
  }: LegendChatMessageProps) => {
62
- console.log('customMessageItemFactory', customMessageItemFactory);
63
69
  const customNode = customMessageItemFactory?.(message);
70
+
64
71
  if (customNode) {
65
- return customNode;
72
+ return (
73
+ <LongPressWrapper onLongPress={onLongPress}>
74
+ {customNode}
75
+ </LongPressWrapper>
76
+ );
66
77
  }
67
78
 
68
79
  const MessageComponent: ComponentType<MessageProps> =
@@ -74,15 +85,49 @@ export const LegendChatMessage = memo(
74
85
  onPressUrl && message?.contentType === MessageType.UrlTextMessage;
75
86
 
76
87
  return (
77
- <MessageComponent
78
- message={message}
79
- isOutgoing={isOutgoing}
80
- createdAtTime={createdAtTime}
81
- onMediaPress={hasMediaPress ? onMediaPress : undefined}
82
- onPressUrl={hasUrlPress ? onPressUrl : undefined}
83
- />
88
+ <KContainer.View style={messageStyles.wrapper} row alignItems>
89
+ <MessageStatusIndicator
90
+ message={message}
91
+ isOutgoing={isOutgoing}
92
+ onRetry={onRetry}
93
+ />
94
+ <LongPressWrapper onLongPress={onLongPress}>
95
+ <MessageComponent
96
+ message={message}
97
+ isOutgoing={isOutgoing}
98
+ createdAtTime={createdAtTime}
99
+ onMediaPress={hasMediaPress ? onMediaPress : undefined}
100
+ onPressUrl={hasUrlPress ? onPressUrl : undefined}
101
+ />
102
+ </LongPressWrapper>
103
+ </KContainer.View>
84
104
  );
85
105
  }
86
106
  );
87
107
 
88
108
  LegendChatMessage.displayName = 'LegendChatMessage';
109
+
110
+ const LongPressWrapper = ({
111
+ children,
112
+ onLongPress,
113
+ }: PropsWithChildren<{ onLongPress?: () => void }>) => {
114
+ const longPressGesture = useMemo(
115
+ () =>
116
+ onLongPress
117
+ ? Gesture.LongPress().onStart(() => {
118
+ runOnJS(onLongPress)();
119
+ })
120
+ : null,
121
+ [onLongPress]
122
+ );
123
+
124
+ if (!longPressGesture) {
125
+ return children;
126
+ }
127
+
128
+ return (
129
+ <GestureDetector gesture={longPressGesture}>
130
+ <KContainer.View>{children}</KContainer.View>
131
+ </GestureDetector>
132
+ );
133
+ };