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