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