Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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