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