@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
@@ -1,9 +1,39 @@
1
1
  import { memo, useState, useCallback, useMemo } from 'react';
2
- import { StyleSheet, Image, useWindowDimensions } from 'react-native';
2
+ import { StyleSheet, useWindowDimensions } from 'react-native';
3
3
  import { KContainer, KImage } from '@droppii/libs';
4
4
  import ChatImageDimens from '../../../utils/chatImageDimens';
5
5
  import { messageStyles } from '../styles';
6
6
  import type { BaseLegendMessageProps } from '../types';
7
+ const DEFAULT_DIMS = { width: 300, height: 300 };
8
+
9
+ // Module-level cache: survives component unmount/remount (e.g. status updates)
10
+ // Bounded to ~500 entries (~40KB) — evicts oldest when full
11
+ const MAX_CACHE = 500;
12
+ const dimsCache = new Map<string, { width: number; height: number }>();
13
+ const cacheDims = (url: string, dims: { width: number; height: number }) => {
14
+ if (dimsCache.size >= MAX_CACHE) {
15
+ dimsCache.delete(dimsCache.keys().next().value!);
16
+ }
17
+ dimsCache.set(url, dims);
18
+ };
19
+
20
+ const calcDisplayDims = (
21
+ imgWidth: number,
22
+ imgHeight: number,
23
+ windowWidth: number
24
+ ) => {
25
+ const aspectRatio = imgWidth / imgHeight;
26
+ const horizontalBuffer = 16;
27
+ const maxWidth = Math.floor(windowWidth * 0.75 - horizontalBuffer);
28
+ let finalWidth = maxWidth;
29
+ let finalHeight = maxWidth / aspectRatio;
30
+ const maxHeight = 500;
31
+ if (finalHeight > maxHeight) {
32
+ finalHeight = maxHeight;
33
+ finalWidth = finalHeight * aspectRatio;
34
+ }
35
+ return { width: Math.round(finalWidth), height: Math.round(finalHeight) };
36
+ };
7
37
 
