forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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/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 Toast from '#/view/com/util/Toast'
16import * as ContextMenu from '#/components/ContextMenu'
17import {type TriggerProps} from '#/components/ContextMenu/types'
18import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
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 {ReportDialog} from '#/components/moderation/ReportDialog'
24import * as Prompt from '#/components/Prompt'
25import {usePromptControl} from '#/components/Prompt'
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`), 'clipboard-check')
62 }, [_, message.text, message.facets])
63
64 const onPressTranslateMessage = useCallback(() => {
65 void translate(message.text, langPrefs.primaryLanguage)
66
67 ax.metric('translate', {
68 sourceLanguages: [],
69 targetLanguage: langPrefs.primaryLanguage,
70 textLength: message.text.length,
71 })
72 }, [ax, langPrefs.primaryLanguage, message.text, translate])
73
74 const onDelete = useCallback(() => {
75 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
76 convo
77 .deleteMessage(message.id)
78 .then(() =>
79 Toast.show(_(msg({message: 'Message deleted', context: 'toast'}))),
80 )
81 .catch(() => Toast.show(_(msg`Failed to delete message`)))
82 }, [_, convo, message.id])
83
84 const onEmojiSelect = useCallback(
85 (emoji: string) => {
86 if (
87 message.reactions?.find(
88 reaction =>
89 reaction.value === emoji &&
90 reaction.sender.did === currentAccount?.did,
91 )
92 ) {
93 convo
94 .removeReaction(message.id, emoji)
95 .catch(() => Toast.show(_(msg`Failed to remove emoji reaction`)))
96 } else {
97 if (hasReachedReactionLimit(message, currentAccount?.did)) return
98 convo
99 .addReaction(message.id, emoji)
100 .catch(() =>
101 Toast.show(_(msg`Failed to add emoji reaction`), 'xmark'),
102 )
103 }
104 },
105 [_, convo, message, currentAccount?.did],
106 )
107
108 const sender = convo.convo.members.find(
109 member => member.did === message.sender.did,
110 )
111
112 return (
113 <>
114 <ContextMenu.Root>
115 {IS_NATIVE && (
116 <ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'}>
117 <EmojiReactionPicker
118 message={message}
119 onEmojiSelect={onEmojiSelect}
120 />
121 </ContextMenu.AuxiliaryView>
122 )}
123
124 <ContextMenu.Trigger
125 label={_(msg`Message options`)}
126 contentLabel={_(
127 msg`Message from @${
128 sender?.handle ?? 'unknown' // should always be defined
129 }: ${message.text}`,
130 )}>
131 {children}
132 </ContextMenu.Trigger>
133
134 <ContextMenu.Outer align={isFromSelf ? 'right' : 'left'}>
135 {message.text.length > 0 && (
136 <>
137 <ContextMenu.Item
138 testID="messageDropdownTranslateBtn"
139 label={_(msg`Translate`)}
140 onPress={onPressTranslateMessage}>
141 <ContextMenu.ItemText>{_(msg`Translate`)}</ContextMenu.ItemText>
142 <ContextMenu.ItemIcon icon={Translate} position="right" />
143 </ContextMenu.Item>
144 <ContextMenu.Item
145 testID="messageDropdownCopyBtn"
146 label={_(msg`Copy message text`)}
147 onPress={onCopyMessage}>
148 <ContextMenu.ItemText>
149 {_(msg`Copy message text`)}
150 </ContextMenu.ItemText>
151 <ContextMenu.ItemIcon icon={ClipboardIcon} position="right" />
152 </ContextMenu.Item>
153 <ContextMenu.Divider />
154 </>
155 )}
156 <ContextMenu.Item
157 testID="messageDropdownDeleteBtn"
158 label={_(msg`Delete message for me`)}
159 onPress={() => deleteControl.open()}>
160 <ContextMenu.ItemText>{_(msg`Delete for me`)}</ContextMenu.ItemText>
161 <ContextMenu.ItemIcon icon={Trash} position="right" />
162 </ContextMenu.Item>
163 {!isFromSelf && (
164 <ContextMenu.Item
165 testID="messageDropdownReportBtn"
166 label={_(msg`Report message`)}
167 onPress={() => reportControl.open()}>
168 <ContextMenu.ItemText>{_(msg`Report`)}</ContextMenu.ItemText>
169 <ContextMenu.ItemIcon icon={Warning} position="right" />
170 </ContextMenu.Item>
171 )}
172 </ContextMenu.Outer>
173 </ContextMenu.Root>
174
175 <ReportDialog
176 control={reportControl}
177 subject={{
178 view: 'message',
179 convoId: convo.convo.id,
180 message,
181 }}
182 onAfterSubmit={() => {
183 if (sender) {
184 unstableCacheProfileView(queryClient, sender)
185 }
186 blockOrDeleteControl.open()
187 }}
188 />
189 <AfterReportDialog
190 control={blockOrDeleteControl}
191 currentScreen="conversation"
192 params={{
193 convoId: convo.convo.id,
194 message,
195 }}
196 />
197
198 <Prompt.Basic
199 control={deleteControl}
200 title={_(msg`Delete message`)}
201 description={_(
202 msg`Are you sure you want to delete this message? The message will be deleted for you, but not for the other participant.`,
203 )}
204 confirmButtonCta={_(msg`Delete`)}
205 confirmButtonColor="negative"
206 onConfirm={onDelete}
207 />
208 </>
209 )
210}
211MessageContextMenu = memo(MessageContextMenu)