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