8
38
  export const ImageMessage = memo(
9
39
  ({ message, onMediaPress }: BaseLegendMessageProps) => {
@@ -15,60 +45,46 @@ export const ImageMessage = memo(
15
45
  pictureElem?.bigPicture?.url ||
16
46
  pictureElem?.sourcePicture?.url;
17
47
 
18
- // Calculate dimensions respecting aspect ratio and 80% device width
19
- const imageDimensions = useMemo(() => {
20
- if (!rawImageUrl) return { width: 250, height: 250 };
21
-
22
- const responseWidth =
23
- pictureElem?.width ||
24
- pictureElem?.bigPicture?.width ||
25
- pictureElem?.snapshotPicture?.width;
26
- const responseHeight =
27
- pictureElem?.height ||
28
- pictureElem?.bigPicture?.height ||
29
- pictureElem?.snapshotPicture?.height;
48
+ const metaWidth =
49
+ pictureElem?.bigPicture?.width ||
50
+ pictureElem?.snapshotPicture?.width ||
51
+ pictureElem?.width;
52
+ const metaHeight =
53
+ pictureElem?.bigPicture?.height ||
54
+ pictureElem?.snapshotPicture?.height ||
55
+ pictureElem?.height;
56
+
57
+ // Fallback dims from onLoad when metadata is missing (zero extra request)
58
+ const cachedDims = rawImageUrl
59
+ ? (dimsCache.get(rawImageUrl) ?? null)
60
+ : null;
61
+ const [loadedDims, setLoadedDims] = useState<{
62
+ width: number;
63
+ height: number;
64
+ } | null>(cachedDims);
30
65
 
31
- let imgWidth = responseWidth;
32
- let imgHeight = responseHeight;
33
-
34
- // If no dimensions in response, try to resolve from image source
35
- if (!imgWidth || !imgHeight) {
36
- try {
37
- const source = Image.resolveAssetSource({ uri: rawImageUrl });
38
- imgWidth = source?.width || 300;
39
- imgHeight = source?.height || 300;
40
- } catch {
41
- // Fallback to default if resolution fails
42
- imgWidth = 300;
43
- imgHeight = 300;
66
+ const imageDimensions = useMemo(() => {
67
+ if (!rawImageUrl) return DEFAULT_DIMS;
68
+ const srcWidth = metaWidth || loadedDims?.width;
69
+ const srcHeight = metaHeight || loadedDims?.height;
70
+ if (!srcWidth || !srcHeight) return DEFAULT_DIMS;
71
+ return calcDisplayDims(srcWidth, srcHeight, windowWidth);
72
+ }, [metaWidth, metaHeight, loadedDims, rawImageUrl, windowWidth]);
73
+
74
+ const handleLoad = useCallback(
75
+ (e: any) => {
76
+ setIsLoading(false);
77
+ if (!metaWidth || !metaHeight) {
78
+ const { width, height } = e?.nativeEvent ?? {};
79
+ if (width && height) {
80
+ const dims = { width, height };
81
+ if (rawImageUrl) cacheDims(rawImageUrl, dims);
82
+ setLoadedDims(dims);
83
+ }
44
84
  }
45
- }
46
-
47
- const aspectRatio = imgWidth / imgHeight;
48
- // Account for all margins/padding: use 75% of window width with buffer
49
- const horizontalBuffer = 16; // Left and right combined buffer
50
- const maxWidth = Math.floor(windowWidth * 0.75 - horizontalBuffer);
51
-
52
- // Calculate final dimensions
53
- let finalWidth = maxWidth;
54
- let finalHeight = maxWidth / aspectRatio;
55
-
56
- // Cap maximum height to avoid overly tall images
57
- const maxHeight = 500;
58
- if (finalHeight > maxHeight) {
59
- finalHeight = maxHeight;
60
- finalWidth = finalHeight * aspectRatio;
61
- }
62
-
63
- return {
64
- width: Math.round(finalWidth),
65
- height: Math.round(finalHeight),
66
- };
67
- }, [pictureElem, rawImageUrl, windowWidth]);
68
-
69
- const handleLoadEnd = useCallback(() => {
70
- setIsLoading(false);
71
- }, []);
85
+ },
86
+ [metaWidth, metaHeight, rawImageUrl]
87
+ );
72
88
 
73
89
  const handlePress = useCallback(() => {
74
90
  onMediaPress?.([{ url: rawImageUrl }], 0);
@@ -78,12 +94,12 @@ export const ImageMessage = memo(
78
94
  return null;
79
95
  }
80
96
 
81
- // Use dynamic optimization matching calculated dimensions
82
97
  const optimizedUrl = ChatImageDimens.getOptimizedImageUrlWithDimensions(
83
98
  rawImageUrl,
84
99
  imageDimensions.width,
85
100
  imageDimensions.height
86
101
  );
102
+
87
103
  return (
88
104
  <KContainer.View style={[messageStyles.wrapper, styles.wrapper]}>
89
105
  <KContainer.Touchable
@@ -102,9 +118,8 @@ export const ImageMessage = memo(
102
118
  width={imageDimensions.width}
103
119
  height={imageDimensions.height}
104
120
  style={[messageStyles.imageBubble]}
105
- onLoadEnd={handleLoadEnd}
121
+ onLoad={handleLoad}
106
122
  />
107
-
108
123
  {isLoading && <KContainer.View style={styles.skeleton} />}
109
124
  </KContainer.Touchable>
110
125
  </KContainer.View>
@@ -116,7 +131,6 @@ ImageMessage.displayName = 'ImageMessage';
116
131
 
117
132
  const styles = StyleSheet.create({
118
133
  wrapper: {
119
- // paddingRight: 8,
120
134
  overflow: 'hidden',
121
135
  },
122
136
  skeleton: {
@@ -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);
@@ -1,7 +1,7 @@
1
1
  const MB = 1024 * 1024;
2
2
 
3
3
  export const UPLOAD_LIMITS = {
4
- image: 5 * MB,
4
+ image: 15 * MB,
5
5
  video: 200 * MB,
6
6
  file: 50 * MB,
7
7
  } as const;
@@ -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