Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import {memo, useMemo} from 'react'
2import * as ExpoClipboard from 'expo-clipboard'
3import {AtUri} from '@atproto/api'
4import {isIOS} from '@bsky.app/alf'
5import {msg} from '@lingui/core/macro'
6import {useLingui} from '@lingui/react'
7import {Trans} from '@lingui/react/macro'
8import {useNavigation} from '@react-navigation/native'
9import {useQueryClient} from '@tanstack/react-query'
10
11import {useOpenLink} from '#/lib/hooks/useOpenLink'
12import {makeProfileLink} from '#/lib/routes/links'
13import {type NavigationProp} from '#/lib/routes/types'
14import {shareText, shareUrl} from '#/lib/sharing'
15import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers'
16import {useProfileShadow} from '#/state/cache/profile-shadow'
17import {useShowExternalShareButtons} from '#/state/preferences/external-share-buttons'
18import {precachePost} from '#/state/queries/post'
19import {useSession} from '#/state/session'
20import {atoms as a} from '#/alf'
21import {Admonition} from '#/components/Admonition'
22import {useDialogControl} from '#/components/Dialog'
23import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
24import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
25import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
26import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
27import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
28import {SquareArrowTopRight_Stroke2_Corner0_Rounded as ExternalIcon} from '#/components/icons/SquareArrowTopRight'
29import * as Menu from '#/components/Menu'
30import * as Toast from '#/components/Toast'
31import {useAgeAssurance} from '#/ageAssurance'
32import {useAnalytics} from '#/analytics'
33import {IS_IOS} from '#/env'
34import {useDevMode} from '#/storage/hooks/dev-mode'
35import {RecentChats} from './RecentChats'
36import {type ShareMenuItemsProps} from './ShareMenuItems.types'
37
38let ShareMenuItems = ({
39 post,
40 onShare: onShareProp,
41}: ShareMenuItemsProps): React.ReactNode => {
42 const ax = useAnalytics()
43 const {hasSession} = useSession()
44 const {_} = useLingui()
45 const navigation = useNavigation<NavigationProp>()
46 const sendViaChatControl = useDialogControl()
47 const [devModeEnabled] = useDevMode()
48 const aa = useAgeAssurance()
49 const openLink = useOpenLink()
50 const queryClient = useQueryClient()
51
52 const postUri = post.uri
53 const postAuthor = useProfileShadow(post.author)
54
55 const href = useMemo(() => {
56 const urip = new AtUri(postUri)
57 return makeProfileLink(postAuthor, 'post', urip.rkey)
58 }, [postUri, postAuthor])
59
60 const hideInPWI = useMemo(() => {
61 return !!postAuthor.labels?.find(
62 label => label.val === '!no-unauthenticated',
63 )
64 }, [postAuthor])
65
66 const onSharePost = () => {
67 ax.metric('share:press:nativeShare', {})
68 const url = toShareUrl(href)
69 shareUrl(url)
70 onShareProp()
71 }
72
73 const onSharePostBsky = () => {
74 ax.metric('share:press:nativeShare', {})
75 const url = toShareUrlBsky(href)
76 shareUrl(url)
77 onShareProp()
78 }
79
80 const onCopyLink = async () => {
81 ax.metric('share:press:copyLink', {})
82 const url = toShareUrl(href)
83 if (IS_IOS) {
84 // iOS only
85 await ExpoClipboard.setUrlAsync(url)
86 } else {
87 await ExpoClipboard.setStringAsync(url)
88 }
89 Toast.show(_(msg`Copied to clipboard`), {
90 type: 'success',
91 })
92 onShareProp()
93 }
94
95 const onCopyLinkBsky = async () => {
96 ax.metric('share:press:copyLink', {})
97 const url = toShareUrlBsky(href)
98 if (isIOS) {
99 // iOS only
100 await ExpoClipboard.setUrlAsync(url)
101 } else {
102 await ExpoClipboard.setStringAsync(url)
103 }
104 Toast.show(_(msg`Copied to clipboard`), {
105 type: 'success',
106 })
107 onShareProp()
108 }
109
110 const onBeforeShareViaChat = () => {
111 precachePost(queryClient, postUri, post)
112 }
113
114 const onSelectChatToShareTo = (conversation: string) => {
115 onBeforeShareViaChat()
116 navigation.navigate('MessagesConversation', {
117 conversation,
118 embed: postUri,
119 })
120 }
121
122 const onShareATURI = () => {
123 shareText(postUri)
124 }
125
126 const onShareAuthorDID = () => {
127 shareText(postAuthor.did)
128 }
129
130 const showExternalShareButtons = useShowExternalShareButtons()
131 const isBridgedPost =
132 !!post.record.bridgyOriginalUrl || !!post.record.fediverseId
133 const originalPostUrl = (post.record.bridgyOriginalUrl ||
134 post.record.fediverseId) as string | undefined
135
136 const onOpenOriginalPost = () => {
137 if (originalPostUrl) {
138 openLink(originalPostUrl, true)
139 }
140 }
141
142 const onOpenPostInPdsls = () => {
143 openLink(`https://pdsls.dev/${post.uri}`, true)
144 }
145
146 return (
147 <>
148 <Menu.Outer>
149 {hasSession && aa.state.access === aa.Access.Full && (
150 <Menu.Group>
151 <Menu.ContainerItem>
152 <RecentChats
153 postUri={postUri}
154 onBeforePress={onBeforeShareViaChat}
155 />
156 </Menu.ContainerItem>
157 <Menu.Item
158 testID="postDropdownSendViaDMBtn"
159 label={_(msg`Send via direct message`)}
160 onPress={() => {
161 ax.metric('share:press:openDmSearch', {})
162 sendViaChatControl.open()
163 }}>
164 <Menu.ItemText>
165 <Trans>Send via direct message</Trans>
166 </Menu.ItemText>
167 <Menu.ItemIcon icon={PaperPlaneIcon} position="right" />
168 </Menu.Item>
169 </Menu.Group>
170 )}
171
172 {showExternalShareButtons && (
173 <Menu.Group>
174 {isBridgedPost && (
175 <Menu.Item
176 testID="postDropdownOpenOriginalPost"
177 label={_(msg`Open original post`)}
178 onPress={onOpenOriginalPost}>
179 <Menu.ItemText>
180 <Trans>Open original post</Trans>
181 </Menu.ItemText>
182 <Menu.ItemIcon icon={ExternalIcon} position="right" />
183 </Menu.Item>
184 )}
185
186 <Menu.Item
187 testID="postDropdownOpenInPdsls"
188 label={_(msg`Open post in PDSls`)}
189 onPress={onOpenPostInPdsls}>
190 <Menu.ItemText>
191 <Trans>Open post in PDSls</Trans>
192 </Menu.ItemText>
193 <Menu.ItemIcon icon={ExternalIcon} position="right" />
194 </Menu.Item>
195 </Menu.Group>
196 )}
197
198 <Menu.Group>
199 <Menu.Item
200 testID="postDropdownShareBtn"
201 label={_(msg`Share via...`)}
202 onPress={onSharePost}>
203 <Menu.ItemText>
204 <Trans>Share via...</Trans>
205 </Menu.ItemText>
206 <Menu.ItemIcon icon={ArrowOutOfBoxIcon} position="right" />
207 </Menu.Item>
208
209 <Menu.Item
210 testID="postDropdownShareBtn"
211 label={_(msg`Share via bsky.app...`)}
212 onPress={onSharePostBsky}>
213 <Menu.ItemText>
214 <Trans>Share via bsky.app...</Trans>
215 </Menu.ItemText>
216 <Menu.ItemIcon icon={ArrowOutOfBoxIcon} position="right" />
217 </Menu.Item>
218
219 <Menu.Item
220 testID="postDropdownShareBtn"
221 label={_(msg`Copy link to post`)}
222 onPress={onCopyLink}>
223 <Menu.ItemText>
224 <Trans>Copy link to post</Trans>
225 </Menu.ItemText>
226 <Menu.ItemIcon icon={ChainLinkIcon} position="right" />
227 </Menu.Item>
228
229 <Menu.Item
230 testID="postDropdownShareBtn"
231 label={_(msg`Copy via bsky.app`)}
232 onPress={onCopyLinkBsky}>
233 <Menu.ItemText>
234 <Trans>Copy via bsky.app</Trans>
235 </Menu.ItemText>
236 <Menu.ItemIcon icon={ChainLinkIcon} position="right" />
237 </Menu.Item>
238 </Menu.Group>
239
240 {hideInPWI && (
241 <Menu.Group>
242 <Menu.ContainerItem>
243 <Admonition
244 type="warning"
245 style={[a.flex_1, a.border_0, a.p_0, a.bg_transparent]}>
246 <Trans>This post is only visible to logged-in users.</Trans>
247 </Admonition>
248 </Menu.ContainerItem>
249 </Menu.Group>
250 )}
251
252 {devModeEnabled && (
253 <Menu.Group>
254 <Menu.Item
255 testID="postAtUriShareBtn"
256 label={_(msg`Share post at:// URI`)}
257 onPress={onShareATURI}>
258 <Menu.ItemText>
259 <Trans>Share post at:// URI</Trans>
260 </Menu.ItemText>
261 <Menu.ItemIcon icon={ClipboardIcon} position="right" />
262 </Menu.Item>
263 <Menu.Item
264 testID="postAuthorDIDShareBtn"
265 label={_(msg`Share author DID`)}
266 onPress={onShareAuthorDID}>
267 <Menu.ItemText>
268 <Trans>Share author DID</Trans>
269 </Menu.ItemText>
270 <Menu.ItemIcon icon={ClipboardIcon} position="right" />
271 </Menu.Item>
272 </Menu.Group>
273 )}
274 </Menu.Outer>
275
276 <SendViaChatDialog
277 control={sendViaChatControl}
278 onSelectChat={onSelectChatToShareTo}
279 />
280 </>
281 )
282}
283ShareMenuItems = memo(ShareMenuItems)
284export {ShareMenuItems}