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