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