forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {type AppBskyActorDefs, AppBskyGraphDefs, AtUri} from '@atproto/api'
2import {msg} from '@lingui/core/macro'
3import {useLingui} from '@lingui/react'
4import {Trans} from '@lingui/react/macro'
5import {useNavigation} from '@react-navigation/native'
6
7import {type NavigationProp} from '#/lib/routes/types'
8import {shareUrl} from '#/lib/sharing'
9import {toShareUrl} from '#/lib/strings/url-helpers'
10import {logger} from '#/logger'
11import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
12import {
13 useListBlockMutation,
14 useListDeleteMutation,
15 useListMuteMutation,
16} from '#/state/queries/list'
17import {useRemoveFeedMutation} from '#/state/queries/preferences'
18import {useSession} from '#/state/session'
19import {Button, ButtonIcon} from '#/components/Button'
20import {useDialogControl} from '#/components/Dialog'
21import {CreateOrEditListDialog} from '#/components/dialogs/lists/CreateOrEditListDialog'
22import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox'
23import {ChainLink_Stroke2_Corner0_Rounded as ChainLink} from '#/components/icons/ChainLink'
24import {DotGrid_Stroke2_Corner0_Rounded as DotGridIcon} from '#/components/icons/DotGrid'
25import {PencilLine_Stroke2_Corner0_Rounded as PencilLineIcon} from '#/components/icons/Pencil'
26import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheckIcon} from '#/components/icons/Person'
27import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
28import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
29import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
30import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
31import * as Menu from '#/components/Menu'
32import {
33 ReportDialog,
34 useReportDialogControl,
35} from '#/components/moderation/ReportDialog'
36import * as Prompt from '#/components/Prompt'
37import * as Toast from '#/components/Toast'
38import {useAnalytics} from '#/analytics'
39import {IS_WEB} from '#/env'
40
41export function MoreOptionsMenu({
42 list,
43 savedFeedConfig,
44}: {
45 list: AppBskyGraphDefs.ListView
46 savedFeedConfig?: AppBskyActorDefs.SavedFeed
47}) {
48 const {_} = useLingui()
49 const ax = useAnalytics()
50 const {currentAccount} = useSession()
51 const editListDialogControl = useDialogControl()
52 const deleteListPromptControl = useDialogControl()
53 const reportDialogControl = useReportDialogControl()
54 const navigation = useNavigation<NavigationProp>()
55
56 const {mutateAsync: removeSavedFeed} = useRemoveFeedMutation()
57 const {mutateAsync: deleteList} = useListDeleteMutation()
58 const {mutateAsync: muteList} = useListMuteMutation()
59 const {mutateAsync: blockList} = useListBlockMutation()
60
61 const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
62 const isModList = list.purpose === AppBskyGraphDefs.MODLIST
63 const isBlocking = !!list.viewer?.blocked
64 const isMuting = !!list.viewer?.muted
65 const isPinned = Boolean(savedFeedConfig?.pinned)
66 const isOwner = currentAccount?.did === list.creator.did
67
68 const enableSquareButtons = useEnableSquareButtons()
69
70 const onPressShare = () => {
71 const {rkey} = new AtUri(list.uri)
72 const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
73 shareUrl(url)
74 }
75
76 const onRemoveFromSavedFeeds = async () => {
77 if (!savedFeedConfig) return
78 try {
79 await removeSavedFeed(savedFeedConfig)
80 Toast.show(_(msg`Removed from your feeds`))
81 } catch (e) {
82 Toast.show(_(msg`There was an issue contacting the server`), {
83 type: 'error',
84 })
85 logger.error('Failed to remove pinned list', {message: e})
86 }
87 }
88
89 const onPressDelete = async () => {
90 await deleteList({uri: list.uri})
91
92 if (savedFeedConfig) {
93 await removeSavedFeed(savedFeedConfig)
94 }
95
96 Toast.show(_(msg({message: 'List deleted', context: 'toast'})))
97 if (navigation.canGoBack()) {
98 navigation.goBack()
99 } else {
100 navigation.navigate('Home')
101 }
102 }
103
104 const onUnpinModList = async () => {
105 try {
106 if (!savedFeedConfig) return
107 await removeSavedFeed(savedFeedConfig)
108 Toast.show(_(msg`Unpinned list`))
109 } catch {
110 Toast.show(_(msg`Failed to unpin list`), {
111 type: 'error',
112 })
113 }
114 }
115
116 const onUnsubscribeMute = async () => {
117 try {
118 await muteList({uri: list.uri, mute: false})
119 Toast.show(_(msg({message: 'List unmuted', context: 'toast'})))
120 ax.metric('moderation:unsubscribedFromList', {listType: 'mute'})
121 } catch {
122 Toast.show(
123 _(
124 msg`There was an issue. Please check your internet connection and try again.`,
125 ),
126 )
127 }
128 }
129
130 const onUnsubscribeBlock = async () => {
131 try {
132 await blockList({uri: list.uri, block: false})
133 Toast.show(_(msg({message: 'List unblocked', context: 'toast'})))
134 ax.metric('moderation:unsubscribedFromList', {listType: 'block'})
135 } catch {
136 Toast.show(
137 _(
138 msg`There was an issue. Please check your internet connection and try again.`,
139 ),
140 )
141 }
142 }
143
144 return (
145 <>
146 <Menu.Root>
147 <Menu.Trigger label={_(msg`More options`)}>
148 {({props}) => (
149 <Button
150 label={props.accessibilityLabel}
151 testID="moreOptionsBtn"
152 size="small"
153 color="secondary"
154 shape={enableSquareButtons ? 'square' : 'round'}
155 {...props}>
156 <ButtonIcon icon={DotGridIcon} />
157 </Button>
158 )}
159 </Menu.Trigger>
160 <Menu.Outer>
161 <Menu.Group>
162 <Menu.Item
163 label={IS_WEB ? _(msg`Copy link to list`) : _(msg`Share via...`)}
164 onPress={onPressShare}>
165 <Menu.ItemText>
166 {IS_WEB ? (
167 <Trans>Copy link to list</Trans>
168 ) : (
169 <Trans>Share via...</Trans>
170 )}
171 </Menu.ItemText>
172 <Menu.ItemIcon
173 position="right"
174 icon={IS_WEB ? ChainLink : ShareIcon}
175 />
176 </Menu.Item>
177 {savedFeedConfig && (
178 <Menu.Item
179 label={_(msg`Remove from my feeds`)}
180 onPress={onRemoveFromSavedFeeds}>
181 <Menu.ItemText>
182 <Trans>Remove from my feeds</Trans>
183 </Menu.ItemText>
184 <Menu.ItemIcon position="right" icon={TrashIcon} />
185 </Menu.Item>
186 )}
187 </Menu.Group>
188
189 <Menu.Divider />
190
191 {isOwner ? (
192 <Menu.Group>
193 <Menu.Item
194 label={_(msg`Edit list details`)}
195 onPress={editListDialogControl.open}>
196 <Menu.ItemText>
197 <Trans>Edit list details</Trans>
198 </Menu.ItemText>
199 <Menu.ItemIcon position="right" icon={PencilLineIcon} />
200 </Menu.Item>
201 <Menu.Item
202 label={_(msg`Delete list`)}
203 onPress={deleteListPromptControl.open}>
204 <Menu.ItemText>
205 <Trans>Delete list</Trans>
206 </Menu.ItemText>
207 <Menu.ItemIcon position="right" icon={TrashIcon} />
208 </Menu.Item>
209 </Menu.Group>
210 ) : (
211 <Menu.Group>
212 <Menu.Item
213 label={_(msg`Report list`)}
214 onPress={reportDialogControl.open}>
215 <Menu.ItemText>
216 <Trans>Report list</Trans>
217 </Menu.ItemText>
218 <Menu.ItemIcon position="right" icon={WarningIcon} />
219 </Menu.Item>
220 </Menu.Group>
221 )}
222
223 {isModList && isPinned && (
224 <>
225 <Menu.Divider />
226 <Menu.Group>
227 <Menu.Item
228 label={_(msg`Unpin moderation list`)}
229 onPress={onUnpinModList}>
230 <Menu.ItemText>
231 <Trans>Unpin moderation list</Trans>
232 </Menu.ItemText>
233 <Menu.ItemIcon icon={PinIcon} />
234 </Menu.Item>
235 </Menu.Group>
236 </>
237 )}
238
239 {isCurateList && (isBlocking || isMuting) && (
240 <>
241 <Menu.Divider />
242 <Menu.Group>
243 {isBlocking && (
244 <Menu.Item
245 label={_(msg`Unblock list`)}
246 onPress={onUnsubscribeBlock}>
247 <Menu.ItemText>
248 <Trans>Unblock list</Trans>
249 </Menu.ItemText>
250 <Menu.ItemIcon icon={PersonCheckIcon} />
251 </Menu.Item>
252 )}
253 {isMuting && (
254 <Menu.Item
255 label={_(msg`Unmute list`)}
256 onPress={onUnsubscribeMute}>
257 <Menu.ItemText>
258 <Trans>Unmute list</Trans>
259 </Menu.ItemText>
260 <Menu.ItemIcon icon={UnmuteIcon} />
261 </Menu.Item>
262 )}
263 </Menu.Group>
264 </>
265 )}
266 </Menu.Outer>
267 </Menu.Root>
268
269 <CreateOrEditListDialog control={editListDialogControl} list={list} />
270
271 <Prompt.Basic
272 control={deleteListPromptControl}
273 title={_(msg`Delete this list?`)}
274 description={_(
275 msg`If you delete this list, you won't be able to recover it.`,
276 )}
277 onConfirm={onPressDelete}
278 confirmButtonCta={_(msg`Delete`)}
279 confirmButtonColor="negative"
280 />
281
282 <ReportDialog
283 control={reportDialogControl}
284 subject={{
285 ...list,
286 $type: 'app.bsky.graph.defs#listView',
287 }}
288 />
289 </>
290 )
291}