Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 307 lines 9.8 kB view raw
1import React, {useCallback} from 'react' 2import {Keyboard, View} from 'react-native' 3import {type ChatBskyConvoDefs, type ModerationCause} from '@atproto/api' 4import {msg} from '@lingui/core/macro' 5import {useLingui} from '@lingui/react' 6import {Trans} from '@lingui/react/macro' 7import {useNavigation} from '@react-navigation/native' 8import {useQueryClient} from '@tanstack/react-query' 9 10import {type NavigationProp} from '#/lib/routes/types' 11import {type Shadow} from '#/state/cache/types' 12import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 13import { 14 useConvoQuery, 15 useMarkAsReadMutation, 16} from '#/state/queries/messages/conversation' 17import {useMuteConvo} from '#/state/queries/messages/mute-conversation' 18import { 19 unstableCacheProfileView, 20 useProfileBlockMutationQueue, 21} from '#/state/queries/profile' 22import * as Toast from '#/view/com/util/Toast' 23import {type ViewStyleProp} from '#/alf' 24import {atoms as a} from '#/alf' 25import {Button, ButtonIcon} from '#/components/Button' 26import {AfterReportDialog} from '#/components/dms/AfterReportDialog' 27import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' 28import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' 29import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt' 30import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft' 31import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble' 32import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 33import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' 34import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' 35import { 36 Person_Stroke2_Corner0_Rounded as Person, 37 PersonCheck_Stroke2_Corner0_Rounded as PersonCheck, 38 PersonX_Stroke2_Corner0_Rounded as PersonX, 39} from '#/components/icons/Person' 40import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 41import * as Menu from '#/components/Menu' 42import {ReportDialog} from '#/components/moderation/ReportDialog' 43import * as Prompt from '#/components/Prompt' 44import type * as bsky from '#/types/bsky' 45 46let ConvoMenu = ({ 47 convo, 48 profile, 49 control, 50 currentScreen, 51 showMarkAsRead, 52 hideTrigger, 53 blockInfo, 54 latestReportableMessage, 55 style, 56}: { 57 convo: ChatBskyConvoDefs.ConvoView 58 profile: Shadow<bsky.profile.AnyProfileView> 59 control?: Menu.MenuControlProps 60 currentScreen: 'list' | 'conversation' 61 showMarkAsRead?: boolean 62 hideTrigger?: boolean 63 blockInfo: { 64 listBlocks: ModerationCause[] 65 userBlock?: ModerationCause 66 } 67 latestReportableMessage?: ChatBskyConvoDefs.MessageView 68 style?: ViewStyleProp['style'] 69}): React.ReactNode => { 70 const {_} = useLingui() 71 const queryClient = useQueryClient() 72 73 const leaveConvoControl = Prompt.usePromptControl() 74 const reportControl = Prompt.usePromptControl() 75 const blockedByListControl = Prompt.usePromptControl() 76 const blockOrDeleteControl = Prompt.usePromptControl() 77 78 const {listBlocks} = blockInfo 79 80 const enableSquareButtons = useEnableSquareButtons() 81 82 return ( 83 <> 84 <Menu.Root control={control}> 85 {!hideTrigger && ( 86 <View style={[style]}> 87 <Menu.Trigger label={_(msg`Chat settings`)}> 88 {({props}) => ( 89 <Button 90 label={props.accessibilityLabel} 91 {...props} 92 onPress={() => { 93 Keyboard.dismiss() 94 props.onPress() 95 }} 96 size="small" 97 color="secondary" 98 shape={enableSquareButtons ? 'square' : 'round'} 99 variant="ghost" 100 style={[a.bg_transparent]}> 101 <ButtonIcon icon={DotsHorizontal} size="md" /> 102 </Button> 103 )} 104 </Menu.Trigger> 105 </View> 106 )} 107 108 <Menu.Outer> 109 <MenuContent 110 profile={profile} 111 showMarkAsRead={showMarkAsRead} 112 blockInfo={blockInfo} 113 convo={convo} 114 leaveConvoControl={leaveConvoControl} 115 reportControl={reportControl} 116 blockedByListControl={blockedByListControl} 117 /> 118 </Menu.Outer> 119 </Menu.Root> 120 121 <LeaveConvoPrompt 122 control={leaveConvoControl} 123 convoId={convo.id} 124 currentScreen={currentScreen} 125 /> 126 {latestReportableMessage ? ( 127 <> 128 <ReportDialog 129 subject={{ 130 view: 'convo', 131 convoId: convo.id, 132 message: latestReportableMessage, 133 }} 134 control={reportControl} 135 onAfterSubmit={() => { 136 const sender = convo.members.find( 137 member => member.did === latestReportableMessage.sender.did, 138 ) 139 if (sender) { 140 unstableCacheProfileView(queryClient, sender) 141 } 142 blockOrDeleteControl.open() 143 }} 144 /> 145 <AfterReportDialog 146 control={blockOrDeleteControl} 147 currentScreen={currentScreen} 148 params={{ 149 convoId: convo.id, 150 message: latestReportableMessage, 151 }} 152 /> 153 </> 154 ) : ( 155 <ReportConversationPrompt control={reportControl} /> 156 )} 157 158 <BlockedByListDialog 159 control={blockedByListControl} 160 listBlocks={listBlocks} 161 /> 162 </> 163 ) 164} 165ConvoMenu = React.memo(ConvoMenu) 166 167function MenuContent({ 168 convo: initialConvo, 169 profile, 170 showMarkAsRead, 171 blockInfo, 172 leaveConvoControl, 173 reportControl, 174 blockedByListControl, 175}: { 176 convo: ChatBskyConvoDefs.ConvoView 177 profile: Shadow<bsky.profile.AnyProfileView> 178 showMarkAsRead?: boolean 179 blockInfo: { 180 listBlocks: ModerationCause[] 181 userBlock?: ModerationCause 182 } 183 leaveConvoControl: Prompt.PromptControlProps 184 reportControl: Prompt.PromptControlProps 185 blockedByListControl: Prompt.PromptControlProps 186}) { 187 const navigation = useNavigation<NavigationProp>() 188 const {_} = useLingui() 189 const {mutate: markAsRead} = useMarkAsReadMutation() 190 191 const {listBlocks, userBlock} = blockInfo 192 const isBlocking = userBlock || !!listBlocks.length 193 const isDeletedAccount = profile.handle === 'missing.invalid' 194 195 const convoId = initialConvo.id 196 const {data: convo} = useConvoQuery(initialConvo) 197 198 const onNavigateToProfile = useCallback(() => { 199 navigation.navigate('Profile', {name: profile.did}) 200 }, [navigation, profile.did]) 201 202 const {mutate: muteConvo} = useMuteConvo(convoId, { 203 onSuccess: data => { 204 if (data.convo.muted) { 205 Toast.show(_(msg({message: 'Chat muted', context: 'toast'}))) 206 } else { 207 Toast.show(_(msg({message: 'Chat unmuted', context: 'toast'}))) 208 } 209 }, 210 onError: () => { 211 Toast.show(_(msg`Could not mute chat`), 'xmark') 212 }, 213 }) 214 215 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) 216 217 const toggleBlock = React.useCallback(() => { 218 if (listBlocks.length) { 219 blockedByListControl.open() 220 return 221 } 222 223 if (userBlock) { 224 queueUnblock() 225 } else { 226 queueBlock() 227 } 228 }, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock]) 229 230 return isDeletedAccount ? ( 231 <Menu.Item 232 label={_(msg`Leave conversation`)} 233 onPress={() => leaveConvoControl.open()}> 234 <Menu.ItemText> 235 <Trans>Leave conversation</Trans> 236 </Menu.ItemText> 237 <Menu.ItemIcon icon={ArrowBoxLeft} /> 238 </Menu.Item> 239 ) : ( 240 <> 241 <Menu.Group> 242 {showMarkAsRead && ( 243 <Menu.Item 244 label={_(msg`Mark as read`)} 245 onPress={() => markAsRead({convoId})}> 246 <Menu.ItemText> 247 <Trans>Mark as read</Trans> 248 </Menu.ItemText> 249 <Menu.ItemIcon icon={Bubble} /> 250 </Menu.Item> 251 )} 252 <Menu.Item 253 label={_(msg`Go to user's profile`)} 254 onPress={onNavigateToProfile}> 255 <Menu.ItemText> 256 <Trans>Go to profile</Trans> 257 </Menu.ItemText> 258 <Menu.ItemIcon icon={Person} /> 259 </Menu.Item> 260 <Menu.Item 261 label={_(msg`Mute conversation`)} 262 onPress={() => muteConvo({mute: !convo?.muted})}> 263 <Menu.ItemText> 264 {convo?.muted ? ( 265 <Trans>Unmute conversation</Trans> 266 ) : ( 267 <Trans>Mute conversation</Trans> 268 )} 269 </Menu.ItemText> 270 <Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} /> 271 </Menu.Item> 272 </Menu.Group> 273 <Menu.Divider /> 274 <Menu.Group> 275 <Menu.Item 276 label={isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)} 277 onPress={toggleBlock}> 278 <Menu.ItemText> 279 {isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)} 280 </Menu.ItemText> 281 <Menu.ItemIcon icon={isBlocking ? PersonCheck : PersonX} /> 282 </Menu.Item> 283 <Menu.Item 284 label={_(msg`Report conversation`)} 285 onPress={() => reportControl.open()}> 286 <Menu.ItemText> 287 <Trans>Report conversation</Trans> 288 </Menu.ItemText> 289 <Menu.ItemIcon icon={Flag} /> 290 </Menu.Item> 291 </Menu.Group> 292 <Menu.Divider /> 293 <Menu.Group> 294 <Menu.Item 295 label={_(msg`Leave conversation`)} 296 onPress={() => leaveConvoControl.open()}> 297 <Menu.ItemText> 298 <Trans>Leave conversation</Trans> 299 </Menu.ItemText> 300 <Menu.ItemIcon icon={ArrowBoxLeft} /> 301 </Menu.Item> 302 </Menu.Group> 303 </> 304 ) 305} 306 307export {ConvoMenu}