@droppii-org/chat-mobile 0.2.23 → 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 (194) 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 +13 -0
  18. package/lib/module/config/uploadLimits.js.map +1 -0
  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 +153 -0
  26. package/lib/module/hooks/useChatCompose.js.map +1 -0
  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 +81 -152
  34. package/lib/module/hooks/useImageAttachment.js.map +1 -1
  35. package/lib/module/hooks/useSendAttachment.js +2 -2
  36. package/lib/module/hooks/useSendAttachment.js.map +1 -1
  37. package/lib/module/index.js +1 -1
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/screens/chat-detail/ChatComposer.js +50 -185
  40. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  41. package/lib/module/screens/chat-detail/ChatDetail.js +29 -137
  42. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  43. package/lib/module/screens/chat-detail/ChatListLegend.js +20 -15
  44. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  45. package/lib/module/screens/chat-detail/index.js +0 -1
  46. package/lib/module/screens/chat-detail/index.js.map +1 -1
  47. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +18 -13
  48. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  49. package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js +2 -2
  50. package/lib/module/services/attachmentHandlers/fileAttachmentHandler.js.map +1 -1
  51. package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js +3 -3
  52. package/lib/module/services/attachmentHandlers/imageAttachmentHandler.js.map +1 -1
  53. package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js +2 -1
  54. package/lib/module/services/attachmentHandlers/videoAttachmentHandler.js.map +1 -1
  55. package/lib/module/services/attachmentOrchestrator.js +40 -17
  56. package/lib/module/services/attachmentOrchestrator.js.map +1 -1
  57. package/lib/module/services/message.js +13 -5
  58. package/lib/module/services/message.js.map +1 -1
  59. package/lib/module/translation/resources/i18n.js +4 -3
  60. package/lib/module/translation/resources/i18n.js.map +1 -1
  61. package/lib/module/types/chat.js.map +1 -1
  62. package/lib/module/types/global.d.js +2 -0
  63. package/lib/module/types/global.d.js.map +1 -0
  64. package/lib/module/types/index.d.js +3 -0
  65. package/lib/module/types/index.d.js.map +1 -0
  66. package/lib/module/utils/UIUtils.js +37 -0
  67. package/lib/module/utils/UIUtils.js.map +1 -0
  68. package/lib/module/utils/imageUtils.js +3 -3
  69. package/lib/module/utils/imageUtils.js.map +1 -1
  70. package/lib/module/utils/pLimit.js +19 -0
  71. package/lib/module/utils/pLimit.js.map +1 -0
  72. package/lib/module/utils/videoThumbnail.js +8 -3
  73. package/lib/module/utils/videoThumbnail.js.map +1 -1
  74. package/lib/module/utils/videoThumbnailUpload.js +76 -0
  75. package/lib/module/utils/videoThumbnailUpload.js.map +1 -0
  76. package/lib/typescript/src/assets/images/index.d.ts +1 -0
  77. package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
  78. package/lib/typescript/src/components/messages/MessageStatusIndicator.d.ts +9 -0
  79. package/lib/typescript/src/components/messages/MessageStatusIndicator.d.ts.map +1 -0
  80. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  81. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +3 -8
  83. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/messages/textMessage/index.d.ts +2 -8
  86. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  87. package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
  88. package/lib/typescript/src/config/uploadLimits.d.ts +7 -0
  89. package/lib/typescript/src/config/uploadLimits.d.ts.map +1 -0
  90. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  91. package/lib/typescript/src/core/useUserListener.d.ts.map +1 -1
  92. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +2 -0
  93. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  94. package/lib/typescript/src/hooks/useChatCompose.d.ts +27 -0
  95. package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -0
  96. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  97. package/lib/typescript/src/hooks/useImageAttachment.d.ts +2 -2
  98. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  99. package/lib/typescript/src/hooks/useSendAttachment.d.ts +1 -1
  100. package/lib/typescript/src/hooks/useSendAttachment.d.ts.map +1 -1
  101. package/lib/typescript/src/index.d.ts +1 -2
  102. package/lib/typescript/src/index.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +5 -2
  104. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  105. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  106. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  107. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
  108. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  109. package/lib/typescript/src/screens/chat-detail/index.d.ts +0 -2
  110. package/lib/typescript/src/screens/chat-detail/index.d.ts.map +1 -1
  111. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +3 -5
  112. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  113. package/lib/typescript/src/screens/chat-detail/types.d.ts +1 -35
  114. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  115. package/lib/typescript/src/services/attachmentHandlers/fileAttachmentHandler.d.ts.map +1 -1
  116. package/lib/typescript/src/services/attachmentHandlers/imageAttachmentHandler.d.ts.map +1 -1
  117. package/lib/typescript/src/services/attachmentHandlers/videoAttachmentHandler.d.ts.map +1 -1
  118. package/lib/typescript/src/services/attachmentOrchestrator.d.ts +1 -1
  119. package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
  120. package/lib/typescript/src/services/message.d.ts +7 -0
  121. package/lib/typescript/src/services/message.d.ts.map +1 -1
  122. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  123. package/lib/typescript/src/types/chat.d.ts +11 -0
  124. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  125. package/lib/typescript/src/utils/UIUtils.d.ts +4 -0
  126. package/lib/typescript/src/utils/UIUtils.d.ts.map +1 -0
  127. package/lib/typescript/src/utils/imageUtils.d.ts.map +1 -1
  128. package/lib/typescript/src/utils/pLimit.d.ts +6 -0
  129. package/lib/typescript/src/utils/pLimit.d.ts.map +1 -0
  130. package/lib/typescript/src/utils/videoThumbnail.d.ts.map +1 -1
  131. package/lib/typescript/src/utils/videoThumbnailUpload.d.ts +11 -0
  132. package/lib/typescript/src/utils/videoThumbnailUpload.d.ts.map +1 -0
  133. package/package.json +2 -2
  134. package/src/assets/images/btn_resend.png +0 -0
  135. package/src/assets/images/index.ts +1 -0
  136. package/src/components/AttachmentPreview/FilePreview.tsx +1 -1
  137. package/src/components/messages/MessageStatusIndicator.tsx +82 -0
  138. package/src/components/messages/fileMessage/index.tsx +0 -1
  139. package/src/components/messages/imageMessage/index.tsx +71 -57
  140. package/src/components/messages/linkMessage/index.tsx +13 -19
  141. package/src/components/messages/mergedMessage/index.tsx +0 -1
  142. package/src/components/messages/textMessage/index.tsx +13 -18
  143. package/src/components/messages/videoMessage/index.tsx +0 -1
  144. package/src/config/uploadLimits.ts +11 -0
  145. package/src/core/useChatListener.ts +38 -4
  146. package/src/core/useUserListener.ts +17 -7
  147. package/src/hooks/message/useSendMessage.ts +103 -24
  148. package/src/hooks/useChatCompose.ts +195 -0
  149. package/src/hooks/useChatMessages.ts +54 -10
  150. package/src/hooks/useFileAttachment.ts +1 -1
  151. package/src/hooks/useFileDownload.ts +1 -1
  152. package/src/hooks/useImageAttachment.ts +120 -199
  153. package/src/hooks/useSendAttachment.ts +3 -6
  154. package/src/index.tsx +3 -5
  155. package/src/screens/chat-detail/ChatComposer.tsx +88 -240
  156. package/src/screens/chat-detail/ChatDetail.tsx +36 -195
  157. package/src/screens/chat-detail/ChatListLegend.tsx +16 -13
  158. package/src/screens/chat-detail/index.ts +1 -5
  159. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +19 -12
  160. package/src/screens/chat-detail/types.ts +1 -38
  161. package/src/services/attachmentHandlers/fileAttachmentHandler.ts +3 -2
  162. package/src/services/attachmentHandlers/imageAttachmentHandler.ts +6 -3
  163. package/src/services/attachmentHandlers/videoAttachmentHandler.ts +2 -1
  164. package/src/services/attachmentOrchestrator.ts +49 -27
  165. package/src/services/message.ts +21 -4
  166. package/src/translation/resources/i18n.ts +6 -3
  167. package/src/types/chat.ts +6 -0
  168. package/src/types/global.d.ts +299 -0
  169. package/src/types/index.d.ts +1 -0
  170. package/src/utils/UIUtils.ts +31 -0
  171. package/src/utils/imageUtils.ts +3 -4
  172. package/src/utils/pLimit.ts +20 -0
  173. package/src/utils/videoThumbnail.ts +10 -3
  174. package/src/utils/videoThumbnailUpload.ts +103 -0
  175. package/lib/module/hooks/useVideoAttachment.js +0 -251
  176. package/lib/module/hooks/useVideoAttachment.js.map +0 -1
  177. package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js +0 -288
  178. package/lib/module/screens/chat-detail/hooks/useAttachmentSendHandler.js.map +0 -1
  179. package/lib/module/utils/giftedChatMessage.js +0 -90
  180. package/lib/module/utils/giftedChatMessage.js.map +0 -1
  181. package/lib/module/utils/ui.js +0 -17
  182. package/lib/module/utils/ui.js.map +0 -1
  183. package/lib/typescript/src/hooks/useVideoAttachment.d.ts +0 -29
  184. package/lib/typescript/src/hooks/useVideoAttachment.d.ts.map +0 -1
  185. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts +0 -26
  186. package/lib/typescript/src/screens/chat-detail/hooks/useAttachmentSendHandler.d.ts.map +0 -1
  187. package/lib/typescript/src/utils/giftedChatMessage.d.ts +0 -10
  188. package/lib/typescript/src/utils/giftedChatMessage.d.ts.map +0 -1
  189. package/lib/typescript/src/utils/ui.d.ts +0 -13
  190. package/lib/typescript/src/utils/ui.d.ts.map +0 -1
  191. package/src/hooks/useVideoAttachment.ts +0 -334
  192. package/src/screens/chat-detail/hooks/useAttachmentSendHandler.ts +0 -386
  193. package/src/utils/giftedChatMessage.ts +0 -137
  194. package/src/utils/ui.ts +0 -19
