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