Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
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}