@@ -7,18 +7,10 @@ import {
7
7
  KColors,
8
8
  KSpacingValue,
9
9
  } from '@droppii/libs';
10
- import type { DMessageItem } from '../../../types/chat';
11
10
  import type { IUrlMetadata } from '../../../types/common';
12
11
  import { formatMessageTime } from '../types';
12
+ import type { BaseLegendMessageProps } from '../types';
13
13
  import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
14
-
15
- interface BaseLegendMessageProps {
16
- message: DMessageItem;
17
- isOutgoing: boolean;
18
- createdAtTime: number;
19
- onPressUrl?: (url: string) => void;
20
- }
21
-
22
14
  const parseUrlMetadata = (ex?: string): IUrlMetadata | undefined => {
23
15
  if (!ex) return undefined;
24
16
  try {
@@ -80,7 +72,7 @@ export const LinkMessage = memo(
80
72
  isOutgoing,
81
73
  createdAtTime,
82
74
  onPressUrl,
83
- }: BaseLegendMessageProps) => {
75
+ }: BaseLegendMessageProps & { onPressUrl?: (url: string) => void }) => {
84
76
  const content = message?.urlTextElem?.content;
85
77
  const urls = message?.urlTextElem?.urls ?? [];
86
78
  const metadata = parseUrlMetadata(message.ex);
@@ -147,15 +139,17 @@ export const LinkMessage = memo(
147
139
  )}
