forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useState} from 'react'
2import {Pressable} from 'react-native'
3import {Trans, useLingui} from '@lingui/react/macro'
4import {useNavigation} from '@react-navigation/native'
5
6import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
7import {type NavigationProp} from '#/lib/routes/types'
8import {logger} from '#/logger'
9import {type Shadow} from '#/state/cache/types'
10import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
11import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
12import {useRemoveFromGroupChat} from '#/state/queries/messages/remove-from-group'
13import {useProfileBlockMutationQueue} from '#/state/queries/profile'
14import {atoms as a, useTheme} from '#/alf'
15import {type ConvoWithDetails} from '#/components/dms/util'
16import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
17import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
18import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
19import {
20 Person_Stroke2_Corner2_Rounded as PersonIcon,
21 PersonX_Stroke2_Corner0_Rounded as PersonXIcon,
22} from '#/components/icons/Person'
23import * as Menu from '#/components/Menu'
24import * as Prompt from '#/components/Prompt'
25import * as Toast from '#/components/Toast'
26import {useAnalytics} from '#/analytics'
27import type * as bsky from '#/types/bsky'
28import {BlockMemberPrompt} from './prompts'
29import {StatusBadge} from './StatusBadge'
30
31export function MemberMenu({
32 convo,
33 profile,
34 displayName,
35 type,
36 isOwner,
37}: {
38 convo: ConvoWithDetails
39 profile: Shadow<bsky.profile.AnyProfileView>
40 type: 'owner' | 'standard' | 'invited'
41 displayName: string
42 isOwner: boolean
43}) {
44 const navigation = useNavigation<NavigationProp>()
45 const t = useTheme()
46 const {t: l} = useLingui()
47 const ax = useAnalytics()
48
49 const requireEmailVerification = useRequireEmailVerification()
50
51 const blockMemberPrompt = Prompt.usePromptControl()
52
53 const [menuDidOpen, setMenuDidOpen] = useState(false)
54 const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did, {
55 enabled: menuDidOpen,
56 })
57 const {mutate: initiateConvo} = useGetConvoForMembers({
58 onSuccess: ({convo}) => {
59 ax.metric('chat:open', {logContext: 'ConvoSettings'})
60 navigation.navigate('MessagesConversation', {conversation: convo.id})
61 },
62 onError: () => {
63 Toast.show(l`Failed to create conversation`, {type: 'error'})
64 },
65 })
66 const convoId = convo.view.id
67 const {mutate: removeMembers} = useRemoveFromGroupChat(convoId, {
68 onError: e => {
69 logger.error('Failed to remove group chat member', {message: e})
70 Toast.show(l`Failed to remove group chat member`, {type: 'error'})
71 },
72 })
73 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
74
75 const messageMember = () => {
76 if (!convoAvailability?.canChat) {
77 return
78 }
79
80 if (convoAvailability.convo) {
81 ax.metric('chat:open', {logContext: 'ConvoSettings'})
82 navigation.navigate('MessagesConversation', {
83 conversation: convoAvailability.convo.id,
84 })
85 } else {
86 ax.metric('chat:create', {logContext: 'ConvoSettings'})
87 initiateConvo([profile.did])
88 }
89 }
90
91 const handleMessageMember = requireEmailVerification(messageMember, {
92 instructions: [
93 <Trans key="message">
94 Before you can message another user, you must first verify your email.
95 </Trans>,
96 ],
97 })
98
99 const handleBlockMember = async () => {
100 if (profile.viewer?.blocking) {
101 try {
102 await queueUnblock()
103 Toast.show(l({message: 'Account unblocked', context: 'toast'}))
104 } catch (err) {
105 const e = err as Error
106 if (e?.name !== 'AbortError') {
107 logger.error('Failed to unblock account', {message: e})
108 Toast.show(l`There was an issue! ${e.toString()}`, {
109 type: 'error',
110 })
111 }
112 }
113 } else {
114 try {
115 await queueBlock()
116 Toast.show(l({message: 'Account blocked', context: 'toast'}))
117 } catch (err) {
118 const e = err as Error
119 if (e?.name !== 'AbortError') {
120 logger.error('Failed to block account', {message: e})
121 Toast.show(l`There was an issue! ${e.toString()}`, {
122 type: 'error',
123 })
124 }
125 }
126 }
127 }
128
129 const canBlockMember = type === 'owner' || type === 'standard'
130 const canRemoveMember = isOwner && type !== 'invited'
131 // TODO Need to integrate this. -dsb
132 const canUninviteMember = false
133 // const canUninviteMember = isOwner && type === 'invited'
134
135 return (
136 <>
137 <Menu.Root>
138 <Menu.Trigger label={l`Open chat member options for ${displayName}`}>
139 {({props, state, control: menuControl}) => {
140 const isActive =
141 state.hovered || state.pressed || menuControl.isOpen
142 const triggerProps = {
143 ...props,
144 onPress: () => {
145 setMenuDidOpen(true)
146 props.onPress()
147 },
148 }
149 return type === 'owner' || type === 'invited' ? (
150 <StatusBadge
151 label={type === 'owner' ? l`Admin` : l`Invited`}
152 pressableProps={triggerProps}
153 style={[
154 isActive
155 ? {
156 backgroundColor: t.palette.contrast_0,
157 }
158 : null,
159 ]}
160 />
161 ) : (
162 <Pressable
163 {...triggerProps}
164 style={[
165 a.rounded_full,
166 a.p_sm,
167 isActive
168 ? {
169 backgroundColor: t.palette.contrast_0,
170 }
171 : null,
172 ]}>
173 <EllipsisIcon
174 style={[t.atoms.text_contrast_medium]}
175 size="md"
176 />
177 </Pressable>
178 )
179 }}
180 </Menu.Trigger>
181 <Menu.Outer>
182 <Menu.Group>
183 <Menu.Item
184 label={l`View ${displayName}鈥檚 profile`}
185 onPress={() => {
186 navigation.navigate('Profile', {name: profile.did})
187 }}>
188 <Menu.ItemText>
189 <Trans>Go to profile</Trans>
190 </Menu.ItemText>
191 <Menu.ItemIcon icon={PersonIcon} />
192 </Menu.Item>
193 <Menu.Item
194 label={l`Message ${displayName}`}
195 onPress={handleMessageMember}>
196 <Menu.ItemText>
197 <Trans context="action">Message</Trans>
198 </Menu.ItemText>
199 <Menu.ItemIcon icon={MessageIcon} />
200 </Menu.Item>
201 </Menu.Group>
202 <Menu.Divider />
203 <Menu.Group>
204 {canBlockMember ? (
205 <Menu.Item
206 label={
207 profile.viewer?.blocking
208 ? l`Unblock ${displayName}`
209 : l`Block ${displayName}`
210 }
211 onPress={
212 profile.viewer?.blocking
213 ? handleBlockMember
214 : blockMemberPrompt.open
215 }>
216 <Menu.ItemText>
217 <Trans>Block</Trans>
218 </Menu.ItemText>
219 <Menu.ItemIcon icon={PersonXIcon} />
220 </Menu.Item>
221 ) : null}
222 {canRemoveMember ? (
223 <Menu.Item
224 label={l`Remove ${displayName} from this group chat`}
225 onPress={() => removeMembers({members: [profile.did]})}>
226 <Menu.ItemText>
227 <Trans>Remove from chat</Trans>
228 </Menu.ItemText>
229 <Menu.ItemIcon icon={ArrowBoxLeftIcon} />
230 </Menu.Item>
231 ) : null}
232 {canUninviteMember ? (
233 <Menu.Item
234 label={l`Uninvite ${displayName} from this group chat`}
235 // TODO Need to wire up the uninvite flow. -dsb
236 onPress={() => {}}>
237 <Menu.ItemText>
238 <Trans>Uninvite</Trans>
239 </Menu.ItemText>
240 <Menu.ItemIcon icon={ArrowBoxLeftIcon} />
241 </Menu.Item>
242 ) : null}
243 </Menu.Group>
244 </Menu.Outer>
245 </Menu.Root>
246 <BlockMemberPrompt
247 control={blockMemberPrompt}
248 onConfirm={() => void handleBlockMember()}
249 />
250 </>
251 )
252}