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