forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}