mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {memo, useCallback} from 'react'
2import {LayoutAnimation} from 'react-native'
3import * as Clipboard from 'expo-clipboard'
4import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
5import {msg} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
7
8import {useTranslate} from '#/lib/hooks/useTranslate'
9import {richTextToString} from '#/lib/strings/rich-text-helpers'
10import {logger} from '#/logger'
11import {isNative} from '#/platform/detection'
12import {useConvoActive} from '#/state/messages/convo'
13import {useLanguagePrefs} from '#/state/preferences'
14import {useSession} from '#/state/session'
15import * as Toast from '#/view/com/util/Toast'
16import * as ContextMenu from '#/components/ContextMenu'
17import {type TriggerProps} from '#/components/ContextMenu/types'
18import {ReportDialog} from '#/components/dms/ReportDialog'
19import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
20import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
21import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
22import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
23import * as Prompt from '#/components/Prompt'
24import {usePromptControl} from '#/components/Prompt'
25import {EmojiReactionPicker} from './EmojiReactionPicker'
26import {hasReachedReactionLimit} from './util'
27
28export let MessageContextMenu = ({
29 message,
30 children,
31}: {
32 message: ChatBskyConvoDefs.MessageView
33 children: TriggerProps['children']
34}): React.ReactNode => {
35 const {_} = useLingui()
36 const {currentAccount} = useSession()
37 const convo = useConvoActive()
38 const deleteControl = usePromptControl()
39 const reportControl = usePromptControl()
40 const langPrefs = useLanguagePrefs()
41 const translate = useTranslate()
42
43 const isFromSelf = message.sender?.did === currentAccount?.did
44
45 const onCopyMessage = useCallback(() => {
46 const str = richTextToString(
47 new RichText({
48 text: message.text,
49 facets: message.facets,
50 }),
51 true,
52 )
53
54 Clipboard.setStringAsync(str)
55 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
56 }, [_, message.text, message.facets])
57
58 const onPressTranslateMessage = useCallback(() => {
59 translate(message.text, langPrefs.primaryLanguage)
60
61 logger.metric(
62 'translate',
63 {
64 sourceLanguages: [],
65 targetLanguage: langPrefs.primaryLanguage,
66 textLength: message.text.length,
67 },
68 {statsig: false},
69 )
70 }, [langPrefs.primaryLanguage, message.text, translate])
71
72 const onDelete = useCallback(() => {
73 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
74 convo
75 .deleteMessage(message.id)
76 .then(() =>
77 Toast.show(_(msg({message: 'Message deleted', context: 'toast'}))),
78 )
79 .catch(() => Toast.show(_(msg`Failed to delete message`)))
80 }, [_, convo, message.id])
81
82 const onEmojiSelect = useCallback(
83 (emoji: string) => {
84 if (
85 message.reactions?.find(
86 reaction =>
87 reaction.value === emoji &&
88 reaction.sender.did === currentAccount?.did,
89 )
90 ) {
91 convo
92 .removeReaction(message.id, emoji)
93 .catch(() => Toast.show(_(msg`Failed to remove emoji reaction`)))
94 } else {
95 if (hasReachedReactionLimit(message, currentAccount?.did)) return
96 convo
97 .addReaction(message.id, emoji)
98 .catch(() =>
99 Toast.show(_(msg`Failed to add emoji reaction`), 'xmark'),
100 )
101 }
102 },
103 [_, convo, message, currentAccount?.did],
104 )
105
106 const sender = convo.convo.members.find(
107 member => member.did === message.sender.did,
108 )
109
110 return (
111 <>
112 <ContextMenu.Root>
113 {isNative && (
114 <ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'}>
115 <EmojiReactionPicker
116 message={message}
117 onEmojiSelect={onEmojiSelect}
118 />
119 </ContextMenu.AuxiliaryView>
120 )}
121
122 <ContextMenu.Trigger
123 label={_(msg`Message options`)}
124 contentLabel={_(
125 msg`Message from @${
126 sender?.handle ?? 'unknown' // should always be defined
127 }: ${message.text}`,
128 )}>
129 {children}
130 </ContextMenu.Trigger>
131
132 <ContextMenu.Outer align={isFromSelf ? 'right' : 'left'}>
133 {message.text.length > 0 && (
134 <>
135 <ContextMenu.Item
136 testID="messageDropdownTranslateBtn"
137 label={_(msg`Translate`)}
138 onPress={onPressTranslateMessage}>
139 <ContextMenu.ItemText>{_(msg`Translate`)}</ContextMenu.ItemText>
140 <ContextMenu.ItemIcon icon={Translate} position="right" />
141 </ContextMenu.Item>
142 <ContextMenu.Item
143 testID="messageDropdownCopyBtn"
144 label={_(msg`Copy message text`)}
145 onPress={onCopyMessage}>
146 <ContextMenu.ItemText>
147 {_(msg`Copy message text`)}
148 </ContextMenu.ItemText>
149 <ContextMenu.ItemIcon icon={ClipboardIcon} position="right" />
150 </ContextMenu.Item>
151 <ContextMenu.Divider />
152 </>
153 )}
154 <ContextMenu.Item
155 testID="messageDropdownDeleteBtn"
156 label={_(msg`Delete message for me`)}
157 onPress={() => deleteControl.open()}>
158 <ContextMenu.ItemText>{_(msg`Delete for me`)}</ContextMenu.ItemText>
159 <ContextMenu.ItemIcon icon={Trash} position="right" />
160 </ContextMenu.Item>
161 {!isFromSelf && (
162 <ContextMenu.Item
163 testID="messageDropdownReportBtn"
164 label={_(msg`Report message`)}
165 onPress={() => reportControl.open()}>
166 <ContextMenu.ItemText>{_(msg`Report`)}</ContextMenu.ItemText>
167 <ContextMenu.ItemIcon icon={Warning} position="right" />
168 </ContextMenu.Item>
169 )}
170 </ContextMenu.Outer>
171 </ContextMenu.Root>
172
173 <ReportDialog
174 currentScreen="conversation"
175 params={{type: 'convoMessage', convoId: convo.convo.id, message}}
176 control={reportControl}
177 />
178
179 <Prompt.Basic
180 control={deleteControl}
181 title={_(msg`Delete message`)}
182 description={_(
183 msg`Are you sure you want to delete this message? The message will be deleted for you, but not for the other participant.`,
184 )}
185 confirmButtonCta={_(msg`Delete`)}
186 confirmButtonColor="negative"
187 onConfirm={onDelete}
188 />
189 </>
190 )
191}
192MessageContextMenu = memo(MessageContextMenu)