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