Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 206 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 {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)