@droppii-org/chat-mobile 0.2.37 → 0.2.39

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 (150) hide show
  1. package/lib/module/components/ThreadCard/ThreadCard.js +2 -1
  2. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  3. package/lib/module/components/messages/linkMessage/index.js +1 -10
  4. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  5. package/lib/module/components/messages/quotedMessage/index.js +177 -0
  6. package/lib/module/components/messages/quotedMessage/index.js.map +1 -0
  7. package/lib/module/context/ChatContext.js +26 -22
  8. package/lib/module/context/ChatContext.js.map +1 -1
  9. package/lib/module/context/ChatDetailContext.js +7 -4
  10. package/lib/module/context/ChatDetailContext.js.map +1 -1
  11. package/lib/module/core/useChatListener.js +15 -6
  12. package/lib/module/core/useChatListener.js.map +1 -1
  13. package/lib/module/core/useUserListener.js +12 -21
  14. package/lib/module/core/useUserListener.js.map +1 -1
  15. package/lib/module/hooks/message/useSendMessage.js +26 -4
  16. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  17. package/lib/module/hooks/useChatMessages.js.map +1 -1
  18. package/lib/module/index.js +0 -7
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/screens/chat-detail/ChatComposer.js +2 -1
  21. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  22. package/lib/module/screens/chat-detail/ChatCurrentQuotedMessage.js +144 -0
  23. package/lib/module/screens/chat-detail/ChatCurrentQuotedMessage.js.map +1 -0
  24. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  25. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  26. package/lib/module/screens/chat-detail/PinnedMessageBar.js +4 -2
  27. package/lib/module/screens/chat-detail/PinnedMessageBar.js.map +1 -1
  28. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
  29. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +2 -1
  30. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  31. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +9 -1
  32. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -1
  33. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +11 -1
  34. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
  35. package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js +5 -2
  36. package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js.map +1 -1
  37. package/lib/module/screens/inbox/Inbox.js +1 -5
  38. package/lib/module/screens/inbox/Inbox.js.map +1 -1
  39. package/lib/module/services/apis.js +0 -3
  40. package/lib/module/services/apis.js.map +1 -1
  41. package/lib/module/services/message.js.map +1 -1
  42. package/lib/module/store/message.js.map +1 -1
  43. package/lib/module/store/pinnedMessage.js.map +1 -1
  44. package/lib/module/store/user.js +2 -3
  45. package/lib/module/store/user.js.map +1 -1
  46. package/lib/module/translation/resources/i18n.js +5 -1
  47. package/lib/module/translation/resources/i18n.js.map +1 -1
  48. package/lib/module/types/auth.js +0 -7
  49. package/lib/module/types/auth.js.map +1 -1
  50. package/lib/module/types/chat.js +12 -0
  51. package/lib/module/types/chat.js.map +1 -1
  52. package/lib/module/utils/legendListMessage.js +1 -0
  53. package/lib/module/utils/legendListMessage.js.map +1 -1
  54. package/lib/module/utils/message.js.map +1 -1
  55. package/lib/module/utils/messageUtils.js +47 -6
  56. package/lib/module/utils/messageUtils.js.map +1 -1
  57. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  58. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  59. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts +7 -0
  60. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -0
  61. package/lib/typescript/src/components/messages/types.d.ts +2 -2
  62. package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
  63. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  64. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
  65. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  66. package/lib/typescript/src/core/useUserListener.d.ts.map +1 -1
  67. package/lib/typescript/src/hooks/message/usePinMessage.d.ts +1 -1
  68. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +2 -1
  69. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  70. package/lib/typescript/src/hooks/useChatMessages.d.ts +2 -2
  71. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  72. package/lib/typescript/src/index.d.ts +1 -1
  73. package/lib/typescript/src/index.d.ts.map +1 -1
  74. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  75. package/lib/typescript/src/screens/chat-detail/ChatCurrentQuotedMessage.d.ts +2 -0
  76. package/lib/typescript/src/screens/chat-detail/ChatCurrentQuotedMessage.d.ts.map +1 -0
  77. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  78. package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts +2 -2
  79. package/lib/typescript/src/screens/chat-detail/PinnedMessageBar.d.ts.map +1 -1
  80. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts +1 -2
  81. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
  82. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -3
  83. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  84. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts +6 -4
  85. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -1
  86. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +4 -4
  87. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
  88. package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts +5 -3
  89. package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts.map +1 -1
  90. package/lib/typescript/src/screens/chat-detail/messageToolbox/types.d.ts +2 -2
  91. package/lib/typescript/src/screens/chat-detail/messageToolbox/types.d.ts.map +1 -1
  92. package/lib/typescript/src/screens/chat-detail/types.d.ts +5 -5
  93. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  94. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  95. package/lib/typescript/src/services/apis.d.ts +0 -1
  96. package/lib/typescript/src/services/apis.d.ts.map +1 -1
  97. package/lib/typescript/src/services/message.d.ts +1 -2
  98. package/lib/typescript/src/services/message.d.ts.map +1 -1
  99. package/lib/typescript/src/store/message.d.ts.map +1 -1
  100. package/lib/typescript/src/store/pinnedMessage.d.ts +4 -4
  101. package/lib/typescript/src/store/pinnedMessage.d.ts.map +1 -1
  102. package/lib/typescript/src/store/user.d.ts +4 -4
  103. package/lib/typescript/src/store/user.d.ts.map +1 -1
  104. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  105. package/lib/typescript/src/types/auth.d.ts +0 -5
  106. package/lib/typescript/src/types/auth.d.ts.map +1 -1
  107. package/lib/typescript/src/types/chat.d.ts +16 -36
  108. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  109. package/lib/typescript/src/utils/legendListMessage.d.ts +4 -4
  110. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
  111. package/lib/typescript/src/utils/message.d.ts +4 -5
  112. package/lib/typescript/src/utils/message.d.ts.map +1 -1
  113. package/lib/typescript/src/utils/messageUtils.d.ts +16 -7
  114. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  115. package/package.json +2 -2
  116. package/src/components/ThreadCard/ThreadCard.tsx +5 -1
  117. package/src/components/messages/linkMessage/index.tsx +1 -11
  118. package/src/components/messages/quotedMessage/index.tsx +201 -0
  119. package/src/components/messages/types.ts +2 -2
  120. package/src/context/ChatContext.tsx +30 -25
  121. package/src/context/ChatDetailContext.tsx +9 -3
  122. package/src/core/useChatListener.ts +29 -7
  123. package/src/core/useUserListener.ts +9 -25
  124. package/src/hooks/message/useSendMessage.ts +25 -4
  125. package/src/hooks/useChatMessages.ts +4 -6
  126. package/src/index.tsx +5 -6
  127. package/src/screens/chat-detail/ChatComposer.tsx +2 -0
  128. package/src/screens/chat-detail/ChatCurrentQuotedMessage.tsx +153 -0
  129. package/src/screens/chat-detail/ChatDetail.tsx +3 -2
  130. package/src/screens/chat-detail/ChatListLegend.tsx +3 -3
  131. package/src/screens/chat-detail/PinnedMessageBar.tsx +52 -49
  132. package/src/screens/chat-detail/PinnedMessagesSheet.tsx +7 -8
  133. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +4 -4
  134. package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +15 -3
  135. package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +15 -6
  136. package/src/screens/chat-detail/messageToolbox/messageToolbox.utils.ts +21 -12
  137. package/src/screens/chat-detail/messageToolbox/types.ts +2 -2
  138. package/src/screens/chat-detail/types.ts +5 -5
  139. package/src/screens/inbox/Inbox.tsx +1 -5
  140. package/src/services/apis.ts +0 -4
  141. package/src/services/message.ts +1 -2
  142. package/src/store/message.ts +3 -3
  143. package/src/store/pinnedMessage.ts +4 -4
  144. package/src/store/user.ts +5 -5
  145. package/src/translation/resources/i18n.ts +4 -0
  146. package/src/types/auth.ts +0 -6
  147. package/src/types/chat.ts +18 -40
  148. package/src/utils/legendListMessage.ts +4 -5
  149. package/src/utils/message.ts +10 -11
  150. package/src/utils/messageUtils.ts +76 -15