148
140
  </KContainer.View>
149
141
 
150
- {!isOutgoing && timeLabel ? (
151
- <KLabel.Text
152
- typo="TextXsNormal"
153
- color={CHAT_BUBBLE_COLORS.timestamp}
154
- marginL="0.25rem"
155
- >
156
- {timeLabel}
157
- </KLabel.Text>
158
- ) : null}
142
+ <KContainer.View row alignItems style={{ gap: 4, marginTop: 2 }}>
143
+ {!isOutgoing && timeLabel ? (
144
+ <KLabel.Text
145
+ typo="TextXsNormal"
146
+ color={CHAT_BUBBLE_COLORS.timestamp}
147
+ marginL="0.25rem"
148
+ >
149
+ {timeLabel}
150
+ </KLabel.Text>
151
+ ) : null}
152
+ </KContainer.View>
159
153
  </KContainer.View>
160
154
  );
161
155
  }
@@ -3,7 +3,6 @@ import { KContainer } from '@droppii/libs';
3
3
  import { MergedImageGrid } from '../../MergedImageGrid';
4
4
  import { messageStyles } from '../styles';
5
5
  import type { BaseLegendMessageProps } from '../types';
6
-
7
6
  export const MergedMessage = memo(
8
7
  ({ message, isOutgoing, onMediaPress }: BaseLegendMessageProps) => {
9
8
  const mergeElem = (message as any).mergeElem;
@@ -1,19 +1,12 @@
1
1
  import { memo } from 'react';
2
2
  import { KContainer, KLabel } from '@droppii/libs';
3
3
  import { getMessageText } from '../../../utils/legendListMessage';
4
- import type { DMessageItem } from '../../../types/chat';
5
4
  import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
6
5
  import { messageStyles } from '../styles';
7
6
  import { formatMessageTime } from '../types';
8
-
9
- interface TextMessageProps {
10
- message: DMessageItem;
11
- isOutgoing: boolean;
12
- createdAtTime: number;
13
- }
14
-
7
+ import type { BaseLegendMessageProps } from '../types';
15
8
  export const TextMessage = memo(
16
- ({ message, isOutgoing, createdAtTime }: TextMessageProps) => {
9
+ ({ message, isOutgoing, createdAtTime }: BaseLegendMessageProps) => {
17
10
  const messageText = getMessageText(message);
18
11
  const timeLabel = formatMessageTime(createdAtTime);
19
12
 
@@ -36,15 +29,17 @@ export const TextMessage = memo(
36
29
  </KLabel.Text>
37
30
  </KContainer.View>
38
31
 
39
- {!isOutgoing && timeLabel ? (
40
- <KLabel.Text
41
- typo="TextXsNormal"
42
- color={CHAT_BUBBLE_COLORS.timestamp}
43
- marginL={'0.25rem'}
44
- >
45
- {timeLabel}
46
- </KLabel.Text>
47
- ) : null}
32
+ <KContainer.View row alignItems style={{ gap: 4, marginTop: 2 }}>
33
+ {!isOutgoing && timeLabel ? (
34
+ <KLabel.Text
35
+ typo="TextXsNormal"
36
+ color={CHAT_BUBBLE_COLORS.timestamp}
37
+ marginL={'0.25rem'}
38
+ >
39
+ {timeLabel}
40
+ </KLabel.Text>
41
+ ) : null}
42
+ </KContainer.View>
48
43
  </KContainer.View>
49
44
  );
50
45
  }
@@ -7,7 +7,6 @@ import ChatImageDimens, {
7
7
  import { CHAT_BUBBLE_COLORS } from '../../../screens/chat-detail/constants';
8
8
  import { messageStyles } from '../styles';
9
9
  import type { BaseLegendMessageProps } from '../types';
10
-
11
10
  const formatDuration = (durationMs: number): string => {
12
11
  const totalSeconds = Math.floor(durationMs / 1000);
13
12
  const minutes = Math.floor(totalSeconds / 60);
@@ -0,0 +1,11 @@
1
+ const MB = 1024 * 1024;
2
+
3
+ export const UPLOAD_LIMITS = {
4
+ image: 15 * MB,
5
+ video: 200 * MB,
6
+ file: 50 * MB,
7
+ } as const;
8
+
9
+ // Max concurrent thumbnail generations and file uploads.
10
+ // Keep low to avoid OOM on low-end devices.
11
+ export const UPLOAD_CONCURRENCY = 2;
@@ -2,30 +2,58 @@ import OpenIMSDK, {
2
2
  OpenIMEvent,
3
3
  type ConversationItem,
4
4
  type GroupMemberItem,
5
+ type MessageItem,
5
6
  } from '@droppii/openim-rn-client-sdk';
6
7
  import { useCallback, useEffect } from 'react';
7
8
  import { useConversationStore } from '../store/conversation';
9
+ import { useMessageStore } from '../store/message';
8
10
 
9
11
  export const useChatListener = () => {
12
+ const updateOneMessage = useMessageStore((state) => state.updateOneMessage);
13
+
14
+ const onSendMessageProgress = useCallback(
15
+ ({ progress, message }: { progress: number; message: MessageItem }) => {
16
+ console.log('[Listener] SendMessageProgress', {
17
+ clientMsgID: message.clientMsgID,
18
+ status: message.status,
19
+ progress,
20
+ });
21
+ updateOneMessage(message);
22
+ },
23
+ [updateOneMessage]
24
+ );
25
+
10
26
  const onConversationChanged = useCallback(
11
27
  (conversations: ConversationItem[]) => {
12
- console.log('onConversationChanged', conversations);
28
+ console.log('[Listener] OnConversationChanged', {
29
+ count: conversations.length,
30
+ ids: conversations.map((c) => c.conversationID),
31
+ });
13
32
  useConversationStore.getState().updateConversations(conversations);
14
33
  },
15
34
  []
16
35
  );
17
36
 
18
37
  const onNewConversation = useCallback((conversations: ConversationItem[]) => {
19
- console.log('onNewConversation', conversations);
38
+ console.log('[Listener] OnNewConversation', {
39
+ count: conversations.length,
40
+ ids: conversations.map((c) => c.conversationID),
41
+ });
20
42
  useConversationStore.getState().updateConversations(conversations);
21
43
  }, []);
22
44
 
23
45
  const onGroupMemberAdded = useCallback((member: GroupMemberItem) => {
24
- console.log('onGroupMemberAdded', member);
46
+ console.log('[Listener] OnGroupMemberAdded', {
47
+ userID: member.userID,
48
+ groupID: member.groupID,
49
+ });
25
50
  }, []);
26
51
 
27
52
  const onGroupMemberDeleted = useCallback((member: GroupMemberItem) => {
28
- console.log('onGroupMemberDeleted', member);
53
+ console.log('[Listener] OnGroupMemberDeleted', {
54
+ userID: member.userID,
55
+ groupID: member.groupID,
56
+ });
29
57
  }, []);
30
58
 
31
59
  useEffect(() => {
@@ -37,6 +65,9 @@ export const useChatListener = () => {
37
65
  OpenIMSDK.on(OpenIMEvent.OnGroupMemberAdded, onGroupMemberAdded);
38
66
  OpenIMSDK.on(OpenIMEvent.OnGroupMemberDeleted, onGroupMemberDeleted);
39
67
 
68
+ // Message status
69
+ OpenIMSDK.on(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
70
+
40
71
  return () => {
41
72
  // Conversation
42
73
  OpenIMSDK.off(OpenIMEvent.OnConversationChanged, onConversationChanged);
@@ -45,6 +76,9 @@ export const useChatListener = () => {
45
76
  // Group members
46
77
  OpenIMSDK.off(OpenIMEvent.OnGroupMemberAdded, onGroupMemberAdded);
47
78
  OpenIMSDK.off(OpenIMEvent.OnGroupMemberDeleted, onGroupMemberDeleted);
79
+
80
+ // Message status
81
+ OpenIMSDK.off(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
48
82
  };
49
83
  // eslint-disable-next-line react-hooks/exhaustive-deps
50
84
  }, []);
@@ -1,6 +1,7 @@
1
1
  import OpenIMSDK, {
2
2
  LoginStatus,
3
3
  OpenIMEvent,
4
+ type OpenIMEventBaseErrorType,
4
5
  } from '@droppii/openim-rn-client-sdk';
5
6
  import { useCallback, useEffect } from 'react';
6
7
  import { useUserStore } from '../store';
@@ -28,36 +29,45 @@ export const useUserListener = () => {
28
29
  }, [isLoggedIn, fetchAndUpdateUser]);
29
30
 
30
31
  const onConnecting = useCallback(() => {
32
+ console.log('[Listener] OnConnecting');
31
33
  useUserStore.getState().updateConnectStatus(LoginStatus.Logging);
32
34
  }, []);
33
35
 
34
36
  const onConnectSuccess = useCallback(() => {
37
+ console.log('[Listener] OnConnectSuccess');
35
38
  useUserStore.getState().updateConnectStatus(LoginStatus.Logged);
36
39
  }, []);
37
40
 
38
- const onConnectFailed = useCallback(() => {
39
- useUserStore.getState().updateConnectStatus(LoginStatus.Logout);
41
+ const onConnectFailed = useCallback((data: OpenIMEventBaseErrorType) => {
42
+ console.log('[Listener] OnConnectFailed', data);
40
43
  }, []);
41
44
 
42
- const onSyncServerStart = useCallback(() => {
45
+ const onSyncServerStart = useCallback((isSyncing: boolean) => {
46
+ console.log('[Listener] OnSyncServerStart', { isSyncing });
43
47
  useUserStore.getState().updateSyncStatus(SyncStatus.Loading);
44
48
  }, []);
45
49
 
46
50
  const onSyncServerFinish = useCallback(() => {
51
+ console.log('[Listener] OnSyncServerFinish');
47
52
  useUserStore.getState().updateSyncStatus(SyncStatus.Success);
48
53
  }, []);
49
54
 
50
55
  const onSyncServerFailed = useCallback(() => {
56
+ console.log('[Listener] OnSyncServerFailed');
51
57
  useUserStore.getState().updateSyncStatus(SyncStatus.Failed);
52
58
  }, []);
53
59
 
60
+ const onUserToken = useCallback(() => {
61
+ console.log('[Listener] OnUserToken expired/invalid');
62
+ }, []);
63
+
54
64
  useEffect(() => {
55
65
  // User
56
66
  OpenIMSDK.on(OpenIMEvent.OnConnecting, onConnecting);
57
67
  OpenIMSDK.on(OpenIMEvent.OnConnectSuccess, onConnectSuccess);
58
68
  OpenIMSDK.on(OpenIMEvent.OnConnectFailed, onConnectFailed);
59
- OpenIMSDK.on(OpenIMEvent.OnUserTokenExpired, onConnectFailed);
60
- OpenIMSDK.on(OpenIMEvent.OnUserTokenInvalid, onConnectFailed);
69
+ OpenIMSDK.on(OpenIMEvent.OnUserTokenExpired, onUserToken);
70
+ OpenIMSDK.on(OpenIMEvent.OnUserTokenInvalid, onUserToken);
61
71
 
62
72
  // Sync
63
73
  OpenIMSDK.on(OpenIMEvent.OnSyncServerStart, onSyncServerStart);
@@ -69,8 +79,8 @@ export const useUserListener = () => {
69
79
  OpenIMSDK.off(OpenIMEvent.OnConnecting, onConnecting);
70
80
  OpenIMSDK.off(OpenIMEvent.OnConnectSuccess, onConnectSuccess);
71
81
  OpenIMSDK.off(OpenIMEvent.OnConnectFailed, onConnectFailed);
72
- OpenIMSDK.on(OpenIMEvent.OnUserTokenExpired, onConnectFailed);
73
- OpenIMSDK.on(OpenIMEvent.OnUserTokenInvalid, onConnectFailed);
82
+ OpenIMSDK.off(OpenIMEvent.OnUserTokenExpired, onUserToken);
83
+ OpenIMSDK.off(OpenIMEvent.OnUserTokenInvalid, onUserToken);
74
84
 
75
85
  // Sync
76
86
  OpenIMSDK.off(OpenIMEvent.OnSyncServerStart, onSyncServerStart);
@@ -1,23 +1,64 @@
1
1
  import { useCallback } from 'react';
2
2
  import type { UploadFileItem } from 'react-native-fs';
3
- import { useConversationStore } from '../../store';
4
- import type {
5
- MessageItem,
6
- MessageType,
7
- OfflinePush,
8
- } from '@droppii/openim-rn-client-sdk';
3
+ import { useConversationStore } from '../../store/conversation';
4
+ import type { MessageItem, OfflinePush } from '@droppii/openim-rn-client-sdk';
5
+ import { MessageStatus, MessageType } from '@droppii/openim-rn-client-sdk';
9
6
  import { extractUrls } from '../../utils/url';
10
7
  import OpenIMSDK from '@droppii/openim-rn-client-sdk';
11
8
  import type { IMessageItemEx } from '../../types/chat';
12
9
  import { useChatContext } from '../../context';
13
10
  import { generateContentBasedOnMessageType } from '../../utils/conversation';
14
11
  import { useMessageStore } from '../../store/message';
12
+
15
13
  import type { IUrlMetadata } from '../../types/common';
16
14
  import type { IRunConversationFlowPayload } from '../../types/flows';
17
15
 
16
+ const buildSendParams = (
17
+ message: MessageItem,
18
+ recvID: string,
19
+ groupID: string,
20
+ extendMessageInfo: IMessageItemEx,
21
+ plainText: string
22
+ ) => ({
23
+ groupID,
24
+ recvID,
25
+ message: { ...message, ex: JSON.stringify(extendMessageInfo) || '{}' },
26
+ offlinePushInfo: generateOfflinePushInfo(message.contentType, plainText),
27
+ });
28
+
18
29
  export const useSendMessage = () => {
19
30
  const applicationType = useChatContext().applicationType;
20
31
  const pushNewMessage = useMessageStore((state) => state.pushNewMessage);
32
+ const updateOneMessage = useMessageStore((state) => state.updateOneMessage);
33
+ const deleteOneMessage = useMessageStore((state) => state.deleteOneMessage);
34
+ const dispatchSend = useCallback(
35
+ async (
36
+ message: MessageItem,
37
+ recvID: string,
38
+ groupID: string,
39
+ extendMessageInfo: IMessageItemEx,
40
+ plainText: string
41
+ ) => {
42
+ pushNewMessage({ ...message, status: MessageStatus.Sending });
43
+
44
+ try {
45
+ const sent = await OpenIMSDK.sendMessage(
46
+ buildSendParams(
47
+ message,
48
+ recvID,
49
+ groupID,
50
+ extendMessageInfo,
51
+ plainText
52
+ )
53
+ );
54
+ deleteOneMessage(message.clientMsgID);
55
+ pushNewMessage(sent);
56
+ } catch {
57
+ updateOneMessage({ ...message, status: MessageStatus.Failed });
58
+ }
59
+ },
60
+ [pushNewMessage, updateOneMessage, deleteOneMessage]
61
+ );
21
62
 
22
63
  const sendMessage = useCallback(
23
64
  async ({
@@ -27,7 +68,7 @@ export const useSendMessage = () => {
27
68
  flow,
28
69
  }: {
29
70
  plainText?: string;
30
- files?: UploadFileItem[]; //TODO
71
+ files?: UploadFileItem[];
31
72
  urlMetadata?: IUrlMetadata;
32
73
  flow?: IRunConversationFlowPayload;
33
74
  }) => {
@@ -41,7 +82,6 @@ export const useSendMessage = () => {
41
82
  if (!file) continue;
42
83
  }
43
84
 
44
- // Allow empty text if urlMetadata (preview link) is present
45
85
  const hasText = !!plainText && plainText.trim() !== '';
46
86
  const hasPreview = !!urlMetadata?.url;
47
87
 
@@ -51,7 +91,6 @@ export const useSendMessage = () => {
51
91
  let message: MessageItem | null = null;
52
92
 
53
93
  if (isUrlMessage) {
54
- // Create URL message even with empty text if preview exists
55
94
  message = await createUrlTextMessage(plainText || '', urls);
56
95
  } else if (hasText) {
57
96
  message = await createTextMessage(plainText);
@@ -68,27 +107,67 @@ export const useSendMessage = () => {
68
107
  flow,
69
108
  };
70
109
 
71
- const successMessage = await OpenIMSDK.sendMessage({
72
- groupID: groupID || '',
73
- recvID: recvID || '',
74
- message: {
75
- ...message,
76
- ex: JSON.stringify(extendMessageInfo) || '{}',
77
- },
78
- offlinePushInfo: generateOfflinePushInfo(
79
- message.contentType,
80
- plainText || ''
81
- ),
82
- });
83
-
84
- pushNewMessage(successMessage);
110
+ await dispatchSend(
111
+ message,
112
+ recvID || '',
113
+ groupID || '',
114
+ extendMessageInfo,
115
+ plainText || ''
116
+ );
117
+ }
118
+ },
119
+ [applicationType, dispatchSend]
120
+ );
121
+
122
+ const retryMessage = useCallback(
123
+ async (message: MessageItem) => {
124
+ const { userID: recvID, groupID } =
125
+ useConversationStore.getState().getCurrentConversation() || {};
126
+ if (!recvID && !groupID) return;
127
+
128
+ updateOneMessage({ ...message, status: MessageStatus.Sending });
129
+
130
+ // Attachment messages were sent via sendMessageNotOss (URL already uploaded).
131
+ // Text/URL messages use sendMessage (SDK handles upload).
132
+ const isAttachment = (
133
+ [
134
+ MessageType.PictureMessage,
135
+ MessageType.VideoMessage,
136
+ MessageType.FileMessage,
137
+ MessageType.MergeMessage,
138
+ ] as MessageType[]
139
+ ).includes(message.contentType);
140
+
141
+ const params = {
142
+ recvID: recvID || '',
143
+ groupID: groupID || '',
144
+ message,
145
+ };
146
+
147
+ try {
148
+ const sent = isAttachment
149
+ ? await OpenIMSDK.sendMessageNotOss(params)
150
+ : await OpenIMSDK.sendMessage(
151
+ buildSendParams(
152
+ message,
153
+ recvID || '',
154
+ groupID || '',
155
+ JSON.parse(message.ex || '{}') as IMessageItemEx,
156
+ ''
157
+ )
158
+ );
159
+ deleteOneMessage(message.clientMsgID);
160
+ pushNewMessage(sent);
161
+ } catch {
162
+ updateOneMessage({ ...message, status: MessageStatus.Failed });
85
163
  }
86
164
  },
87
- [applicationType, pushNewMessage]
165
+ [pushNewMessage, updateOneMessage, deleteOneMessage]
88
166
  );
89
167
 
90
168
  return {
91
169
  sendMessage,
170
+ retryMessage,
92
171
  };
93
172
  };
94
173
 
@@ -0,0 +1,195 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import ImagePicker from 'react-native-image-crop-picker';
3
+ import {
4
+ useImageAttachment,
5
+ type ImageWithUploadStatus,
6
+ } from './useImageAttachment';
7
+ import { useFileAttachment } from './useFileAttachment';
8
+ import { useLinkPreview } from './useLinkPreview/useLinkPreview';
9
+ import { useSendAttachment } from './useSendAttachment';
10
+ import { useSendMessage } from './message/useSendMessage';
11
+ import { isVideoFromPicker } from '../utils/imageUtils';
12
+ import {
13
+ generateAndUploadVideoThumbnails,
14
+ type VideoWithUploadStatus,
15
+ } from '../utils/videoThumbnailUpload';
16
+
17
+ export function useChatCompose() {
18
+ const [text, setText] = useState('');
19
+
20
+ const imageAttachment = useImageAttachment();
21
+ const fileAttachment = useFileAttachment();
22
+ const linkPreview = useLinkPreview(text);
23
+ const { send: sendAttachments } = useSendAttachment();
24
+ const { sendMessage } = useSendMessage();
25
+
26
+ const isUploading = useMemo(
27
+ () =>
28
+ imageAttachment.selectedImages.some(
29
+ (img: ImageWithUploadStatus) => img.isUploading
30
+ ) || fileAttachment.selectedFiles.some((f) => f.isUploading || !!f.error),
31
+ [imageAttachment.selectedImages, fileAttachment.selectedFiles]
32
+ );
33
+
34
+ const canSend = useMemo(
35
+ () =>
36
+ (text.trim().length > 0 ||
37
+ imageAttachment.selectedImages.length > 0 ||
38
+ fileAttachment.selectedFiles.length > 0) &&
39
+ !isUploading,
40
+ [
41
+ text,
42
+ imageAttachment.selectedImages,
43
+ fileAttachment.selectedFiles,
44
+ isUploading,
45
+ ]
46
+ );
47
+
48
+ const send = useCallback(async () => {
49
+ const { selectedImages, clearAllImages } = imageAttachment;
50
+ const { selectedFiles, clearAllFiles } = fileAttachment;
51
+ const urlMeta = linkPreview.isVisible ? linkPreview.metadata : undefined;
52
+
53
+ // --- Attachments ---
54
+ if (selectedImages.length > 0 || selectedFiles.length > 0) {
55
+ const actualImages = selectedImages.filter(
56
+ (img: ImageWithUploadStatus) => !isVideoFromPicker(img.image)
57
+ );
58
+ const videos = selectedImages.filter((img: ImageWithUploadStatus) =>
59
+ isVideoFromPicker(img.image)
60
+ );
61
+
62
+ const uploadedImages = actualImages.filter(
63
+ (img: ImageWithUploadStatus) => img.uploadedUrl
64
+ );
65
+ const uploadedVideos = videos.filter(
66
+ (v) => v.uploadedUrl
67
+ ) as unknown as VideoWithUploadStatus[];
68
+
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
+ );
75
+ let videoThumbnails: Record<string, { url: string; size: number }> = {};
76
+ if (videosMissingThumbnail.length > 0) {
77
+ videoThumbnails = await generateAndUploadVideoThumbnails(
78
+ videosMissingThumbnail
79
+ );
80
+ }
81
+
82
+ // Send images + videos
83
+ if (uploadedImages.length > 0 || uploadedVideos.length > 0) {
84
+ const attachmentItems = [
85
+ ...uploadedImages.map((img: ImageWithUploadStatus) => ({
86
+ ...img,
87
+ type: 'image' as const,
88
+ })),
89
+ ...uploadedVideos.map(
90
+ (vid: VideoWithUploadStatus, vidIdx: number) => {
91
+ const videoId = vid.uploadedUrl || `video-${vidIdx}`;
92
+ const thumb = videoThumbnails[videoId];
93
+ return {
94
+ type: 'video' as const,
95
+ video: {
96
+ path: vid.image.path,
97
+ filename: vid.image.filename ?? undefined,
98
+ size: vid.image.size ?? undefined,
99
+ width: vid.image.width ?? undefined,
100
+ height: vid.image.height ?? undefined,
101
+ duration: vid.image.duration ?? undefined,
102
+ },
103
+ uploadProgress: vid.uploadProgress,
104
+ error: vid.error,
105
+ isUploading: vid.isUploading,
106
+ uploadedUrl: vid.uploadedUrl,
107
+ thumbnailUrl: vid.thumbnailUrl ?? thumb?.url,
108
+ thumbnailSize: vid.thumbnailSize ?? thumb?.size,
109
+ };
110
+ }
111
+ ),
112
+ ];
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);
121
+ }
122
+
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.
125
+ const uploadedFiles = selectedFiles.filter(
126
+ (f) => f.uploadedUrl && !f.error
127
+ );
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
+ }
145
+ }
146
+ }
147
+
148
+ // --- Text / link preview ---
149
+ if (text.trim() || urlMeta) {
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.
153
+ setText('');
154
+ await sendMessage({ files: [], plainText, urlMetadata: urlMeta });
155
+ }
156
+ }, [
157
+ imageAttachment,
158
+ fileAttachment,
159
+ linkPreview.isVisible,
160
+ linkPreview.metadata,
161
+ sendAttachments,
162
+ sendMessage,
163
+ text,
164
+ ]);
165
+
166
+ return {
167
+ // Text
168
+ text,
169
+ setText,
170
+ // Attachments
171
+ selectedImages: imageAttachment.selectedImages,
172
+ selectedFiles: fileAttachment.selectedFiles,
173
+ isImagePreviewVisible: imageAttachment.isImagePreviewVisible,
174
+ isFilePreviewVisible: fileAttachment.isFilePreviewVisible,
175
+ addImages: imageAttachment.handleImageAttach,
176
+ addFiles: fileAttachment.handleFileAttach,
177
+ removeImage: imageAttachment.handleImageRemove,
178
+ removeFile: fileAttachment.handleFileRemove,
179
+ // Link preview
180
+ showLinkPreview: linkPreview.isVisible,
181
+ urlMetadata: linkPreview.metadata,
182
+ isMetadataLoading: linkPreview.isLoading,
183
+ dismissPreview: linkPreview.dismiss,
184
+ // Status
185
+ isUploading,
186
+ canSend,
187
+ // Actions
188
+ send,
189
+ clearAll: () => {
190
+ imageAttachment.clearAllImages();
191
+ fileAttachment.clearAllFiles();
192
+ setText('');
193
+ },
194
+ };
195
+ }