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