Bluesky app fork with some witchin' additions 馃挮
at main 211 lines 7.4 kB view raw
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)