@@ -0,0 +1,153 @@
1
+ import {
2
+ KColors,
3
+ KContainer,
4
+ KImage,
5
+ KLabel,
6
+ KRadiusValue,
7
+ KSpacingValue,
8
+ } from '@droppii/libs';
9
+ import { memo, useMemo } from 'react';
10
+ import { StyleSheet } from 'react-native';
11
+ import { useChatDetailContext } from '../../context';
12
+ import {
13
+ getMessagePreviewParts,
14
+ getReplySenderName,
15
+ parseUrlMetadata,
16
+ } from '../../utils/messageUtils';
17
+ import { trans } from '../../translation';
18
+ import { useUserStore } from '../../store';
19
+ import { MessageType } from '@droppii/openim-rn-client-sdk';
20
+ import { getFileIcon, getMimeType } from '../../utils/imageUtils';
21
+
22
+ export const ChatCurrentQuotedMessage = memo(() => {
23
+ const { quotedMessage, conversation, setQuotedMessage } =
24
+ useChatDetailContext();
25
+
26
+ const messagePreview = getMessagePreviewParts(quotedMessage);
27
+
28
+ const currentUserId = useUserStore((s) => s.user?.userID);
29
+
30
+ const imageUrl = useMemo(() => {
31
+ switch (quotedMessage?.contentType) {
32
+ case MessageType.PictureMessage:
33
+ return quotedMessage?.pictureElem?.snapshotPicture?.url || '';
34
+ case MessageType.VideoMessage:
35
+ return quotedMessage?.videoElem?.snapshotUrl || '';
36
+ case MessageType.UrlTextMessage:
37
+ const metadata = parseUrlMetadata(quotedMessage?.ex);
38
+ return metadata?.image || '';
39
+ default:
40
+ return '';
41
+ }
42
+ }, [quotedMessage]);
43
+
44
+ const iconSource = useMemo(() => {
45
+ if (quotedMessage?.contentType !== MessageType.FileMessage) return '';
46
+ const mimeType = getMimeType(quotedMessage?.fileElem?.fileName || '');
47
+ return getFileIcon(mimeType);
48
+ }, [quotedMessage]);
49
+
50
+ if (!quotedMessage) return null;
51
+
52
+ return (
53
+ <KContainer.View style={styles.container}>
54
+ <KContainer.View style={styles.dot} />
55
+ <KContainer.VisibleView visible={!!imageUrl}>
56
+ <KContainer.View style={styles.imageContainer}>
57
+ <KImage.Base uri={imageUrl} style={styles.imageContainer} />
58
+ <KContainer.VisibleView
59
+ visible={quotedMessage?.contentType === MessageType.VideoMessage}
60
+ >
61
+ <KContainer.View style={styles.playIconContainer}>
62
+ <KContainer.View style={styles.playIcon}>
63
+ <KImage.VectorIcons
64
+ name="play-b"
65
+ size={12}
66
+ color={KColors.white}
67
+ />
68
+ </KContainer.View>
69
+ </KContainer.View>
70
+ </KContainer.VisibleView>
71
+ </KContainer.View>
72
+ </KContainer.VisibleView>
73
+ <KContainer.VisibleView visible={!!iconSource}>
74
+ <KImage.Base
75
+ uri={iconSource}
76
+ style={styles.icon}
77
+ resizeMode="contain"
78
+ />
79
+ </KContainer.VisibleView>
80
+ <KContainer.View style={styles.content}>
81
+ <KLabel.Text typo="TextNmMedium" numberOfLines={1}>
82
+ {trans('chat:reply_to', {
83
+ name: getReplySenderName(
84
+ quotedMessage,
85
+ currentUserId,
86
+ conversation
87
+ ),
88
+ })}
89
+ </KLabel.Text>
90
+ <KLabel.Text
91
+ typo="TextSmNormal"
92
+ color={KColors.gray.light}
93
+ numberOfLines={1}
94
+ {...messagePreview.textProps}
95
+ >
96
+ {messagePreview.text}
97
+ </KLabel.Text>
98
+ </KContainer.View>
99
+ <KContainer.Touchable onPress={() => setQuotedMessage(undefined)}>
100
+ <KImage.VectorIcons name="close-o" size={16} />
101
+ </KContainer.Touchable>
102
+ </KContainer.View>
103
+ );
104
+ });
105
+
106
+ ChatCurrentQuotedMessage.displayName = 'ChatCurrentQuotedMessage';
107
+
108
+ const styles = StyleSheet.create({
109
+ container: {
110
+ paddingVertical: KSpacingValue['0.25rem'],
111
+ paddingHorizontal: KSpacingValue['0.5rem'],
112
+ backgroundColor: KColors.opacity.grayLight[10],
113
+ flexDirection: 'row',
114
+ alignItems: 'center',
115
+ gap: KSpacingValue['0.5rem'],
116
+ justifyContent: 'space-between',
117
+ },
118
+ dot: {
119
+ width: 3,
120
+ height: '100%',
121
+ backgroundColor: KColors.palette.grayCool.w200,
122
+ borderRadius: KRadiusValue.x,
123
+ },
124
+ content: {
125
+ flex: 1,
126
+ paddingHorizontal: KSpacingValue['0.25rem'],
127
+ },
128
+ icon: {
129
+ width: 24,
130
+ height: 24,
131
+ },
132
+ imageContainer: {
133
+ width: 42,
134
+ height: 42,
135
+ borderRadius: KRadiusValue['2x'],
136
+ },
137
+ playIconContainer: {
138
+ position: 'absolute',
139
+ top: 0,
140
+ left: 0,
141
+ right: 0,
142
+ bottom: 0,
143
+ justifyContent: 'center',
144
+ alignItems: 'center',
145
+ },
146
+ playIcon: {
147
+ borderRadius: 8,
148
+ padding: KSpacingValue['0.25rem'],
149
+ backgroundColor: KColors.hexToRgba(KColors.black, 0.6),
150
+ justifyContent: 'center',
151
+ alignItems: 'center',
152
+ },
153
+ });
@@ -22,12 +22,13 @@ import type { ChatDetailProps } from './types';
22
22
  import { normalizeUrl } from '../../utils/url';
