Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 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}