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