Bluesky app fork with some witchin' additions 馃挮
at main 346 lines 9.8 kB view raw
1import {useCallback} from 'react' 2import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api' 3import {msg} from '@lingui/core/macro' 4import {useLingui} from '@lingui/react' 5import {Trans} from '@lingui/react/macro' 6import {StackActions, useNavigation} from '@react-navigation/native' 7import {useQueryClient} from '@tanstack/react-query' 8 9import {type NavigationProp} from '#/lib/routes/types' 10import {useProfileShadow} from '#/state/cache/profile-shadow' 11import {useEmail} from '#/state/email-verification' 12import {useAcceptConversation} from '#/state/queries/messages/accept-conversation' 13import {precacheConvoQuery} from '#/state/queries/messages/conversation' 14import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' 15import { 16 unstableCacheProfileView, 17 useProfileBlockMutationQueue, 18} from '#/state/queries/profile' 19import * as Toast from '#/view/com/util/Toast' 20import {atoms as a} from '#/alf' 21import { 22 Button, 23 ButtonIcon, 24 type ButtonProps, 25 ButtonText, 26} from '#/components/Button' 27import {useDialogControl} from '#/components/Dialog' 28import { 29 EmailDialogScreenID, 30 useEmailDialogControl, 31} from '#/components/dialogs/EmailDialog' 32import {AfterReportDialog} from '#/components/dms/AfterReportDialog' 33import {CircleX_Stroke2_Corner0_Rounded} from '#/components/icons/CircleX' 34import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag' 35import {PersonX_Stroke2_Corner0_Rounded as PersonXIcon} from '#/components/icons/Person' 36import {Loader} from '#/components/Loader' 37import * as Menu from '#/components/Menu' 38import {ReportDialog} from '#/components/moderation/ReportDialog' 39 40export function RejectMenu({ 41 convo, 42 profile, 43 size = 'tiny', 44 color = 'secondary', 45 label, 46 showDeleteConvo, 47 currentScreen, 48 ...props 49}: Omit<ButtonProps, 'onPress' | 'children' | 'label'> & { 50 label?: string 51 convo: ChatBskyConvoDefs.ConvoView 52 profile: ChatBskyActorDefs.ProfileViewBasic 53 showDeleteConvo?: boolean 54 currentScreen: 'list' | 'conversation' 55}) { 56 const {_} = useLingui() 57 const shadowedProfile = useProfileShadow(profile) 58 const navigation = useNavigation<NavigationProp>() 59 const queryClient = useQueryClient() 60 61 const {mutate: leaveConvo} = useLeaveConvo(convo.id, { 62 onMutate: () => { 63 if (currentScreen === 'conversation') { 64 navigation.dispatch(StackActions.pop()) 65 } 66 }, 67 onError: () => { 68 Toast.show( 69 _( 70 msg({ 71 context: 'toast', 72 message: 'Failed to delete chat', 73 }), 74 ), 75 'xmark', 76 ) 77 }, 78 }) 79 const [queueBlock] = useProfileBlockMutationQueue(shadowedProfile) 80 81 const onPressDelete = useCallback(() => { 82 Toast.show( 83 _( 84 msg({ 85 context: 'toast', 86 message: 'Chat deleted', 87 }), 88 ), 89 'check', 90 ) 91 leaveConvo() 92 }, [leaveConvo, _]) 93 94 const onPressBlock = useCallback(() => { 95 Toast.show( 96 _( 97 msg({ 98 context: 'toast', 99 message: 'Account blocked', 100 }), 101 ), 102 'check', 103 ) 104 // block and also delete convo 105 queueBlock() 106 leaveConvo() 107 }, [queueBlock, leaveConvo, _]) 108 109 const reportControl = useDialogControl() 110 const blockOrDeleteControl = useDialogControl() 111 112 const lastMessage = ChatBskyConvoDefs.isMessageView(convo.lastMessage) 113 ? convo.lastMessage 114 : null 115 116 return ( 117 <> 118 <Menu.Root> 119 <Menu.Trigger label={_(msg`Reject chat request`)}> 120 {({props: triggerProps}) => ( 121 <Button 122 {...triggerProps} 123 {...props} 124 label={triggerProps.accessibilityLabel} 125 style={[a.flex_1]} 126 color={color} 127 size={size}> 128 <ButtonText> 129 {label || ( 130 <Trans comment="Reject a chat request, this opens a menu with options"> 131 Reject 132 </Trans> 133 )} 134 </ButtonText> 135 </Button> 136 )} 137 </Menu.Trigger> 138 <Menu.Outer showCancel> 139 <Menu.Group> 140 {showDeleteConvo && ( 141 <Menu.Item 142 label={_(msg`Delete conversation`)} 143 onPress={onPressDelete}> 144 <Menu.ItemText> 145 <Trans>Delete conversation</Trans> 146 </Menu.ItemText> 147 <Menu.ItemIcon icon={CircleX_Stroke2_Corner0_Rounded} /> 148 </Menu.Item> 149 )} 150 <Menu.Item label={_(msg`Block account`)} onPress={onPressBlock}> 151 <Menu.ItemText> 152 <Trans>Block account</Trans> 153 </Menu.ItemText> 154 <Menu.ItemIcon icon={PersonXIcon} /> 155 </Menu.Item> 156 {/* note: last message will almost certainly be defined, since you can't 157 delete messages for other people andit's impossible for a convo on this 158 screen to have a message sent by you */} 159 {lastMessage && ( 160 <Menu.Item 161 label={_(msg`Report conversation`)} 162 onPress={reportControl.open}> 163 <Menu.ItemText> 164 <Trans>Report conversation</Trans> 165 </Menu.ItemText> 166 <Menu.ItemIcon icon={FlagIcon} /> 167 </Menu.Item> 168 )} 169 </Menu.Group> 170 </Menu.Outer> 171 </Menu.Root> 172 {lastMessage && ( 173 <> 174 <ReportDialog 175 subject={{ 176 view: 'convo', 177 convoId: convo.id, 178 message: lastMessage, 179 }} 180 control={reportControl} 181 onAfterSubmit={() => { 182 const sender = convo.members.find( 183 member => member.did === lastMessage.sender.did, 184 ) 185 if (sender) { 186 unstableCacheProfileView(queryClient, sender) 187 } 188 blockOrDeleteControl.open() 189 }} 190 /> 191 <AfterReportDialog 192 control={blockOrDeleteControl} 193 currentScreen={currentScreen} 194 params={{ 195 convoId: convo.id, 196 message: lastMessage, 197 }} 198 /> 199 </> 200 )} 201 </> 202 ) 203} 204 205export function AcceptChatButton({ 206 convo, 207 size = 'tiny', 208 color = 'secondary_inverted', 209 label, 210 currentScreen, 211 onAcceptConvo, 212 ...props 213}: Omit<ButtonProps, 'onPress' | 'children' | 'label'> & { 214 label?: string 215 convo: ChatBskyConvoDefs.ConvoView 216 onAcceptConvo?: () => void 217 currentScreen: 'list' | 'conversation' 218}) { 219 const {_} = useLingui() 220 const queryClient = useQueryClient() 221 const navigation = useNavigation<NavigationProp>() 222 const {needsEmailVerification} = useEmail() 223 const emailDialogControl = useEmailDialogControl() 224 225 const {mutate: acceptConvo, isPending} = useAcceptConversation(convo.id, { 226 onMutate: () => { 227 onAcceptConvo?.() 228 if (currentScreen === 'list') { 229 precacheConvoQuery(queryClient, {...convo, status: 'accepted'}) 230 navigation.navigate('MessagesConversation', { 231 conversation: convo.id, 232 accept: true, 233 }) 234 } 235 }, 236 onError: () => { 237 // Should we show a toast here? They'll be on the convo screen, and it'll make 238 // no difference if the request failed - when they send a message, the convo will be accepted 239 // automatically. The only difference is that when they back out of the convo (without sending a message), the conversation will be rejected. 240 // the list will still have this chat in it -sfn 241 Toast.show( 242 _( 243 msg({ 244 context: 'toast', 245 message: 'Failed to accept chat', 246 }), 247 ), 248 'xmark', 249 ) 250 }, 251 }) 252 253 const onPressAccept = useCallback(() => { 254 if (needsEmailVerification) { 255 emailDialogControl.open({ 256 id: EmailDialogScreenID.Verify, 257 instructions: [ 258 <Trans key="request-btn"> 259 Before you can accept this chat request, you must first verify your 260 email. 261 </Trans>, 262 ], 263 }) 264 } else { 265 acceptConvo() 266 } 267 }, [acceptConvo, needsEmailVerification, emailDialogControl]) 268 269 return ( 270 <Button 271 {...props} 272 label={label || _(msg`Accept chat request`)} 273 size={size} 274 color={color} 275 style={a.flex_1} 276 onPress={onPressAccept}> 277 {isPending ? ( 278 <ButtonIcon icon={Loader} /> 279 ) : ( 280 <ButtonText> 281 {label || <Trans comment="Accept a chat request">Accept</Trans>} 282 </ButtonText> 283 )} 284 </Button> 285 ) 286} 287 288export function DeleteChatButton({ 289 convo, 290 size = 'tiny', 291 color = 'secondary', 292 label, 293 currentScreen, 294 ...props 295}: Omit<ButtonProps, 'children' | 'label'> & { 296 label?: string 297 convo: ChatBskyConvoDefs.ConvoView 298 currentScreen: 'list' | 'conversation' 299}) { 300 const {_} = useLingui() 301 const navigation = useNavigation<NavigationProp>() 302 303 const {mutate: leaveConvo} = useLeaveConvo(convo.id, { 304 onMutate: () => { 305 if (currentScreen === 'conversation') { 306 navigation.dispatch(StackActions.pop()) 307 } 308 }, 309 onError: () => { 310 Toast.show( 311 _( 312 msg({ 313 context: 'toast', 314 message: 'Failed to delete chat', 315 }), 316 ), 317 'xmark', 318 ) 319 }, 320 }) 321 322 const onPressDelete = useCallback(() => { 323 Toast.show( 324 _( 325 msg({ 326 context: 'toast', 327 message: 'Chat deleted', 328 }), 329 ), 330 'check', 331 ) 332 leaveConvo() 333 }, [leaveConvo, _]) 334 335 return ( 336 <Button 337 label={label || _(msg`Delete chat`)} 338 size={size} 339 color={color} 340 style={a.flex_1} 341 onPress={onPressDelete} 342 {...props}> 343 <ButtonText>{label || <Trans>Delete chat</Trans>}</ButtonText> 344 </Button> 345 ) 346}