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