Bluesky app fork with some witchin' additions 馃挮
at main 209 lines 7.2 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/macro' 6import {useLingui} from '@lingui/react' 7 8import {useTranslate} from '#/lib/hooks/useTranslate' 9import {richTextToString} from '#/lib/strings/rich-text-helpers' 10import {logger} from '#/logger' 11import {isNative} from '#/platform/detection' 12import {useConvoActive} from '#/state/messages/convo' 13import {useLanguagePrefs} from '#/state/preferences' 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 {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 blockOrDeleteControl = usePromptControl() 42 const langPrefs = useLanguagePrefs() 43 const translate = useTranslate() 44 45 const isFromSelf = message.sender?.did === currentAccount?.did 46 47 const onCopyMessage = useCallback(() => { 48 const str = richTextToString( 49 new RichText({ 50 text: message.text, 51 facets: message.facets, 52 }), 53 true, 54 ) 55 56 Clipboard.setStringAsync(str) 57 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 58 }, [_, message.text, message.facets]) 59 60 const onPressTranslateMessage = useCallback(() => { 61 translate(message.text, langPrefs.primaryLanguage) 62 63 logger.metric( 64 'translate', 65 { 66 sourceLanguages: [], 67 targetLanguage: langPrefs.primaryLanguage, 68 textLength: message.text.length, 69 }, 70 {statsig: false}, 71 ) 72 }, [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 {isNative && ( 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 // currentScreen="conversation" 177 control={reportControl} 178 subject={{ 179 view: 'message', 180 convoId: convo.convo.id, 181 message, 182 }} 183 onAfterSubmit={() => { 184 blockOrDeleteControl.open() 185 }} 186 /> 187 <AfterReportDialog 188 control={blockOrDeleteControl} 189 currentScreen="conversation" 190 params={{ 191 convoId: convo.convo.id, 192 message, 193 }} 194 /> 195 196 <Prompt.Basic 197 control={deleteControl} 198 title={_(msg`Delete message`)} 199 description={_( 200 msg`Are you sure you want to delete this message? The message will be deleted for you, but not for the other participant.`, 201 )} 202 confirmButtonCta={_(msg`Delete`)} 203 confirmButtonColor="negative" 204 onConfirm={onDelete} 205 /> 206 </> 207 ) 208} 209MessageContextMenu = memo(MessageContextMenu)