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