Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 252 lines 8.6 kB view raw
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}