Bluesky app fork with some witchin' additions 馃挮
at main 145 lines 4.9 kB view raw
1import {useCallback, useRef, useState} from 'react' 2import {Pressable, View} from 'react-native' 3import {type ChatBskyConvoDefs} from '@atproto/api' 4import {msg} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import {useConvoActive} from '#/state/messages/convo' 8import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 9import {useSession} from '#/state/session' 10import * as Toast from '#/view/com/util/Toast' 11import {atoms as a, useTheme} from '#/alf' 12import {MessageContextMenu} from '#/components/dms/MessageContextMenu' 13import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid' 14import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji' 15import {EmojiReactionPicker} from './EmojiReactionPicker' 16import {hasReachedReactionLimit} from './util' 17 18export function ActionsWrapper({ 19 message, 20 isFromSelf, 21 children, 22}: { 23 message: ChatBskyConvoDefs.MessageView 24 isFromSelf: boolean 25 children: React.ReactNode 26}) { 27 const viewRef = useRef(null) 28 const t = useTheme() 29 const {_} = useLingui() 30 const convo = useConvoActive() 31 const {currentAccount} = useSession() 32 33 const [showActions, setShowActions] = useState(false) 34 35 const enableSquareButtons = useEnableSquareButtons() 36 37 const onMouseEnter = useCallback(() => { 38 setShowActions(true) 39 }, []) 40 41 const onMouseLeave = useCallback(() => { 42 setShowActions(false) 43 }, []) 44 45 // We need to handle the `onFocus` separately because we want to know if there is a related target (the element 46 // that is losing focus). If there isn't that means the focus is coming from a dropdown that is now closed. 47 const onFocus = useCallback<React.FocusEventHandler>(e => { 48 if (e.nativeEvent.relatedTarget == null) return 49 setShowActions(true) 50 }, []) 51 52 const onEmojiSelect = useCallback( 53 (emoji: string) => { 54 if ( 55 message.reactions?.find( 56 reaction => 57 reaction.value === emoji && 58 reaction.sender.did === currentAccount?.did, 59 ) 60 ) { 61 convo 62 .removeReaction(message.id, emoji) 63 .catch(() => Toast.show(_(msg`Failed to remove emoji reaction`))) 64 } else { 65 if (hasReachedReactionLimit(message, currentAccount?.did)) return 66 convo 67 .addReaction(message.id, emoji) 68 .catch(() => 69 Toast.show(_(msg`Failed to add emoji reaction`), 'xmark'), 70 ) 71 } 72 }, 73 [_, convo, message, currentAccount?.did], 74 ) 75 76 return ( 77 <View 78 onMouseEnter={onMouseEnter} 79 onMouseLeave={onMouseLeave} 80 // @ts-expect-error web only 81 onFocus={onFocus} 82 onBlur={onMouseLeave} 83 style={[a.flex_1, isFromSelf ? a.flex_row : a.flex_row_reverse]} 84 ref={viewRef}> 85 <View 86 style={[ 87 a.justify_center, 88 a.flex_row, 89 a.align_center, 90 isFromSelf 91 ? [a.mr_xs, {marginLeft: 'auto'}, a.flex_row_reverse] 92 : [a.ml_xs, {marginRight: 'auto'}], 93 ]}> 94 <EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}> 95 {({props, state, isNative, control}) => { 96 // always false, file is platform split 97 if (isNative) return null 98 const showMenuTrigger = showActions || control.isOpen ? 1 : 0 99 return ( 100 <Pressable 101 {...props} 102 style={[ 103 {opacity: showMenuTrigger}, 104 a.p_xs, 105 enableSquareButtons ? a.rounded_sm : a.rounded_full, 106 (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 107 ]}> 108 <EmojiSmileIcon 109 size="md" 110 style={t.atoms.text_contrast_medium} 111 /> 112 </Pressable> 113 ) 114 }} 115 </EmojiReactionPicker> 116 <MessageContextMenu message={message}> 117 {({props, state, isNative, control}) => { 118 // always false, file is platform split 119 if (isNative) return null 120 const showMenuTrigger = showActions || control.isOpen ? 1 : 0 121 return ( 122 <Pressable 123 {...props} 124 style={[ 125 {opacity: showMenuTrigger}, 126 a.p_xs, 127 enableSquareButtons ? a.rounded_sm : a.rounded_full, 128 (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 129 ]}> 130 <DotsHorizontalIcon 131 size="md" 132 style={t.atoms.text_contrast_medium} 133 /> 134 </Pressable> 135 ) 136 }} 137 </MessageContextMenu> 138 </View> 139 <View 140 style={[{maxWidth: '80%'}, isFromSelf ? a.align_end : a.align_start]}> 141 {children} 142 </View> 143 </View> 144 ) 145}