this repo has no description
1import {memo, 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 {type ViewStyleProp} from '#/alf'
22import {atoms as a} from '#/alf'
23import {Button, ButtonIcon} from '#/components/Button'
24import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
25import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
26import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
27import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
28import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft'
29import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
30import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
31import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
32import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
33import {
34 Person_Stroke2_Corner0_Rounded as Person,
35 PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
36 PersonX_Stroke2_Corner0_Rounded as PersonX,
37} from '#/components/icons/Person'
38import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
39import * as Menu from '#/components/Menu'
40import {ReportDialog} from '#/components/moderation/ReportDialog'
41import * as Prompt from '#/components/Prompt'
42import * as Toast from '#/components/Toast'
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 = 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`), {
209 type: 'error',
210 })
211 },
212 })
213
214 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
215
216 const toggleBlock = useCallback(() => {
217 if (listBlocks.length) {
218 blockedByListControl.open()
219 return
220 }
221
222 if (userBlock) {
223 queueUnblock()
224 } else {
225 queueBlock()
226 }
227 }, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock])
228
229 return isDeletedAccount ? (
230 <Menu.Item
231 label={_(msg`Leave conversation`)}
232 onPress={() => leaveConvoControl.open()}>
233 <Menu.ItemText>
234 <Trans>Leave conversation</Trans>
235 </Menu.ItemText>
236 <Menu.ItemIcon icon={ArrowBoxLeft} />
237 </Menu.Item>
238 ) : (
239 <>
240 <Menu.Group>
241 {showMarkAsRead && (
242 <Menu.Item
243 label={_(msg`Mark as read`)}
244 onPress={() => markAsRead({convoId})}>
245 <Menu.ItemText>
246 <Trans>Mark as read</Trans>
247 </Menu.ItemText>
248 <Menu.ItemIcon icon={Bubble} />
249 </Menu.Item>
250 )}
251 <Menu.Item
252 label={_(msg`Go to user's profile`)}
253 onPress={onNavigateToProfile}>
254 <Menu.ItemText>
255 <Trans>Go to profile</Trans>
256 </Menu.ItemText>
257 <Menu.ItemIcon icon={Person} />
258 </Menu.Item>
259 <Menu.Item
260 label={_(msg`Mute conversation`)}
261 onPress={() => muteConvo({mute: !convo?.muted})}>
262 <Menu.ItemText>
263 {convo?.muted ? (
264 <Trans>Unmute conversation</Trans>
265 ) : (
266 <Trans>Mute conversation</Trans>
267 )}
268 </Menu.ItemText>
269 <Menu.ItemIcon icon={convo?.muted ? Unmute : Mute} />
270 </Menu.Item>
271 </Menu.Group>
272 <Menu.Divider />
273 <Menu.Group>
274 <Menu.Item
275 label={isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
276 onPress={toggleBlock}>
277 <Menu.ItemText>
278 {isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)}
279 </Menu.ItemText>
280 <Menu.ItemIcon icon={isBlocking ? PersonCheck : PersonX} />
281 </Menu.Item>
282 <Menu.Item
283 label={_(msg`Report conversation`)}
284 onPress={() => reportControl.open()}>
285 <Menu.ItemText>
286 <Trans>Report conversation</Trans>
287 </Menu.ItemText>
288 <Menu.ItemIcon icon={Flag} />
289 </Menu.Item>
290 </Menu.Group>
291 <Menu.Divider />
292 <Menu.Group>
293 <Menu.Item
294 label={_(msg`Leave conversation`)}
295 onPress={() => leaveConvoControl.open()}>
296 <Menu.ItemText>
297 <Trans>Leave conversation</Trans>
298 </Menu.ItemText>
299 <Menu.ItemIcon icon={ArrowBoxLeft} />
300 </Menu.Item>
301 </Menu.Group>
302 </>
303 )
304}
305
306export {ConvoMenu}