23
23
  import { MessageToolboxOverlay } from './messageToolbox/MessageToolboxOverlay';
24
24
  import type { MeasuredRect, MessageToolboxRef } from './messageToolbox/types';
25
- import { DBotStatusValue, type DMessageItem } from '../../types/chat';
25
+ import { DBotStatusValue } from '../../types/chat';
26
26
  import {
27
27
  useGetConversationDetail,
28
28
  useGetStatusConversationBot,
29
29
  } from '../../hooks/conversation/useGetConversationDetail';
30
30
  import { PeerType } from '@droppii/openim-rn-client-sdk';
31
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
31
32
 
32
33
  const ChatDetail = memo(
33
34
  ({
@@ -134,7 +135,7 @@ const ChatDetail = memo(
134
135
  );
135
136
 
136
137
  const handleMessageLongPress = useCallback(
137
- (message: DMessageItem, rect: MeasuredRect) => {
138
+ (message: MessageItem, rect: MeasuredRect) => {
138
139
  messageToolboxRef.current?.show(message, rect);
139
140
  },
140
141
  []
@@ -9,7 +9,6 @@ import { LegendList, type LegendListRef } from '@legendapp/list/react-native';
9
9
  import { KContainer, KColors, KSpacingValue, KLabel } from '@droppii/libs';
10
10
  import { precomputeLegendListData } from '../../utils/legendListMessage';
11
11
  import type { ChatListProps } from './types';
12
- import type { DMessageItem } from '../../types/chat';
13
12
  import { LegendChatMessage } from './legend/LegendChatMessage';
14
13
  import { LegendChatDay } from './legend/LegendChatDay';
15
14
  import { LegendChatLoadEarlier } from './legend/LegendChatLoadEarlier';
@@ -20,6 +19,7 @@ import { useResetFlow, useRunFlow } from '../../hooks/flows/useFlow';
20
19
  import type { IRunConversationFlowPayload } from '../../types/flows';
21
20
  import { useSendMessage } from '../../hooks/message/useSendMessage';
22
21
  import { messageStyles } from './legend/messageTypes';
22
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
23
23
 
24
24
  export const ChatListLegend = memo(
25
25
  ({
@@ -90,7 +90,7 @@ export const ChatListLegend = memo(
90
90
  );
91
91
 
92
92
  const handleMessageLongPress = useCallback(
93
- (messageId: string, message: DMessageItem) => {
93
+ (messageId: string, message: MessageItem) => {
94
94
  const node = rowRefsMap.current.get(messageId);
95
95
  node?.measureInWindow((x, y, width, height) => {
96
96
  onLongPress?.(message, { x, y, width, height });
@@ -115,7 +115,7 @@ export const ChatListLegend = memo(
115
115
  try {
116
116
  if (!item) return null;
117
117
 
118
- const message = item as DMessageItem;
118
+ const message = item as MessageItem;
119
119
  const isOutgoing = message.sendID === currentUserId;
120
120
  const isLastIncoming =
121
121
  !isOutgoing &&
@@ -10,68 +10,66 @@ import {
10
10
  } from '@droppii/libs';
11
11
  import { trans } from '../../translation';
12
12
  import {
13
- getMessagePreview,
13
+ getMessagePreviewParts,
14
14
  getPinnedMessagePreview,
15
15
  } from '../../utils/messageUtils';
16
- import type { DMessageItem } from '../../types/chat';
16
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
17
17
 
18
18
  interface PinnedMessageBarProps {
19
- messages: DMessageItem[];
19
+ messages: MessageItem[];
20
20
  currentUserId?: string;
21
21
  onPress?: () => void;
22
22
  }
23
23
 
24
- const PinnedMessageThumbnail = memo(
25
- ({ message }: { message: DMessageItem }) => {
26
- const preview = useMemo(() => getPinnedMessagePreview(message), [message]);
24
+ const PinnedMessageThumbnail = memo(({ message }: { message: MessageItem }) => {
25
+ const preview = useMemo(() => getPinnedMessagePreview(message), [message]);
27
26
 
28
- if (preview.mediaType === 'image' && preview.thumbnailUrl) {
29
- return (
30
- <KImage.Base
31
- uri={preview.thumbnailUrl}
32
- style={styles.thumbnail}
33
- resizeMode="cover"
34
- />
35
- );
36
- }
37
-
38
- if (preview.mediaType === 'video') {
39
- return (
40
- <KContainer.View style={styles.thumbnail}>
41
- {preview.thumbnailUrl && (
42
- <KImage.Base
43
- uri={preview.thumbnailUrl}
44
- style={styles.thumbnail}
45
- resizeMode="cover"
46
- />
47
- )}
48
- <KContainer.View style={styles.videoOverlay} center>
49
- <KImage.VectorIcons
50
- name="play-circle"
51
- provider="MaterialCommunityIcons"
52
- size={16}
53
- color={KColors.white}
54
- />
55
- </KContainer.View>
56
- </KContainer.View>
57
- );
58
- }
27
+ if (preview.mediaType === 'image' && preview.thumbnailUrl) {
28
+ return (
29
+ <KImage.Base
30
+ uri={preview.thumbnailUrl}
31
+ style={styles.thumbnail}
32
+ resizeMode="cover"
33
+ />
34
+ );
35
+ }
59
36
 
60
- if (preview.mediaType === 'file' && preview.fileIcon) {
61
- return (
62
- <KContainer.View marginL="0.75rem">
37
+ if (preview.mediaType === 'video') {
38
+ return (
39
+ <KContainer.View style={styles.thumbnail}>
40
+ {preview.thumbnailUrl && (
63
41
  <KImage.Base
64
- uri={preview.fileIcon}
65
- style={styles.fileIcon}
66
- resizeMode="contain"
42
+ uri={preview.thumbnailUrl}
43
+ style={styles.thumbnail}
44
+ resizeMode="cover"
45
+ />
46
+ )}
47
+ <KContainer.View style={styles.videoOverlay} center>
48
+ <KImage.VectorIcons
49
+ name="play-circle"
50
+ provider="MaterialCommunityIcons"
51
+ size={16}
52
+ color={KColors.white}
67
53
  />
68
54
  </KContainer.View>
69
- );
70
- }
55
+ </KContainer.View>
56
+ );
57
+ }
71
58
 
72
- return null;
59
+ if (preview.mediaType === 'file' && preview.fileIcon) {
60
+ return (
61
+ <KContainer.View marginL="0.75rem">
62
+ <KImage.Base
63
+ uri={preview.fileIcon}
64
+ style={styles.fileIcon}
65
+ resizeMode="contain"
66
+ />
67
+ </KContainer.View>
68
+ );
73
69
  }
74
- );
70
+
71
+ return null;
72
+ });
75
73
 
76
74
  PinnedMessageThumbnail.displayName = 'PinnedMessageThumbnail';
77
75
 
@@ -86,6 +84,7 @@ export const PinnedMessageBar = memo(
86
84
  latest.sendID === currentUserId ? 'Bạn' : latest.senderNickname || '';
87
85
  const remainingCount = messages.length - 1;
88
86
  const { subtitleKey } = getPinnedMessagePreview(latest);
87
+ const messagePreview = getMessagePreviewParts(latest);
89
88
 
90
89
  return (
91
90
  <KContainer.Touchable
@@ -97,8 +96,12 @@ export const PinnedMessageBar = memo(
97
96
 
98
97
  <KContainer.View row alignItems flex paddingR="0.75rem">
99
98
  <KContainer.View style={styles.content}>
100
- <KLabel.Text typo="TextNmMedium" numberOfLines={1}>
101
- {getMessagePreview(latest)}
99
+ <KLabel.Text
100
+ typo="TextNmMedium"
101
+ numberOfLines={1}
102
+ {...messagePreview.textProps}
103
+ >
104
+ {messagePreview.text}
102
105
  </KLabel.Text>
103
106
 
104
107
  <KContainer.View row alignItems paddingR="0.75rem">
@@ -18,17 +18,16 @@ import { formatMessageTime, messageStyles } from './legend/messageTypes';
18
18
  import { MessageToolboxWrapper } from './messageToolbox/MessageToolboxWrapper';
19
19
  import { MessageToolboxMenu } from './messageToolbox/MessageToolboxMenu';
20
20
  import type { MeasuredRect } from './messageToolbox/types';
21
- import type { DMessageItem } from '../../types/chat';
22
21
 
23
22
  interface PinnedMessageToolboxRef {
24
- show: (message: DMessageItem, rect: MeasuredRect) => void;
23
+ show: (message: MessageItem, rect: MeasuredRect) => void;
25
24
  hide: () => void;
26
25
  }
27
26
 
28
27
  interface PinnedMessageToolboxOverlayProps {
29
28
  currentUserId?: string;
30
29
  unpinMessage: (message: MessageItem) => void | Promise<void>;
31
- customMessageItemFactory?: (item: DMessageItem) => ReactNode;
30
+ customMessageItemFactory?: (item: MessageItem) => ReactNode;
32
31
  }
33
32
 
34
33
  // Mirrors MessageToolboxOverlay (used for the main chat list's long-press
@@ -38,7 +37,7 @@ const PinnedMessageToolboxOverlay = memo(
38
37
  forwardRef<PinnedMessageToolboxRef, PinnedMessageToolboxOverlayProps>(
39
38
  ({ currentUserId, unpinMessage, customMessageItemFactory }, ref) => {
40
39
  const [visible, setVisible] = useState(false);
41
- const [message, setMessage] = useState<DMessageItem | undefined>(
40
+ const [message, setMessage] = useState<MessageItem | undefined>(
42
41
  undefined
43
42
  );
44
43
  const [rect, setRect] = useState<MeasuredRect | null>(null);
@@ -49,7 +48,7 @@ const PinnedMessageToolboxOverlay = memo(
49
48
  setRect(null);
50
49
  }, []);
51
50
 
52
- const show = useCallback((msg: DMessageItem, msgRect: MeasuredRect) => {
51
+ const show = useCallback((msg: MessageItem, msgRect: MeasuredRect) => {
53
52
  setMessage(msg);
54
53
  setRect(msgRect);
55
54
  setVisible(true);
@@ -127,7 +126,7 @@ const styles = StyleSheet.create({
127
126
  interface PinnedMessagesSheetContentProps {
128
127
  currentUserId?: string;
129
128
  unpinMessage: (message: MessageItem) => void | Promise<void>;
130
- customMessageItemFactory?: (item: DMessageItem) => ReactNode;
129
+ customMessageItemFactory?: (item: MessageItem) => ReactNode;
131
130
  }
132
131
 
133
132
  const PinnedMessagesSheetContent = memo(
@@ -141,7 +140,7 @@ const PinnedMessagesSheetContent = memo(
141
140
  const rowRefsMap = useRef<Map<string, View | null>>(new Map());
142
141
 
143
142
  const handleLongPress = useCallback(
144
- (messageId: string, message: DMessageItem) => {
143
+ (messageId: string, message: MessageItem) => {
145
144
  const node = rowRefsMap.current.get(messageId);
146
145
  node?.measureInWindow((x, y, width, height) => {
147
146
  toolboxRef.current?.show(message, { x, y, width, height });
@@ -216,7 +215,7 @@ PinnedMessagesSheetContent.displayName = 'PinnedMessagesSheetContent';
216
215
  export interface OpenPinnedMessagesSheetParams {
217
216
  currentUserId?: string;
218
217
  unpinMessage: (message: MessageItem) => void | Promise<void>;
219
- customMessageItemFactory?: (item: DMessageItem) => ReactNode;
218
+ customMessageItemFactory?: (item: MessageItem) => ReactNode;
220
219
  }
221
220
 
222
221
  export const openPinnedMessagesSheet = ({
@@ -6,7 +6,6 @@ import { MessageType } from '@droppii/openim-rn-client-sdk';
6
6
  import type { MessageItem } from '@droppii/openim-rn-client-sdk';
7
7
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
8
8
  import { runOnJS } from 'react-native-reanimated';
9
- import type { DMessageItem } from '../../../types/chat';
10
9
  import type { MediaItem } from '../../../hooks/useMediaViewer';
11
10
  import { TextMessage } from '../../../components/messages/textMessage';
12
11
  import { ImageMessage } from '../../../components/messages/imageMessage';
@@ -18,6 +17,7 @@ import { RevokedMessage } from '../../../components/messages/revokedMessage';
18
17
  import { MessageStatusIndicator } from '../../../components/messages/MessageStatusIndicator';
19
18
  import type { BaseLegendMessageProps } from '../../../components/messages/types';
20
19
  import { messageStyles } from './messageTypes';
20
+ import QuotedMessage from '../../../components/messages/quotedMessage';
21
21
 
22
22
  type MessageProps = BaseLegendMessageProps & {
23
23
  onPressUrl?: (url: string) => void;
@@ -25,14 +25,14 @@ type MessageProps = BaseLegendMessageProps & {
25
25
  };
26
26
 
27
27
  interface LegendChatMessageProps {
28
- message: DMessageItem;
28
+ message: MessageItem;
29
29
  isOutgoing: boolean;
30
30
  createdAtTime: number;
31
31
  onMediaPress?: (items: MediaItem[], index?: number) => void;
32
32
  onPressUrl?: (url: string) => void;
33
33
  onRetry?: (message: MessageItem) => void;
34
34
  onLongPress?: () => void;
35
- customMessageItemFactory?: (m: DMessageItem) => ReactNode;
35
+ customMessageItemFactory?: (m: MessageItem) => ReactNode;
36
36
  }
37
37
 
38
38
  // OpenIM message types (not all exposed as MessageType enum)
@@ -43,7 +43,7 @@ const messageComponentMap: Partial<
43
43
  > = {
44
44
  [MessageType.TextMessage]: TextMessage,
45
45
  [MessageType.AtTextMessage]: TextMessage,
46
- [MessageType.QuoteMessage]: TextMessage,
46
+ [MessageType.QuoteMessage]: QuotedMessage,
47
47
  [MessageType.UrlTextMessage]: LinkMessage,
48
48
  [MessageType.PictureMessage]: ImageMessage,
49
49
  [MessageType.VideoMessage]: VideoMessage,
@@ -12,13 +12,15 @@ interface MessageToolboxMenuProps {
12
12
  showRetryMessage?: boolean;
13
13
  showRevokeMessage?: boolean;
14
14
  showViewOriginalMessage?: boolean;
15
- onCopyMessage?: () => void;
16
- onCopyMessageLink?: () => void;
17
15
  onPinMessage?: () => void;
18
16
  onUnpinMessage?: () => void;
17
+ onViewOriginalMessage?: () => void;
18
+ showQuoteMessage?: boolean;
19
+ onCopyMessage?: () => void;
20
+ onCopyMessageLink?: () => void;
19
21
  onRetryMessage?: () => void;
20
22
  onRevokeMessage?: () => void;
21
- onViewOriginalMessage?: () => void;
23
+ onQuoteMessage?: () => void;
22
24
  }
23
25
 
24
26
  export const MessageToolboxMenu = memo(
@@ -30,6 +32,7 @@ export const MessageToolboxMenu = memo(
30
32
  showRetryMessage,
31
33
  showRevokeMessage,
32
34
  showViewOriginalMessage,
35
+ showQuoteMessage,
33
36
  onCopyMessage,
34
37
  onCopyMessageLink,
35
38
  onPinMessage,
@@ -37,9 +40,18 @@ export const MessageToolboxMenu = memo(
37
40
  onRetryMessage,
38
41
  onRevokeMessage,
39
42
  onViewOriginalMessage,
43
+ onQuoteMessage,
40
44
  }: MessageToolboxMenuProps) => {
41
45
  return (
42
46
  <KContainer.View style={styles.card}>
47
+ <MessageToolboxItem
48
+ icon="arrow-reply-o"
49
+ color={KColors.gray.dark}
50
+ label={trans('chat:reply_message')}
51
+ visible={showQuoteMessage}
52
+ onPress={onQuoteMessage}
53
+ />
54
+
43
55
  <MessageToolboxItem
44
56
  icon="copy-o"
45
57
  color={KColors.gray.dark}
@@ -15,17 +15,18 @@ import {
15
15
  getCopyableMessageText,
16
16
  getMessageToolboxItems,
17
17
  } from './messageToolbox.utils';
18
- import type { DMessageItem } from '../../../types/chat';
19
18
  import type { MeasuredRect, MessageToolboxRef } from './types';
20
19
  import { MessageToolboxWrapper } from './MessageToolboxWrapper';
21
20
  import type { ReactNode } from 'react';
22
21
  import { useSendMessage } from '../../../hooks/message/useSendMessage';
22
+ import { useChatDetailContext } from '../../../context';
23
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
23
24
 
24
25
  interface MessageToolboxOverlayProps {
25
26
  currentUserId?: string;
26
- customMessageItemFactory?: (item: DMessageItem) => ReactNode;
27
- onPinMessage?: (message: DMessageItem) => void;
28
- onUnpinMessage?: (message: DMessageItem) => void;
27
+ customMessageItemFactory?: (item: MessageItem) => ReactNode;
28
+ onPinMessage?: (message: MessageItem) => void;
29
+ onUnpinMessage?: (message: MessageItem) => void;
29
30
  }
30
31
 
31
32
  export const MessageToolboxOverlay = forwardRef<
@@ -41,8 +42,9 @@ export const MessageToolboxOverlay = forwardRef<
41
42
  },
42
43
  ref
43
44
  ) => {
45
+ const { setQuotedMessage } = useChatDetailContext();
44
46
  const [visible, setVisible] = useState(false);
45
- const [message, setMessage] = useState<DMessageItem | undefined>(undefined);
47
+ const [message, setMessage] = useState<MessageItem | undefined>(undefined);
46
48
  const [rect, setRect] = useState<MeasuredRect | null>(null);
47
49
  const toolboxItems = useMemo(
48
50
  () => getMessageToolboxItems(message, currentUserId),
@@ -57,7 +59,7 @@ export const MessageToolboxOverlay = forwardRef<
57
59
  }, []);
58
60
 
59
61
  const show = useCallback(
60
- (msg: DMessageItem, msgRect: MeasuredRect) => {
62
+ (msg: MessageItem, msgRect: MeasuredRect) => {
61
63
  const toolboxNotAvailable =
62
64
  Object.values(getMessageToolboxItems(msg, currentUserId)).filter(
63
65
  Boolean
@@ -135,6 +137,12 @@ export const MessageToolboxOverlay = forwardRef<
135
137
  onUnpinMessage?.(message);
136
138
  }, [message, hide, onUnpinMessage]);
137
139
 
140
+ const handleQuoteMessage = useCallback(() => {
141
+ if (!message) return;
142
+ setQuotedMessage(message);
143
+ hide();
144
+ }, [message, hide, setQuotedMessage]);
145
+
138
146
  if (!visible || !message || !rect) {
139
147
  return null;
140
148
  }
@@ -171,6 +179,7 @@ export const MessageToolboxOverlay = forwardRef<
171
179
  onUnpinMessage={handleUnpinMessage}
172
180
  onRetryMessage={handleRetryMessage}
173
181
  onRevokeMessage={handleRevokeMessage}
182
+ onQuoteMessage={handleQuoteMessage}
174
183
  />
175
184
  }
176
185
  />
@@ -1,5 +1,8 @@
1
- import { MessageStatus, MessageType } from '@droppii/openim-rn-client-sdk';
2
- import type { DMessageItem } from '../../../types/chat';
1
+ import {
2
+ MessageStatus,
3
+ MessageType,
4
+ type MessageItem,
5
+ } from '@droppii/openim-rn-client-sdk';
3
6
  import { getMessageText } from '../../../utils/legendListMessage';
4
7
  import { isMessageRevoked } from '../../../utils/messageUtils';
5
8
 
@@ -22,46 +25,50 @@ const PIN_SUPPORTED_TYPES: number[] = [
22
25
  MessageType.FileMessage,
23
26
  ];
24
27
 
25
- const isMessageSucceeded = (message: DMessageItem): boolean =>
28
+ const isMessageSucceeded = (message: MessageItem): boolean =>
26
29
  message.status !== MessageStatus.Failed &&
27
30
  message.status !== MessageStatus.Sending;
28
31
 
29
32
  // Failed or revoked messages never offer either copy action.
30
- const isEligibleForToolbox = (message: DMessageItem): boolean =>
33
+ const isEligibleForToolbox = (message: MessageItem): boolean =>
31
34
  isMessageSucceeded(message) && !isMessageRevoked(message);
32
35
 
33
- const isCopyMessageSupported = (message: DMessageItem): boolean =>
36
+ const isCopyMessageSupported = (message: MessageItem): boolean =>
34
37
  isEligibleForToolbox(message) &&
35
38
  COPYABLE_TEXT_TYPES.includes(message.contentType);
36
39
 
37
40
  // All message types are supported, as long as the message is eligible.
38
- // const isCopyMessageLinkSupported = (message: DMessageItem): boolean =>
41
+ // const isCopyMessageLinkSupported = (message: MessageItem): boolean =>
39
42
  // isEligibleForToolbox(message);
40
43
 
41
- const isRetrySupported = (message: DMessageItem): boolean =>
44
+ const isRetrySupported = (message: MessageItem): boolean =>
42
45
  message.status === MessageStatus.Failed;
43
46
 
44
47
  // Merged/album messages (grouped photos) count as "image" for pinning
45
48
  // purposes, mirroring the preview logic in utils/messageUtils.ts.
46
- const isMergedImageMessage = (message: DMessageItem): boolean =>
49
+ const isMergedImageMessage = (message: MessageItem): boolean =>
47
50
  message.contentType === MessageType.MergeMessage &&
48
51
  !!message.mergeElem?.multiMessage?.some(
49
52
  (msg) => msg.contentType === MessageType.PictureMessage
50
53
  );
51
54
 
52
55
  // BR-002: Pin message supports only successfully sent messages.
53
- const isPinMessageSupported = (message: DMessageItem): boolean =>
56
+ const isPinMessageSupported = (message: MessageItem): boolean =>
54
57
  isEligibleForToolbox(message) &&
55
58
  (PIN_SUPPORTED_TYPES.includes(message.contentType) ||
56
59
  isMergedImageMessage(message));
57
60
 
61
+ const isQuoteMessageSupported = (message: MessageItem): boolean =>
62
+ isMessageSucceeded(message) &&
63
+ message?.contentType !== MessageType.RevokeMessage;
64
+
58
65
  // Revoke is only available within 24h of a successful send.
59
66
  const REVOKE_WINDOW_MS = 24 * 60 * 60 * 1000;
60
67
 
61
68
  // Revoke is only available for messages sent by the current user.
62
69
  // And not limited by message content type.
63
70
  const isRevokeMessageSupported = (
64
- message: DMessageItem,
71
+ message: MessageItem,
65
72
  currentUserId?: string
66
73
  ): boolean =>
67
74
  isEligibleForToolbox(message) &&
@@ -69,7 +76,7 @@ const isRevokeMessageSupported = (
69
76
  message.sendID === currentUserId &&
70
77
  Date.now() - (message.sendTime || message.createTime || 0) < REVOKE_WINDOW_MS;
71
78
 
72
- export const getCopyableMessageText = (message: DMessageItem): string => {
79
+ export const getCopyableMessageText = (message: MessageItem): string => {
73
80
  if (message.contentType === MessageType.UrlTextMessage) {
74
81
  return message.urlTextElem?.content ?? '';
75
82
  }
@@ -77,7 +84,7 @@ export const getCopyableMessageText = (message: DMessageItem): string => {
77
84
  };
78
85
 
79
86
  export const getMessageToolboxItems = (
80
- message?: DMessageItem,
87
+ message?: MessageItem,
81
88
  currentUserId?: string
82
89
  ) => {
83
90
  if (message == null) {
@@ -88,6 +95,7 @@ export const getMessageToolboxItems = (
88
95
  // const showCopyMessageLink = isCopyMessageLinkSupported(message);
89
96
  const showRetryMessage = isRetrySupported(message);
90
97
  const showRevokeMessage = isRevokeMessageSupported(message, currentUserId);
98
+ const showQuoteMessage = isQuoteMessageSupported(message);
91
99
 
92
100
  const isPinSupported = isPinMessageSupported(message);
93
101
  const showPinMessage = isPinSupported && !message.isPinned;
@@ -100,5 +108,6 @@ export const getMessageToolboxItems = (
100
108
  showRevokeMessage,
101
109
  showPinMessage,
102
110
  showUnpinMessage,
111
+ showQuoteMessage,
103
112
  };
104
113
  };
@@ -1,4 +1,4 @@
1
- import type { DMessageItem } from '../../../types/chat';
1
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
2
2
 
3
3
  export interface MeasuredRect {
4
4
  x: number;
@@ -8,6 +8,6 @@ export interface MeasuredRect {
8
8
  }
9
9
 
10
10
  export interface MessageToolboxRef {
11
- show: (message: DMessageItem, rect: MeasuredRect) => void;
11
+ show: (message: MessageItem, rect: MeasuredRect) => void;
12
12
  hide: () => void;
13
13
  }