Something went wrong. Try again.
hardened bluesky app fork with microcosm
Something went wrong. Try again.
6.4 kB · 187 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188import {memo, useCallback} from 'react'import {Platform} from 'react-native'import * as Clipboard from 'expo-clipboard'import { type ChatBskyConvoDefs, type ModerationOpts, RichText,} from '@atproto/api'import {plural} from '@lingui/core/macro'import {useLingui} from '@lingui/react/macro'
import {EMOJI_REACTION_LIMIT} from '#/lib/constants'import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'import {richTextToString} from '#/lib/strings/rich-text-helpers'import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'import {useConvoActive} from '#/state/messages/convo'import {useLanguagePrefs} from '#/state/preferences'import {useSession} from '#/state/session'import {atoms as a} from '#/alf'import * as ContextMenu from '#/components/ContextMenu'import {type TriggerProps} from '#/components/ContextMenu/types'import {useMessageDialogs} from '#/components/dms/MessageOverlays'import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'import {Language_Stroke2_Corner2_Rounded as LanguageIcon} from '#/components/icons/Language'import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'import * as Toast from '#/components/Toast'import {IS_NATIVE} from '#/env'import type * as bsky from '#/types/bsky'import {EmojiReactionPicker} from './EmojiReactionPicker'import {canReact, hasReachedReactionLimit} from './util'
export let MessageContextMenu = ({ message, senderProfile, moderationOpts, children,}: { message: ChatBskyConvoDefs.MessageView senderProfile?: bsky.profile.AnyProfileView moderationOpts: ModerationOpts | undefined children: TriggerProps['children']}): React.ReactNode => { const {t: l, i18n} = useLingui() const {currentAccount} = useSession() const convo = useConvoActive() const {openDeleteMessage, openReportMessage} = useMessageDialogs() const langPrefs = useLanguagePrefs() const translate = useGoogleTranslate()
const isFromSelf = message.sender?.did === currentAccount?.did const isGroupChatEnabled = true
const primaryMember = useMaybeProfileShadow(convo.convo.primaryMember) const reactionsAvailable = canReact({ convoState: convo, primaryMember, moderationOpts, })
const onCopyMessage = useCallback(() => { const str = richTextToString( new RichText({ text: message.text, facets: message.facets, }), true, )
void Clipboard.setStringAsync(str) Toast.show(l`Copied to clipboard`, { type: 'success', }) }, [l, message.text, message.facets])
const onPressTranslateMessage = useCallback(() => { void translate(message.text, langPrefs.primaryLanguage)
}, [langPrefs.primaryLanguage, message.text, translate])
const onEmojiSelect = useCallback( (emoji: string) => { if ( message.reactions?.find( reaction => reaction.value === emoji && reaction.sender.did === currentAccount?.did, ) ) { convo .removeReaction(message.id, emoji) .catch(() => Toast.show(l`Failed to remove emoji reaction`)) } else { if (hasReachedReactionLimit(message, currentAccount?.did)) { Toast.show( l`You cannot add more than ${plural(EMOJI_REACTION_LIMIT, { one: '# emoji reaction', other: '# emoji reactions', })}`, { type: 'info', }, ) return } convo.addReaction(message.id, emoji).catch(() => Toast.show(l`Failed to add emoji reaction`, { type: 'error', }), ) } }, [l, convo, message, currentAccount?.did], )
const sender = senderProfile
return ( <ContextMenu.Root> {IS_NATIVE && reactionsAvailable && ( <ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'} style={[isFromSelf && isGroupChatEnabled ? null : a.ml_sm]}> <EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect} /> </ContextMenu.AuxiliaryView> )}
<ContextMenu.Trigger label={l`Message options`} contentLabel={l`Message from @${ sender?.handle ?? 'unknown' // should always be defined }: ${message.text}`}> {children} </ContextMenu.Trigger>
<ContextMenu.Outer align={isFromSelf ? 'right' : 'left'} label={l`Sent at ${i18n.date(new Date(message.sentAt), { timeStyle: 'short', })}`} style={[isFromSelf && isGroupChatEnabled ? null : a.ml_sm]}> {message.text.length > 0 && ( <> <ContextMenu.Item testID="messageDropdownTranslateBtn" label={l`Translate`} onPress={onPressTranslateMessage}> <ContextMenu.ItemIcon icon={LanguageIcon} position="left" /> <ContextMenu.ItemText>{l`Translate`}</ContextMenu.ItemText> </ContextMenu.Item> <ContextMenu.Item testID="messageDropdownCopyBtn" label={l`Copy message text`} onPress={onCopyMessage}> <ContextMenu.ItemIcon icon={ClipboardIcon} position="left" /> <ContextMenu.ItemText> {l`Copy message text`} </ContextMenu.ItemText> </ContextMenu.Item> </> )} <ContextMenu.Item destructive testID="messageDropdownDeleteBtn" label={l`Delete message for me`} onPress={() => openDeleteMessage(message)}> <ContextMenu.ItemIcon icon={TrashIcon} position="left" /> <ContextMenu.ItemText>{l`Delete for me`}</ContextMenu.ItemText> </ContextMenu.Item> {!isFromSelf && ( <ContextMenu.Item destructive testID="messageDropdownReportBtn" label={l`Report message`} onPress={() => openReportMessage(message, senderProfile)}> <ContextMenu.ItemIcon icon={FlagIcon} position="left" /> <ContextMenu.ItemText>{l`Report`}</ContextMenu.ItemText> </ContextMenu.Item> )} </ContextMenu.Outer> </ContextMenu.Root> )}MessageContextMenu = memo(MessageContextMenu)