Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 140 lines 4.5 kB view raw
1import {useMemo, useState} from 'react' 2import {useWindowDimensions, View} from 'react-native' 3import {type ChatBskyConvoDefs} from '@atproto/api' 4import {msg} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 8import {useSession} from '#/state/session' 9import {atoms as a, tokens, useTheme} from '#/alf' 10import * as ContextMenu from '#/components/ContextMenu' 11import { 12 useContextMenuContext, 13 useContextMenuMenuContext, 14} from '#/components/ContextMenu/context' 15import { 16 EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyesIcon, 17 EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon, 18} from '#/components/icons/Emoji' 19import {type TriggerProps} from '#/components/Menu/types' 20import {Text} from '#/components/Typography' 21import {EmojiPopup} from './EmojiPopup' 22import {hasAlreadyReacted, hasReachedReactionLimit} from './util' 23 24export function EmojiReactionPicker({ 25 message, 26 onEmojiSelect, 27}: { 28 message: ChatBskyConvoDefs.MessageView 29 children?: TriggerProps['children'] 30 onEmojiSelect: (emoji: string) => void 31}) { 32 const {_} = useLingui() 33 const {currentAccount} = useSession() 34 const t = useTheme() 35 const isFromSelf = message.sender?.did === currentAccount?.did 36 const {measurement, close} = useContextMenuContext() 37 const {align} = useContextMenuMenuContext() 38 const [layout, setLayout] = useState({width: 0, height: 0}) 39 const {width: screenWidth} = useWindowDimensions() 40 41 // 1 in 100 chance of showing heart eyes icon 42 const EmojiIcon = useMemo(() => { 43 return Math.random() < 0.01 ? EmojiHeartEyesIcon : EmojiSmileIcon 44 }, []) 45 46 const position = useMemo(() => { 47 return { 48 x: align === 'left' ? 12 : screenWidth - layout.width - 12, 49 y: (measurement?.y ?? 0) - tokens.space.xs - layout.height, 50 height: layout.height, 51 width: layout.width, 52 } 53 }, [measurement, align, screenWidth, layout]) 54 55 const limitReacted = hasReachedReactionLimit(message, currentAccount?.did) 56 57 const bgColor = t.scheme === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25 58 59 const enableSquareButtons = useEnableSquareButtons() 60 61 return ( 62 <View 63 onLayout={evt => setLayout(evt.nativeEvent.layout)} 64 style={[ 65 bgColor, 66 enableSquareButtons ? a.rounded_sm : a.rounded_full, 67 a.absolute, 68 {bottom: '100%'}, 69 isFromSelf ? a.right_0 : a.left_0, 70 a.flex_row, 71 a.p_xs, 72 a.gap_xs, 73 a.mb_xs, 74 a.z_20, 75 a.border, 76 t.atoms.border_contrast_low, 77 a.shadow_md, 78 ]}> 79 {['馃憤', '馃槅', '鉂わ笍', '馃憖', '馃槩'].map(emoji => { 80 const alreadyReacted = hasAlreadyReacted( 81 message, 82 currentAccount?.did, 83 emoji, 84 ) 85 return ( 86 <ContextMenu.Item 87 position={position} 88 label={_(msg`React with ${emoji}`)} 89 key={emoji} 90 onPress={() => onEmojiSelect(emoji)} 91 unstyled 92 disabled={limitReacted ? !alreadyReacted : false}> 93 {hovered => ( 94 <View 95 style={[ 96 enableSquareButtons ? a.rounded_sm : a.rounded_full, 97 hovered 98 ? { 99 backgroundColor: alreadyReacted 100 ? t.palette.negative_100 101 : t.palette.primary_500, 102 } 103 : alreadyReacted 104 ? {backgroundColor: t.palette.primary_200} 105 : bgColor, 106 {height: 40, width: 40}, 107 a.justify_center, 108 a.align_center, 109 ]}> 110 <Text style={[a.text_center, {fontSize: 30}]} emoji> 111 {emoji} 112 </Text> 113 </View> 114 )} 115 </ContextMenu.Item> 116 ) 117 })} 118 <EmojiPopup 119 onEmojiSelected={emoji => { 120 close() 121 onEmojiSelect(emoji) 122 }}> 123 <View 124 style={[ 125 enableSquareButtons ? a.rounded_sm : a.rounded_full, 126 t.scheme === 'light' 127 ? t.atoms.bg_contrast_25 128 : t.atoms.bg_contrast_50, 129 {height: 40, width: 40}, 130 a.justify_center, 131 a.align_center, 132 a.border, 133 t.atoms.border_contrast_low, 134 ]}> 135 <EmojiIcon size="xl" fill={t.palette.contrast_400} /> 136 </View> 137 </EmojiPopup> 138 </View> 139 ) 140}