forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React, {useCallback} from 'react'
2import {Keyboard, View} from 'react-native'
3import {type ChatBskyConvoDefs, type ModerationCause} from '@atproto/api'
4import {msg} from '@lingui/core/macro'
5import {useLingui} from '@lingui/react'
6import {Trans} from '@lingui/react/macro'
7import {useNavigation} from '@react-navigation/native'
8import {useQueryClient} from '@tanstack/react-query'
9
10import {type NavigationProp} from '#/lib/routes/types'
11import {type Shadow} from '#/state/cache/types'
12import {
13 useConvoQuery,
14 useMarkAsReadMutation,
15} from '#/state/queries/messages/conversation'
16import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
17import {
18 unstableCacheProfileView,
19 useProfileBlockMutationQueue,
20} from '#/state/queries/profile'
21import * as Toast from '#/view/com/util/Toast'
22import {type ViewStyleProp} from '#/alf'
23import {atoms as a} from '#/alf'
24import {Button, ButtonIcon} from '#/components/Button'
25import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
26import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
27import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
28import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
29import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft'
30import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
31import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
32import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
33import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
34import {
35 Person_Stroke2_Corner0_Rounded as Person,
36 PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
37 PersonX_Stroke2_Corner0_Rounded as PersonX,
38} from '#/components/icons/Person'
39import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
40import * as Menu from '#/components/Menu'
41import {ReportDialog} from '#/components/moderation/ReportDialog'
42import * as Prompt from '#/components/Prompt'
43import type * as bsky from '#/types/bsky'
44
45let ConvoMenu = ({
46 convo,
47 profile,
48 control,
49 currentScreen,
50 showMarkAsRead,
51 hideTrigger,
52 blockInfo,
53 latestReportableMessage,
54 style,
55}: {
56 convo: ChatBskyConvoDefs.ConvoView
57 profile: Shadow<bsky.profile.AnyProfileView>
58 control?: Menu.MenuControlProps
59 currentScreen: 'list' | 'conversation'
60 showMarkAsRead?: boolean
61 hideTrigger?: boolean
62 blockInfo: {
63 listBlocks: ModerationCause[]
64 userBlock?: ModerationCause
65 }
66 latestReportableMessage?: ChatBskyConvoDefs.MessageView
67 style?: ViewStyleProp['style']
68}): React.ReactNode => {
69 const {_} = useLingui()
70 const queryClient = useQueryClient()
71
72 const leaveConvoControl = Prompt.usePromptControl()
73 const reportControl = Prompt.usePromptControl()
74 const blockedByListControl = Prompt.usePromptControl()
75 const blockOrDeleteControl = Prompt.usePromptControl()
76
77 const {listBlocks} = blockInfo
78
79 return (
80 <>
81 <Menu.Root control={control}>
82 {!hideTrigger && (
83 <View style={[style]}>
84 <Menu.Trigger label={_(msg`Chat settings`)}>
85 {({props}) => (
86 <Button
87 label={props.accessibilityLabel}
88 {...props}
89 onPress={() => {
90 Keyboard.dismiss()
91 props.onPress()
92 }}
93 size="small"
94 color="secondary"
95 shape="round"
96 variant="ghost"
97 style={[a.bg_transparent]}>
98 <ButtonIcon icon={DotsHorizontal} size="md" />
99 </Button>
100 )}
101 </Menu.Trigger>
102 </View>
103 )}
104
105 <Menu.Outer>
106 <MenuContent
107 profile={profile}
108 showMarkAsRead={showMarkAsRead}
109 blockInfo={blockInfo}
110 convo={convo}
111 leaveConvoControl={leaveConvoControl}
112 reportControl={reportControl}
113 blockedByListControl={blockedByListControl}
114 />
115 </Menu.Outer>
116 </Menu.Root>
117
118 <LeaveConvoPrompt
119 control={leaveConvoControl}
120 convoId={convo.id}
121 currentScreen={currentScreen}
122 />
123 {latestReportableMessage ? (
124 <>
125 <ReportDialog
126 subject={{
127 view: 'convo',
128 convoId: convo.id,
129 message: latestReportableMessage,
130 }}
131 control={reportControl}
132 onAfterSubmit={() => {
133 const sender = convo.members.find(
134 member => member.did === latestReportableMessage.sender.did,
135 )
136 if (sender) {
137 unstableCacheProfileView(queryClient, sender)
138 }
139 blockOrDeleteControl.open()
140 }}
141 />
142 <AfterReportDialog
143 control={blockOrDeleteControl}
144 currentScreen={currentScreen}
145 params={{
146 convoId: convo.id,
147 message: latestReportableMessage,
148 }}
149 />
150 </>
151 ) : (
152 <ReportConversationPrompt control={reportControl} />
153 )}
154
155 <BlockedByListDialog
156 control={blockedByListControl}
157 listBlocks={listBlocks}
158 />
159 </>
160 )
161}
162ConvoMenu = React.memo(ConvoMenu)
163
164function MenuContent({
165 convo: initialConvo,
166 profile,
167 showMarkAsRead,
168 blockInfo,
169 leaveConvoControl,
170 reportControl,
171 blockedByListControl,
172}: {
173 convo: ChatBskyConvoDefs.ConvoView
174 profile: Shadow<bsky.profile.AnyProfileView>
175 showMarkAsRead?: boolean
176 blockInfo: {
177 listBlocks: ModerationCause[]
178 userBlock?: ModerationCause
179 }
180 leaveConvoControl: Prompt.PromptControlProps
181 reportControl: Prompt.PromptControlProps
182 blockedByListControl: Prompt.PromptControlProps
183}) {
184 const navigation = useNavigation<NavigationProp>()
185 const {_} = useLingui()
186 const {mutate: markAsRead} = useMarkAsReadMutation()
187
188 const {listBlocks, userBlock} = blockInfo
189 const isBlocking = userBlock || !!listBlocks.length
190 const isDeletedAccount = profile.handle === 'missing.invalid'
191
192 const convoId = initialConvo.id
193 const {data: convo} = useConvoQuery(initialConvo)
194
195 const onNavigateToProfile = useCallback(() => {
196 navigation.navigate('Profile', {name: profile.did})
197 }, [navigation, profile.did])
198
199 const {mutate: muteConvo} = useMuteConvo(convoId, {
200 onSuccess: data => {
201 if (data.convo.muted) {
202 Toast.show(_(msg({message: 'Chat muted', context: 'toast'})))
203 } else {
204 Toast.show(_(msg({message: 'Chat unmuted', context: 'toast'})))
205 }
206 },
207 onError: () => {
208 Toast.show(_(msg`Could not mute chat`), 'xmark')
209 },
210 })
211
212 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
213
214 const toggleBlock = React.useCallback(() => {
215 if (listBlocks.length) {
216 blockedByListControl.open()
217 return
218 }
219
220 if (userBlock) {
221 queueUnblock()
222 } else {
223 queueBlock()
224 }
225 }, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock])
226
227 return isDeletedAccount ? (
228 <Menu.Item
229 label={_(msg`Leave conversation`)}
230 onPress={() => leaveConvoControl.open()}>
231 <Menu.ItemText>
232 <Trans>Leave conversation</Trans>
233 </Menu.ItemText>
234 <Menu.ItemIcon icon={ArrowBoxLeft} />
235 </Menu.Item>
236 ) : (
237 <>
238 <Menu.Group>
239 {showMarkAsRead && (
240 <Menu.Item
241 label={_(msg`Mark as read`)}
242 onPress={() => markAsRead({convoId})}>
243 <Menu.ItemText>
244 <Trans>Mark as read</Trans>
245 </Menu.ItemText>
246 <Menu.ItemIcon icon={Bubble} />
247 </Menu.Item>
248 )}
249 <Menu.Item
250 label={_(msg`Go to user's profile`)}
251 onPress={onNavigateToProfile}>
252 <Menu.ItemText>
253 <Trans>Go to profile</Trans>
254 </Menu.ItemText>
255 <Menu.ItemIcon icon={Person} />
256 </Menu.Item>
257 <Menu.Item
258 label={_(msg`Mute conversation`)}
259 onPress={() => muteConvo({mute: !convo?.muted})}>
260 <Menu.ItemText>
261 {convo?.muted ? (
262 <Trans>Unmute conversation</Trans>
263 ) : (
264 <Trans>Mute conversation</Trans>
265 )}
266 </Menu.ItemText>
267 <Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
268 </Menu.Item>
269 </Menu.Group>
270 <Menu.Divider />
271 <Menu.Group>
272 <Menu.Item
273 label={isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
274 onPress={toggleBlock}>
275 <Menu.ItemText>
276 {isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
277 </Menu.ItemText>
278 <Menu.ItemIcon icon={isBlocking ? PersonCheck : PersonX} />
279 </Menu.Item>
280 <Menu.Item
281 label={_(msg`Report conversation`)}
282 onPress={() => reportControl.open()}>
283 <Menu.ItemText>
284 <Trans>Report conversation</Trans>
285 </Menu.ItemText>
286 <Menu.ItemIcon icon={Flag} />
287 </Menu.Item>
288 </Menu.Group>
289 <Menu.Divider />
290 <Menu.Group>
291 <Menu.Item
292 label={_(msg`Leave conversation`)}
293 onPress={() => leaveConvoControl.open()}>
294 <Menu.ItemText>
295 <Trans>Leave conversation</Trans>
296 </Menu.ItemText>
297 <Menu.ItemIcon icon={ArrowBoxLeft} />
298 </Menu.Item>
299 </Menu.Group>
300 </>
301 )
302}
303
304export {ConvoMenu}