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