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 * 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}