forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useRef, useState} from 'react'
2import {Pressable, View} from 'react-native'
3import {type ChatBskyConvoDefs} from '@atproto/api'
4import {msg} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6
7import {useConvoActive} from '#/state/messages/convo'
8import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
9import {useSession} from '#/state/session'
10import * as Toast from '#/view/com/util/Toast'
11import {atoms as a, useTheme} from '#/alf'
12import {MessageContextMenu} from '#/components/dms/MessageContextMenu'
13import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
14import {EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
15import {EmojiReactionPicker} from './EmojiReactionPicker'
16import {hasReachedReactionLimit} from './util'
17
18export function ActionsWrapper({
19 message,
20 isFromSelf,
21 children,
22}: {
23 message: ChatBskyConvoDefs.MessageView
24 isFromSelf: boolean
25 children: React.ReactNode
26}) {
27 const viewRef = useRef(null)
28 const t = useTheme()
29 const {_} = useLingui()
30 const convo = useConvoActive()
31 const {currentAccount} = useSession()
32
33 const [showActions, setShowActions] = useState(false)
34
35 const enableSquareButtons = useEnableSquareButtons()
36
37 const onMouseEnter = useCallback(() => {
38 setShowActions(true)
39 }, [])
40
41 const onMouseLeave = useCallback(() => {
42 setShowActions(false)
43 }, [])
44
45 // We need to handle the `onFocus` separately because we want to know if there is a related target (the element
46 // that is losing focus). If there isn't that means the focus is coming from a dropdown that is now closed.
47 const onFocus = useCallback<React.FocusEventHandler>(e => {
48 if (e.nativeEvent.relatedTarget == null) return
49 setShowActions(true)
50 }, [])
51
52 const onEmojiSelect = useCallback(
53 (emoji: string) => {
54 if (
55 message.reactions?.find(
56 reaction =>
57 reaction.value === emoji &&
58 reaction.sender.did === currentAccount?.did,
59 )
60 ) {
61 convo
62 .removeReaction(message.id, emoji)
63 .catch(() => Toast.show(_(msg`Failed to remove emoji reaction`)))
64 } else {
65 if (hasReachedReactionLimit(message, currentAccount?.did)) return
66 convo
67 .addReaction(message.id, emoji)
68 .catch(() =>
69 Toast.show(_(msg`Failed to add emoji reaction`), 'xmark'),
70 )
71 }
72 },
73 [_, convo, message, currentAccount?.did],
74 )
75
76 return (
77 <View
78 onMouseEnter={onMouseEnter}
79 onMouseLeave={onMouseLeave}
80 // @ts-expect-error web only
81 onFocus={onFocus}
82 onBlur={onMouseLeave}
83 style={[a.flex_1, isFromSelf ? a.flex_row : a.flex_row_reverse]}
84 ref={viewRef}>
85 <View
86 style={[
87 a.justify_center,
88 a.flex_row,
89 a.align_center,
90 isFromSelf
91 ? [a.mr_xs, {marginLeft: 'auto'}, a.flex_row_reverse]
92 : [a.ml_xs, {marginRight: 'auto'}],
93 ]}>
94 <EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}>
95 {({props, state, isNative, control}) => {
96 // always false, file is platform split
97 if (isNative) return null
98 const showMenuTrigger = showActions || control.isOpen ? 1 : 0
99 return (
100 <Pressable
101 {...props}
102 style={[
103 {opacity: showMenuTrigger},
104 a.p_xs,
105 enableSquareButtons ? a.rounded_sm : a.rounded_full,
106 (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
107 ]}>
108 <EmojiSmileIcon
109 size="md"
110 style={t.atoms.text_contrast_medium}
111 />
112 </Pressable>
113 )
114 }}
115 </EmojiReactionPicker>
116 <MessageContextMenu message={message}>
117 {({props, state, isNative, control}) => {
118 // always false, file is platform split
119 if (isNative) return null
120 const showMenuTrigger = showActions || control.isOpen ? 1 : 0
121 return (
122 <Pressable
123 {...props}
124 style={[
125 {opacity: showMenuTrigger},
126 a.p_xs,
127 enableSquareButtons ? a.rounded_sm : a.rounded_full,
128 (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
129 ]}>
130 <DotsHorizontalIcon
131 size="md"
132 style={t.atoms.text_contrast_medium}
133 />
134 </Pressable>
135 )
136 }}
137 </MessageContextMenu>
138 </View>
139 <View
140 style={[{maxWidth: '80%'}, isFromSelf ? a.align_end : a.align_start]}>
141 {children}
142 </View>
143 </View>
144 )
145}