@droppii-org/chat-mobile 0.2.25 → 0.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/components/flows/inputButtons/index.js +3 -6
- package/lib/module/components/flows/inputButtons/index.js.map +1 -1
- package/lib/module/components/messages/fileMessage/index.js +1 -1
- package/lib/module/components/messages/fileMessage/index.js.map +1 -1
- package/lib/module/components/messages/imageMessage/index.js +1 -1
- package/lib/module/components/messages/imageMessage/index.js.map +1 -1
- package/lib/module/components/messages/linkMessage/index.js +7 -7
- package/lib/module/components/messages/linkMessage/index.js.map +1 -1
- package/lib/module/components/messages/mergedMessage/index.js +5 -8
- package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
- package/lib/module/components/messages/styles.js +12 -0
- package/lib/module/components/messages/styles.js.map +1 -1
- package/lib/module/components/messages/textMessage/index.js +10 -7
- package/lib/module/components/messages/textMessage/index.js.map +1 -1
- package/lib/module/components/messages/videoMessage/index.js +7 -11
- package/lib/module/components/messages/videoMessage/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +11 -1
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +21 -14
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/constants.js +3 -2
- package/lib/module/screens/chat-detail/constants.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +36 -8
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js +98 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxMenu.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +110 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +64 -0
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js +37 -0
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js.map +1 -0
- package/lib/module/screens/chat-detail/messageToolbox/types.js +4 -0
- package/lib/module/screens/chat-detail/messageToolbox/types.js.map +1 -0
- package/lib/module/translation/resources/i18n.js +5 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/utils/ui.js +19 -0
- package/lib/module/utils/ui.js.map +1 -0
- package/lib/typescript/src/components/flows/inputButtons/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/styles.d.ts +15 -3
- package/lib/typescript/src/components/messages/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/constants.d.ts +2 -2
- package/lib/typescript/src/screens/chat-detail/constants.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts +11 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +10 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts +11 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts +18 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/types.d.ts +12 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/types.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/types.d.ts +2 -0
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/utils/ui.d.ts +2 -0
- package/lib/typescript/src/utils/ui.d.ts.map +1 -0
- package/package.json +3 -1
- package/src/components/flows/inputButtons/index.tsx +3 -5
- package/src/components/messages/fileMessage/index.tsx +0 -1
- package/src/components/messages/imageMessage/index.tsx +1 -1
- package/src/components/messages/linkMessage/index.tsx +4 -3
- package/src/components/messages/mergedMessage/index.tsx +13 -17
- package/src/components/messages/styles.ts +12 -0
- package/src/components/messages/textMessage/index.tsx +8 -3
- package/src/components/messages/videoMessage/index.tsx +8 -10
- package/src/screens/chat-detail/ChatDetail.tsx +17 -0
- package/src/screens/chat-detail/ChatListLegend.tsx +30 -16
- package/src/screens/chat-detail/constants.ts +3 -2
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +49 -11
- package/src/screens/chat-detail/messageToolbox/MessageToolboxMenu.tsx +114 -0
- package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +130 -0
- package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +99 -0
- package/src/screens/chat-detail/messageToolbox/messageToolbox.utils.ts +56 -0
- package/src/screens/chat-detail/messageToolbox/types.ts +13 -0
- package/src/screens/chat-detail/types.ts +2 -0
- package/src/translation/resources/i18n.ts +5 -0
- package/src/utils/ui.ts +23 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memo, useCallback, useMemo, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { StyleSheet } from 'react-native';
|
|
2
|
+
import { StyleSheet, type View } from 'react-native';
|
|
3
3
|
import Animated, {
|
|
4
4
|
useSharedValue,
|
|
5
5
|
useAnimatedStyle,
|
|
@@ -19,6 +19,7 @@ import { InputButtonFlow } from '../../components/flows/inputButtons';
|
|
|
19
19
|
import { useResetFlow, useRunFlow } from '../../hooks/flows/useFlow';
|
|
20
20
|
import type { IRunConversationFlowPayload } from '../../types/flows';
|
|
21
21
|
import { useSendMessage } from '../../hooks/message/useSendMessage';
|
|
22
|
+
import { messageStyles } from './legend/messageTypes';
|
|
22
23
|
|
|
23
24
|
export const ChatListLegend = memo(
|
|
24
25
|
({
|
|
@@ -33,6 +34,7 @@ export const ChatListLegend = memo(
|
|
|
33
34
|
hasMoreNewer,
|
|
34
35
|
onMediaPress,
|
|
35
36
|
onPressUrl,
|
|
37
|
+
onLongPress,
|
|
36
38
|
}: ChatListProps) => {
|
|
37
39
|
// Precompute all message data in single pass (O(n))
|
|
38
40
|
const messageDataMap = useMemo(
|
|
@@ -60,6 +62,7 @@ export const ChatListLegend = memo(
|
|
|
60
62
|
const itemHeightsRef = useRef<Map<string, number>>(new Map());
|
|
61
63
|
const avgItemHeightRef = useRef(120);
|
|
62
64
|
const heightDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
65
|
+
const rowRefsMap = useRef<Map<string, View | null>>(new Map());
|
|
63
66
|
const [showBadge, setShowBadge] = useState(false);
|
|
64
67
|
|
|
65
68
|
const handleItemLayout = useCallback(
|
|
@@ -86,6 +89,16 @@ export const ChatListLegend = memo(
|
|
|
86
89
|
[]
|
|
87
90
|
);
|
|
88
91
|
|
|
92
|
+
const handleMessageLongPress = useCallback(
|
|
93
|
+
(messageId: string, message: DMessageItem) => {
|
|
94
|
+
const node = rowRefsMap.current.get(messageId);
|
|
95
|
+
node?.measureInWindow((x, y, width, height) => {
|
|
96
|
+
onLongPress?.(message, { x, y, width, height });
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
[onLongPress]
|
|
100
|
+
);
|
|
101
|
+
|
|
89
102
|
const onRunFlow = useCallback(
|
|
90
103
|
(payload: IRunConversationFlowPayload) => {
|
|
91
104
|
runFlow(payload);
|
|
@@ -130,10 +143,15 @@ export const ChatListLegend = memo(
|
|
|
130
143
|
<>
|
|
131
144
|
{dayStart && <LegendChatDay createdAt={createdAt} />}
|
|
132
145
|
<KContainer.View
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
ref={(node: View | null) => {
|
|
147
|
+
if (node) rowRefsMap.current.set(messageId, node);
|
|
148
|
+
else rowRefsMap.current.delete(messageId);
|
|
149
|
+
}}
|
|
150
|
+
style={
|
|
151
|
+
isOutgoing
|
|
152
|
+
? messageStyles.messageRowRight
|
|
153
|
+
: messageStyles.messageRowLeft
|
|
154
|
+
}
|
|
137
155
|
onLayout={(e) =>
|
|
138
156
|
handleItemLayout(messageId, e.nativeEvent.layout.height)
|
|
139
157
|
}
|
|
@@ -145,6 +163,11 @@ export const ChatListLegend = memo(
|
|
|
145
163
|
onMediaPress={onMediaPress}
|
|
146
164
|
onPressUrl={onPressUrl}
|
|
147
165
|
onRetry={retryMessage}
|
|
166
|
+
onLongPress={
|
|
167
|
+
onLongPress
|
|
168
|
+
? () => handleMessageLongPress(messageId, message)
|
|
169
|
+
: undefined
|
|
170
|
+
}
|
|
148
171
|
customMessageItemFactory={customMessageItemFactory}
|
|
149
172
|
/>
|
|
150
173
|
{!!flowInputButtonsNode && (
|
|
@@ -176,6 +199,8 @@ export const ChatListLegend = memo(
|
|
|
176
199
|
isBotConversation,
|
|
177
200
|
onMediaPress,
|
|
178
201
|
onPressUrl,
|
|
202
|
+
onLongPress,
|
|
203
|
+
handleMessageLongPress,
|
|
179
204
|
customMessageItemFactory,
|
|
180
205
|
onResetFlow,
|
|
181
206
|
onRunFlow,
|
|
@@ -399,17 +424,6 @@ const styles = StyleSheet.create({
|
|
|
399
424
|
container: {
|
|
400
425
|
position: 'relative',
|
|
401
426
|
},
|
|
402
|
-
messageRow: {
|
|
403
|
-
width: '100%',
|
|
404
|
-
paddingHorizontal: KSpacingValue['0.75rem'],
|
|
405
|
-
marginBottom: KSpacingValue['0.25rem'],
|
|
406
|
-
},
|
|
407
|
-
messageRowLeft: {
|
|
408
|
-
alignItems: 'flex-start',
|
|
409
|
-
},
|
|
410
|
-
messageRowRight: {
|
|
411
|
-
alignItems: 'flex-end',
|
|
412
|
-
},
|
|
413
427
|
content: {
|
|
414
428
|
paddingVertical: KSpacingValue['0.75rem'],
|
|
415
429
|
flexGrow: 1,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { KColors } from '@droppii/libs';
|
|
2
2
|
import type { DChatQuickAction, DChatAttachmentAction } from './types';
|
|
3
|
+
import { opacityToSolid } from '../../utils/ui';
|
|
3
4
|
|
|
4
5
|
export const CHAT_BUBBLE_COLORS = {
|
|
5
|
-
received: '
|
|
6
|
-
sent: '
|
|
6
|
+
received: opacityToSolid('#83899d1a', KColors.white, 0.1),
|
|
7
|
+
sent: opacityToSolid('#0051ff1a', KColors.white, 0.1),
|
|
7
8
|
text: KColors.gray.dark,
|
|
8
9
|
timestamp: KColors.gray.light,
|
|
9
10
|
dayLabel: KColors.gray.light,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { memo } from 'react';
|
|
2
|
-
import type { ComponentType, ReactNode } from 'react';
|
|
3
1
|
import { KContainer } from '@droppii/libs';
|
|
2
|
+
import { memo, useMemo } from 'react';
|
|
3
|
+
import type { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
|
4
4
|
import { MessageType } from '@droppii/openim-rn-client-sdk';
|
|
5
5
|
import type { MessageItem } from '@droppii/openim-rn-client-sdk';
|
|
6
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
7
|
+
import { runOnJS } from 'react-native-reanimated';
|
|
6
8
|
import type { DMessageItem } from '../../../types/chat';
|
|
7
9
|
import type { MediaItem } from '../../../hooks/useMediaViewer';
|
|
8
10
|
import { TextMessage } from '../../../components/messages/textMessage';
|
|
@@ -13,9 +15,11 @@ import { LinkMessage } from '../../../components/messages/linkMessage';
|
|
|
13
15
|
import { MergedMessage } from '../../../components/messages/mergedMessage';
|
|
14
16
|
import { MessageStatusIndicator } from '../../../components/messages/MessageStatusIndicator';
|
|
15
17
|
import type { BaseLegendMessageProps } from '../../../components/messages/types';
|
|
18
|
+
import { messageStyles } from './messageTypes';
|
|
16
19
|
|
|
17
20
|
type MessageProps = BaseLegendMessageProps & {
|
|
18
21
|
onPressUrl?: (url: string) => void;
|
|
22
|
+
onLongPress?: () => void;
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
interface LegendChatMessageProps {
|
|
@@ -25,6 +29,7 @@ interface LegendChatMessageProps {
|
|
|
25
29
|
onMediaPress?: (items: MediaItem[], index?: number) => void;
|
|
26
30
|
onPressUrl?: (url: string) => void;
|
|
27
31
|
onRetry?: (message: MessageItem) => void;
|
|
32
|
+
onLongPress?: () => void;
|
|
28
33
|
customMessageItemFactory?: (m: DMessageItem) => ReactNode;
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -58,11 +63,17 @@ export const LegendChatMessage = memo(
|
|
|
58
63
|
onMediaPress,
|
|
59
64
|
onPressUrl,
|
|
60
65
|
onRetry,
|
|
66
|
+
onLongPress,
|
|
61
67
|
customMessageItemFactory,
|
|
62
68
|
}: LegendChatMessageProps) => {
|
|
63
69
|
const customNode = customMessageItemFactory?.(message);
|
|
70
|
+
|
|
64
71
|
if (customNode) {
|
|
65
|
-
return
|
|
72
|
+
return (
|
|
73
|
+
<LongPressWrapper onLongPress={onLongPress}>
|
|
74
|
+
{customNode}
|
|
75
|
+
</LongPressWrapper>
|
|
76
|
+
);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
const MessageComponent: ComponentType<MessageProps> =
|
|
@@ -74,22 +85,49 @@ export const LegendChatMessage = memo(
|
|
|
74
85
|
onPressUrl && message?.contentType === MessageType.UrlTextMessage;
|
|
75
86
|
|
|
76
87
|
return (
|
|
77
|
-
<KContainer.View row alignItems>
|
|
88
|
+
<KContainer.View style={messageStyles.wrapper} row alignItems>
|
|
78
89
|
<MessageStatusIndicator
|
|
79
90
|
message={message}
|
|
80
91
|
isOutgoing={isOutgoing}
|
|
81
92
|
onRetry={onRetry}
|
|
82
93
|
/>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
<LongPressWrapper onLongPress={onLongPress}>
|
|
95
|
+
<MessageComponent
|
|
96
|
+
message={message}
|
|
97
|
+
isOutgoing={isOutgoing}
|
|
98
|
+
createdAtTime={createdAtTime}
|
|
99
|
+
onMediaPress={hasMediaPress ? onMediaPress : undefined}
|
|
100
|
+
onPressUrl={hasUrlPress ? onPressUrl : undefined}
|
|
101
|
+
/>
|
|
102
|
+
</LongPressWrapper>
|
|
90
103
|
</KContainer.View>
|
|
91
104
|
);
|
|
92
105
|
}
|
|
93
106
|
);
|
|
94
107
|
|
|
95
108
|
LegendChatMessage.displayName = 'LegendChatMessage';
|
|
109
|
+
|
|
110
|
+
const LongPressWrapper = ({
|
|
111
|
+
children,
|
|
112
|
+
onLongPress,
|
|
113
|
+
}: PropsWithChildren<{ onLongPress?: () => void }>) => {
|
|
114
|
+
const longPressGesture = useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
onLongPress
|
|
117
|
+
? Gesture.LongPress().onStart(() => {
|
|
118
|
+
runOnJS(onLongPress)();
|
|
119
|
+
})
|
|
120
|
+
: null,
|
|
121
|
+
[onLongPress]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!longPressGesture) {
|
|
125
|
+
return children;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<GestureDetector gesture={longPressGesture}>
|
|
130
|
+
<KContainer.View>{children}</KContainer.View>
|
|
131
|
+
</GestureDetector>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
KContainer,
|
|
5
|
+
KImage,
|
|
6
|
+
KLabel,
|
|
7
|
+
KColors,
|
|
8
|
+
KSpacingValue,
|
|
9
|
+
} from '@droppii/libs';
|
|
10
|
+
import { trans } from '../../../translation';
|
|
11
|
+
|
|
12
|
+
interface MessageToolboxMenuProps {
|
|
13
|
+
showCopyMessage?: boolean;
|
|
14
|
+
showCopyMessageLink?: boolean;
|
|
15
|
+
showRetryMessage?: boolean;
|
|
16
|
+
onCopyMessage: () => void;
|
|
17
|
+
onCopyMessageLink: () => void;
|
|
18
|
+
onRetryMessage: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const MessageToolboxMenu = memo(
|
|
22
|
+
({
|
|
23
|
+
showCopyMessage,
|
|
24
|
+
showCopyMessageLink,
|
|
25
|
+
showRetryMessage,
|
|
26
|
+
onCopyMessage,
|
|
27
|
+
onCopyMessageLink,
|
|
28
|
+
onRetryMessage,
|
|
29
|
+
}: MessageToolboxMenuProps) => {
|
|
30
|
+
if (!showCopyMessage && !showCopyMessageLink && !showRetryMessage) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<KContainer.View style={styles.card}>
|
|
36
|
+
<KContainer.VisibleView visible={!!showCopyMessage}>
|
|
37
|
+
<KContainer.Touchable style={styles.row} onPress={onCopyMessage}>
|
|
38
|
+
<KImage.VectorIcons
|
|
39
|
+
name="copy-o"
|
|
40
|
+
provider="DroppiiNew"
|
|
41
|
+
size={24}
|
|
42
|
+
color={KColors.gray.dark}
|
|
43
|
+
/>
|
|
44
|
+
<KLabel.Text
|
|
45
|
+
typo="TextMdMedium"
|
|
46
|
+
color={KColors.gray.dark}
|
|
47
|
+
marginL="0.75rem"
|
|
48
|
+
>
|
|
49
|
+
{trans('chat:copy_message')}
|
|
50
|
+
</KLabel.Text>
|
|
51
|
+
</KContainer.Touchable>
|
|
52
|
+
</KContainer.VisibleView>
|
|
53
|
+
|
|
54
|
+
<KContainer.VisibleView visible={!!showCopyMessageLink}>
|
|
55
|
+
<KContainer.Touchable style={styles.row} onPress={onCopyMessageLink}>
|
|
56
|
+
<KImage.VectorIcons
|
|
57
|
+
name="link-o"
|
|
58
|
+
provider="DroppiiNew"
|
|
59
|
+
size={24}
|
|
60
|
+
color={KColors.gray.dark}
|
|
61
|
+
/>
|
|
62
|
+
<KLabel.Text
|
|
63
|
+
typo="TextMdMedium"
|
|
64
|
+
color={KColors.gray.dark}
|
|
65
|
+
marginL="0.75rem"
|
|
66
|
+
>
|
|
67
|
+
{trans('chat:copy_message_link')}
|
|
68
|
+
</KLabel.Text>
|
|
69
|
+
</KContainer.Touchable>
|
|
70
|
+
</KContainer.VisibleView>
|
|
71
|
+
|
|
72
|
+
<KContainer.VisibleView visible={!!showRetryMessage}>
|
|
73
|
+
<KContainer.Touchable style={styles.row} onPress={onRetryMessage}>
|
|
74
|
+
<KImage.VectorIcons
|
|
75
|
+
name="arrow-reload-o"
|
|
76
|
+
provider="DroppiiNew"
|
|
77
|
+
size={24}
|
|
78
|
+
color={KColors.danger.normal}
|
|
79
|
+
/>
|
|
80
|
+
<KLabel.Text
|
|
81
|
+
typo="TextMdMedium"
|
|
82
|
+
color={KColors.danger.normal}
|
|
83
|
+
marginL="0.75rem"
|
|
84
|
+
>
|
|
85
|
+
{trans('chat:retry_message')}
|
|
86
|
+
</KLabel.Text>
|
|
87
|
+
</KContainer.Touchable>
|
|
88
|
+
</KContainer.VisibleView>
|
|
89
|
+
</KContainer.View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
MessageToolboxMenu.displayName = 'MessageToolboxMenu';
|
|
95
|
+
|
|
96
|
+
const styles = StyleSheet.create({
|
|
97
|
+
card: {
|
|
98
|
+
minWidth: 220,
|
|
99
|
+
paddingVertical: KSpacingValue['0.5rem'],
|
|
100
|
+
borderRadius: KSpacingValue['1rem'],
|
|
101
|
+
backgroundColor: KColors.white,
|
|
102
|
+
shadowColor: KColors.black,
|
|
103
|
+
shadowOffset: { width: 0, height: 0 },
|
|
104
|
+
shadowOpacity: 0.1,
|
|
105
|
+
shadowRadius: 24,
|
|
106
|
+
elevation: 8,
|
|
107
|
+
},
|
|
108
|
+
row: {
|
|
109
|
+
flexDirection: 'row',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
paddingHorizontal: KSpacingValue['1rem'],
|
|
112
|
+
paddingVertical: KSpacingValue['0.5rem'],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
|
|
2
|
+
import { Modal, Pressable, StyleSheet } from 'react-native';
|
|
3
|
+
import Clipboard from '@react-native-clipboard/clipboard';
|
|
4
|
+
import { UIUtils } from '../../../utils/UIUtils';
|
|
5
|
+
import { trans } from '../../../translation';
|
|
6
|
+
import { LegendChatMessage } from '../legend/LegendChatMessage';
|
|
7
|
+
import { MessageToolboxMenu } from './MessageToolboxMenu';
|
|
8
|
+
import {
|
|
9
|
+
getCopyableMessageText,
|
|
10
|
+
useMessageToolboxItems,
|
|
11
|
+
} from './messageToolbox.utils';
|
|
12
|
+
import type { DMessageItem } from '../../../types/chat';
|
|
13
|
+
import type { MeasuredRect, MessageToolboxRef } from './types';
|
|
14
|
+
import { MessageToolboxWrapper } from './MessageToolboxWrapper';
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
16
|
+
import { useSendMessage } from '../../../hooks/message/useSendMessage';
|
|
17
|
+
|
|
18
|
+
interface MessageToolboxOverlayProps {
|
|
19
|
+
currentUserId?: string;
|
|
20
|
+
customMessageItemFactory?: (item: DMessageItem) => ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const MessageToolboxOverlay = forwardRef<
|
|
24
|
+
MessageToolboxRef,
|
|
25
|
+
MessageToolboxOverlayProps
|
|
26
|
+
>(({ currentUserId = '', customMessageItemFactory }, ref) => {
|
|
27
|
+
const [visible, setVisible] = useState(false);
|
|
28
|
+
const [message, setMessage] = useState<DMessageItem | undefined>(undefined);
|
|
29
|
+
const [rect, setRect] = useState<MeasuredRect | null>(null);
|
|
30
|
+
const toolboxItems = useMessageToolboxItems(message);
|
|
31
|
+
const { retryMessage } = useSendMessage();
|
|
32
|
+
console.log(message);
|
|
33
|
+
|
|
34
|
+
const hide = useCallback(() => {
|
|
35
|
+
setVisible(false);
|
|
36
|
+
setMessage(undefined);
|
|
37
|
+
setRect(null);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const show = useCallback((msg: DMessageItem, msgRect: MeasuredRect) => {
|
|
41
|
+
setMessage(msg);
|
|
42
|
+
setRect(msgRect);
|
|
43
|
+
setVisible(true);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
useImperativeHandle(ref, () => ({ show, hide }), [show, hide]);
|
|
47
|
+
|
|
48
|
+
const handleCopyMessage = useCallback(() => {
|
|
49
|
+
if (!message) return;
|
|
50
|
+
const text = getCopyableMessageText(message);
|
|
51
|
+
if (text) {
|
|
52
|
+
Clipboard.setString(text);
|
|
53
|
+
UIUtils.toast.open({
|
|
54
|
+
title: trans('chat:copy_message_success'),
|
|
55
|
+
theme: 'success',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
hide();
|
|
59
|
+
}, [message, hide]);
|
|
60
|
+
|
|
61
|
+
const handleCopyMessageLink = useCallback(() => {
|
|
62
|
+
if (!message) return;
|
|
63
|
+
// UI-only for now: real shareable deeplink format (BR-001) is not decided yet.
|
|
64
|
+
const placeholderLink = `${message.recvID || message.groupID}:${message.clientMsgID ?? message.serverMsgID ?? ''}`;
|
|
65
|
+
Clipboard.setString(placeholderLink);
|
|
66
|
+
UIUtils.toast.open({
|
|
67
|
+
title: trans('chat:copy_message_link_success'),
|
|
68
|
+
theme: 'success',
|
|
69
|
+
});
|
|
70
|
+
hide();
|
|
71
|
+
}, [message, hide]);
|
|
72
|
+
|
|
73
|
+
const handleRetryMessage = useCallback(() => {
|
|
74
|
+
if (!message) return;
|
|
75
|
+
retryMessage(message);
|
|
76
|
+
hide();
|
|
77
|
+
}, [message, hide, retryMessage]);
|
|
78
|
+
|
|
79
|
+
if (!visible || !message || !rect) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const isOutgoing = message.sendID === currentUserId;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Modal
|
|
87
|
+
visible={visible}
|
|
88
|
+
transparent
|
|
89
|
+
statusBarTranslucent
|
|
90
|
+
animationType="fade"
|
|
91
|
+
onRequestClose={hide}
|
|
92
|
+
>
|
|
93
|
+
<Pressable style={styles.backdrop} onPress={hide} />
|
|
94
|
+
|
|
95
|
+
<MessageToolboxWrapper
|
|
96
|
+
rect={rect}
|
|
97
|
+
isOutgoing={isOutgoing}
|
|
98
|
+
message={
|
|
99
|
+
<LegendChatMessage
|
|
100
|
+
message={message}
|
|
101
|
+
isOutgoing={isOutgoing}
|
|
102
|
+
createdAtTime={message.sendTime || message.createTime}
|
|
103
|
+
customMessageItemFactory={customMessageItemFactory}
|
|
104
|
+
/>
|
|
105
|
+
}
|
|
106
|
+
toolbox={
|
|
107
|
+
<MessageToolboxMenu
|
|
108
|
+
{...toolboxItems}
|
|
109
|
+
onCopyMessage={handleCopyMessage}
|
|
110
|
+
onCopyMessageLink={handleCopyMessageLink}
|
|
111
|
+
onRetryMessage={handleRetryMessage}
|
|
112
|
+
/>
|
|
113
|
+
}
|
|
114
|
+
/>
|
|
115
|
+
</Modal>
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
MessageToolboxOverlay.displayName = 'MessageToolboxOverlay';
|
|
120
|
+
|
|
121
|
+
const styles = StyleSheet.create({
|
|
122
|
+
backdrop: {
|
|
123
|
+
position: 'absolute',
|
|
124
|
+
top: 0,
|
|
125
|
+
left: 0,
|
|
126
|
+
right: 0,
|
|
127
|
+
bottom: 0,
|
|
128
|
+
backgroundColor: 'rgba(0,0,0,0.75)',
|
|
129
|
+
},
|
|
130
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useCallback, memo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Platform,
|
|
5
|
+
useWindowDimensions,
|
|
6
|
+
type LayoutChangeEvent,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import {
|
|
9
|
+
useAnimatedStyle,
|
|
10
|
+
useDerivedValue,
|
|
11
|
+
useSharedValue,
|
|
12
|
+
withTiming,
|
|
13
|
+
} from 'react-native-reanimated';
|
|
14
|
+
import type { MeasuredRect } from './types';
|
|
15
|
+
import { messageStyles } from '../legend/messageTypes';
|
|
16
|
+
import Animated from 'react-native-reanimated';
|
|
17
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
18
|
+
|
|
19
|
+
const MENU_GAP = 8;
|
|
20
|
+
|
|
21
|
+
type MessageToolboxWrapperProps = {
|
|
22
|
+
isOutgoing?: boolean;
|
|
23
|
+
rect: MeasuredRect;
|
|
24
|
+
message?: React.ReactNode;
|
|
25
|
+
reaction?: React.ReactNode;
|
|
26
|
+
toolbox?: React.ReactNode;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const MessageToolboxWrapper = memo(
|
|
30
|
+
({
|
|
31
|
+
isOutgoing,
|
|
32
|
+
rect,
|
|
33
|
+
message,
|
|
34
|
+
reaction,
|
|
35
|
+
toolbox,
|
|
36
|
+
}: MessageToolboxWrapperProps) => {
|
|
37
|
+
const insets = useSafeAreaInsets();
|
|
38
|
+
const reactionHeight = useSharedValue(0);
|
|
39
|
+
const toolboxHeight = useSharedValue(0);
|
|
40
|
+
const topOffset = Platform.OS === 'android' ? insets.top : 0;
|
|
41
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
42
|
+
|
|
43
|
+
const onToolboxLayout = useCallback(
|
|
44
|
+
({ nativeEvent }: LayoutChangeEvent) => {
|
|
45
|
+
toolboxHeight.value = nativeEvent.layout.height;
|
|
46
|
+
},
|
|
47
|
+
[toolboxHeight]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const onReactionLayout = useCallback(
|
|
51
|
+
({ nativeEvent }: LayoutChangeEvent) => {
|
|
52
|
+
reactionHeight.value = nativeEvent.layout.height;
|
|
53
|
+
},
|
|
54
|
+
[reactionHeight]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const translateY = useDerivedValue(() => {
|
|
58
|
+
const menuGap =
|
|
59
|
+
(reactionHeight.value > 0 ? MENU_GAP : 0) +
|
|
60
|
+
(toolboxHeight.value > 0 ? MENU_GAP : 0);
|
|
61
|
+
|
|
62
|
+
const overflowBottom =
|
|
63
|
+
Math.max(rect.y, insets.top) +
|
|
64
|
+
topOffset +
|
|
65
|
+
rect.height +
|
|
66
|
+
toolboxHeight.value +
|
|
67
|
+
(insets.bottom || 32) +
|
|
68
|
+
menuGap -
|
|
69
|
+
windowHeight;
|
|
70
|
+
|
|
71
|
+
return withTiming(Math.min(-overflowBottom, 0));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const containerStyles = useAnimatedStyle(() => ({
|
|
75
|
+
top: Math.max(rect.y, insets.top) + topOffset,
|
|
76
|
+
left: rect.x,
|
|
77
|
+
gap: MENU_GAP,
|
|
78
|
+
transform: [{ translateY: translateY.value }],
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Animated.View
|
|
83
|
+
pointerEvents="box-none"
|
|
84
|
+
style={[
|
|
85
|
+
isOutgoing
|
|
86
|
+
? messageStyles.messageRowRight
|
|
87
|
+
: messageStyles.messageRowLeft,
|
|
88
|
+
containerStyles,
|
|
89
|
+
]}
|
|
90
|
+
>
|
|
91
|
+
<View pointerEvents="none">{message}</View>
|
|
92
|
+
<View onLayout={onReactionLayout}>{reaction}</View>
|
|
93
|
+
<View onLayout={onToolboxLayout}>{toolbox}</View>
|
|
94
|
+
</Animated.View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
MessageToolboxWrapper.displayName = 'MessageToolboxWrapper';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { MessageStatus, MessageType } from '@droppii/openim-rn-client-sdk';
|
|
2
|
+
import type { DMessageItem } from '../../../types/chat';
|
|
3
|
+
import { getMessageText } from '../../../utils/legendListMessage';
|
|
4
|
+
|
|
5
|
+
// Copy message content is only offered for text and link messages.
|
|
6
|
+
const COPYABLE_TEXT_TYPES: number[] = [
|
|
7
|
+
MessageType.TextMessage,
|
|
8
|
+
MessageType.AtTextMessage,
|
|
9
|
+
MessageType.QuoteMessage,
|
|
10
|
+
MessageType.UrlTextMessage,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const isMessageRecalled = (message: DMessageItem): boolean =>
|
|
14
|
+
message.contentType === MessageType.RevokeMessage;
|
|
15
|
+
|
|
16
|
+
export const isMessageSucceeded = (message: DMessageItem): boolean =>
|
|
17
|
+
message.status !== MessageStatus.Failed &&
|
|
18
|
+
message.status !== MessageStatus.Sending;
|
|
19
|
+
|
|
20
|
+
// Failed or recalled messages never offer either copy action.
|
|
21
|
+
export const isEligibleForToolbox = (message: DMessageItem): boolean =>
|
|
22
|
+
isMessageSucceeded(message) && !isMessageRecalled(message);
|
|
23
|
+
|
|
24
|
+
export const isCopyMessageSupported = (message: DMessageItem): boolean =>
|
|
25
|
+
isEligibleForToolbox(message) &&
|
|
26
|
+
COPYABLE_TEXT_TYPES.includes(message.contentType);
|
|
27
|
+
|
|
28
|
+
// All message types are supported, as long as the message is eligible.
|
|
29
|
+
export const isCopyMessageLinkSupported = (message: DMessageItem): boolean =>
|
|
30
|
+
isEligibleForToolbox(message);
|
|
31
|
+
|
|
32
|
+
export const isRetrySupported = (message: DMessageItem): boolean =>
|
|
33
|
+
message.status === MessageStatus.Failed;
|
|
34
|
+
|
|
35
|
+
export const getCopyableMessageText = (message: DMessageItem): string => {
|
|
36
|
+
if (message.contentType === MessageType.UrlTextMessage) {
|
|
37
|
+
return message.urlTextElem?.content ?? '';
|
|
38
|
+
}
|
|
39
|
+
return getMessageText(message);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const useMessageToolboxItems = (message?: DMessageItem) => {
|
|
43
|
+
if (message == null) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const showCopyMessage = isCopyMessageSupported(message);
|
|
48
|
+
const showCopyMessageLink = isCopyMessageLinkSupported(message);
|
|
49
|
+
const showRetryMessage = isRetrySupported(message);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
showCopyMessage,
|
|
53
|
+
showCopyMessageLink,
|
|
54
|
+
showRetryMessage,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DMessageItem } from '../../../types/chat';
|
|
2
|
+
|
|
3
|
+
export interface MeasuredRect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MessageToolboxRef {
|
|
11
|
+
show: (message: DMessageItem, rect: MeasuredRect) => void;
|
|
12
|
+
hide: () => void;
|
|
13
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import type { MediaItem } from '../../hooks/useMediaViewer';
|
|
3
3
|
import type { DMessageItem } from '../../types/chat';
|
|
4
4
|
import type { ConversationItem } from '@droppii/openim-rn-client-sdk';
|
|
5
|
+
import type { MeasuredRect } from './messageToolbox/types';
|
|
5
6
|
|
|
6
7
|
export type DChatActionIconProvider = 'MaterialCommunityIcons' | 'DroppiiNew';
|
|
7
8
|
|
|
@@ -72,6 +73,7 @@ export interface ChatListProps {
|
|
|
72
73
|
hasMoreNewer?: boolean;
|
|
73
74
|
onMediaPress?: (items: MediaItem[], index?: number) => void;
|
|
74
75
|
onPressUrl?: (url: string) => void;
|
|
76
|
+
onLongPress?: (message: DMessageItem, rect: MeasuredRect) => void;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
export interface ChatDetailProps extends Omit<
|
|
@@ -28,6 +28,11 @@ const i18nKey: Record<string, string> = {
|
|
|
28
28
|
'chat:retry_failed': 'Thử lại thất bại',
|
|
29
29
|
'chat:upload_in_progress': 'Đang tải ảnh lên',
|
|
30
30
|
'chat:wait_or_cancel': 'Vui lòng chờ hoặc hủy bỏ',
|
|
31
|
+
'chat:copy_message': 'Sao chép tin nhắn',
|
|
32
|
+
'chat:copy_message_link': 'Sao chép đường dẫn tin nhắn',
|
|
33
|
+
'chat:copy_message_success': 'Sao chép tin nhắn thành công',
|
|
34
|
+
'chat:copy_message_link_success': 'Sao chép đường dẫn tin nhắn thành công',
|
|
35
|
+
'chat:retry_message': 'Gửi lại',
|
|
31
36
|
'msg_type_image': '[Hình ảnh]',
|
|
32
37
|
'msg_type_voice': '[Tin nhắn thoại]',
|
|
33
38
|
'msg_type_video': '[Video]',
|
package/src/utils/ui.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const hexToObject = (hex: string) => {
|
|
2
|
+
const [_, r = 0, g = 0, b = 0] =
|
|
3
|
+
/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i
|
|
4
|
+
.exec(hex)
|
|
5
|
+
?.slice(0, 4)
|
|
6
|
+
.map((x) => parseInt(x, 16)) ?? [];
|
|
7
|
+
|
|
8
|
+
return { r, g, b };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function opacityToSolid(
|
|
12
|
+
foregroundColor: string,
|
|
13
|
+
backgroundColor: string,
|
|
14
|
+
alpha: number
|
|
15
|
+
) {
|
|
16
|
+
const fg = hexToObject(foregroundColor);
|
|
17
|
+
const bg = hexToObject(backgroundColor);
|
|
18
|
+
const r = Math.round(fg.r * alpha + bg.r * (1 - alpha));
|
|
19
|
+
const g = Math.round(fg.g * alpha + bg.g * (1 - alpha));
|
|
20
|
+
const b = Math.round(fg.b * alpha + bg.b * (1 - alpha));
|
|
21
|
+
|
|
22
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
23
|
+
}
|