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

Configure Feed

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

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