forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {memo, useMemo} from 'react'
2import * as ExpoClipboard from 'expo-clipboard'
3import {AtUri} from '@atproto/api'
4import {msg, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6import {useNavigation} from '@react-navigation/native'
7
8import {useOpenLink} from '#/lib/hooks/useOpenLink'
9import {makeProfileLink} from '#/lib/routes/links'
10import {type NavigationProp} from '#/lib/routes/types'
11import {shareText, shareUrl} from '#/lib/sharing'
12import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
13import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
15import {isIOS} from '#/platform/detection'
16import {useProfileShadow} from '#/state/cache/profile-shadow'
17import {useTerminologyPreference} from '#/state/preferences'
18import {useShowExternalShareButtons} from '#/state/preferences/external-share-buttons'
19import {useSession} from '#/state/session'
20import * as Toast from '#/view/com/util/Toast'
21import {atoms as a} from '#/alf'
22import {Admonition} from '#/components/Admonition'
23import {useDialogControl} from '#/components/Dialog'
24import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
25import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
26import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
27import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
28import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
29import {SquareArrowTopRight_Stroke2_Corner0_Rounded as ExternalIcon} from '#/components/icons/SquareArrowTopRight'
30import * as Menu from '#/components/Menu'
31import {useAgeAssurance} from '#/ageAssurance'
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 {hasSession} = useSession()
41 const {_} = useLingui()
42 const terminologyPreference = useTerminologyPreference()
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 logger.metric('share:press:nativeShare', {}, {statsig: true})
65 const url = toShareUrl(href)
66 shareUrl(url)
67 onShareProp()
68 }
69
70 const onSharePostBsky = () => {
71 logger.metric('share:press:nativeShare', {}, {statsig: true})
72 const url = toShareUrlBsky(href)
73 shareUrl(url)
74 onShareProp()
75 }
76
77 const onCopyLink = async () => {
78 logger.metric('share:press:copyLink', {}, {statsig: true})
79 const url = toShareUrl(href)
80 if (isIOS) {
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 logger.metric('share:press:copyLink', {}, {statsig: true})
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 originalPostUrl && openLink(originalPostUrl, true)
126 }
127
128 const onOpenPostInPdsls = () => {
129 openLink(`https://pdsls.dev/${post.uri}`, true)
130 }
131
132 return (
133 <>
134 <Menu.Outer>
135 {hasSession && aa.state.access === aa.Access.Full && (
136 <Menu.Group>
137 <Menu.ContainerItem>
138 <RecentChats postUri={postUri} />
139 </Menu.ContainerItem>
140 <Menu.Item
141 testID="postDropdownSendViaDMBtn"
142 label={_(msg`Send via direct message`)}
143 onPress={() => {
144 logger.metric('share:press:openDmSearch', {}, {statsig: true})
145 sendViaChatControl.open()
146 }}>
147 <Menu.ItemText>
148 <Trans>Send via direct message</Trans>
149 </Menu.ItemText>
150 <Menu.ItemIcon icon={PaperPlaneIcon} position="right" />
151 </Menu.Item>
152 </Menu.Group>
153 )}
154
155 {showExternalShareButtons && (
156 <Menu.Group>
157 {isBridgedPost && (
158 <Menu.Item
159 testID="postDropdownOpenOriginalPost"
160 label={_(getTerminology(terminologyPreference, {
161 skeet: msg`Open original skeet`,
162 post: msg`Open original post`,
163 spell: msg`Open original spell`,
164 }))}
165 onPress={onOpenOriginalPost}>
166 <Menu.ItemText>
167 <Trans>{_(getTerminology(terminologyPreference, {
168 skeet: msg`Open original skeet`,
169 post: msg`Open original post`,
170 spell: msg`Open original spell`,
171 }))}</Trans>
172 </Menu.ItemText>
173 <Menu.ItemIcon icon={ExternalIcon} position="right" />
174 </Menu.Item>
175 )}
176
177 <Menu.Item
178 testID="postDropdownOpenInPdsls"
179 label={_(getTerminology(terminologyPreference, {
180 skeet: msg`Open skeet in PDSls`,
181 post: msg`Open post in PDSls`,
182 spell: msg`Open spell in PDSls`,
183 }))}
184 onPress={onOpenPostInPdsls}>
185 <Menu.ItemText>
186 <Trans>{_(getTerminology(terminologyPreference, {
187 skeet: msg`Open skeet in PDSls`,
188 post: msg`Open post in PDSls`,
189 spell: msg`Open spell in PDSls`,
190 }))}</Trans>
191 </Menu.ItemText>
192 <Menu.ItemIcon icon={ExternalIcon} position="right" />
193 </Menu.Item>
194 </Menu.Group>
195 )}
196
197 <Menu.Group>
198 <Menu.Item
199 testID="postDropdownShareBtn"
200 label={_(msg`Share via...`)}
201 onPress={onSharePost}>
202 <Menu.ItemText>
203 <Trans>Share via...</Trans>
204 </Menu.ItemText>
205 <Menu.ItemIcon icon={ArrowOutOfBoxIcon} position="right" />
206 </Menu.Item>
207
208 <Menu.Item
209 testID="postDropdownShareBtn"
210 label={_(msg`Share via bsky.app...`)}
211 onPress={onSharePostBsky}>
212 <Menu.ItemText>
213 <Trans>Share via bsky.app...</Trans>
214 </Menu.ItemText>
215 <Menu.ItemIcon icon={ArrowOutOfBoxIcon} position="right" />
216 </Menu.Item>
217
218 <Menu.Item
219 testID="postDropdownShareBtn"
220 label={_(getTerminology(terminologyPreference, {
221 skeet: msg`Copy link to skeet`,
222 post: msg`Copy link to post`,
223 spell: msg`Copy link to spell`,
224 }))}
225 onPress={onCopyLink}>
226 <Menu.ItemText>
227 <Trans>{_(getTerminology(terminologyPreference, {
228 skeet: msg`Copy link to skeet`,
229 post: msg`Copy link to post`,
230 spell: msg`Copy link to spell`,
231 }))}</Trans>
232 </Menu.ItemText>
233 <Menu.ItemIcon icon={ChainLinkIcon} position="right" />
234 </Menu.Item>
235
236 <Menu.Item
237 testID="postDropdownShareBtn"
238 label={_(msg`Copy via bsky.app`)}
239 onPress={onCopyLinkBsky}>
240 <Menu.ItemText>
241 <Trans>Copy via bsky.app</Trans>
242 </Menu.ItemText>
243 <Menu.ItemIcon icon={ChainLinkIcon} position="right" />
244 </Menu.Item>
245 </Menu.Group>
246
247 {hideInPWI && (
248 <Menu.Group>
249 <Menu.ContainerItem>
250 <Admonition
251 type="warning"
252 style={[a.flex_1, a.border_0, a.p_0, a.bg_transparent]}>
253 <Trans>{_(getTerminology(terminologyPreference, {
254 skeet: msg`This skeet is only visible to logged-in users.`,
255 post: msg`This post is only visible to logged-in users.`,
256 spell: msg`This spell is only visible to logged-in users.`,
257 }))}</Trans>
258 </Admonition>
259 </Menu.ContainerItem>
260 </Menu.Group>
261 )}
262
263 {devModeEnabled && (
264 <Menu.Group>
265 <Menu.Item
266 testID="postAtUriShareBtn"
267 label={_(getTerminology(terminologyPreference, {
268 skeet: msg`Share skeet at:// URI`,
269 post: msg`Share post at:// URI`,
270 spell: msg`Share spell at:// URI`,
271 }))}
272 onPress={onShareATURI}>
273 <Menu.ItemText>
274 <Trans>{_(getTerminology(terminologyPreference, {
275 skeet: msg`Share skeet at:// URI`,
276 post: msg`Share post at:// URI`,
277 spell: msg`Share spell at:// URI`,
278 }))}</Trans>
279 </Menu.ItemText>
280 <Menu.ItemIcon icon={ClipboardIcon} position="right" />
281 </Menu.Item>
282 <Menu.Item
283 testID="postAuthorDIDShareBtn"
284 label={_(msg`Share author DID`)}
285 onPress={onShareAuthorDID}>
286 <Menu.ItemText>
287 <Trans>Share author DID</Trans>
288 </Menu.ItemText>
289 <Menu.ItemIcon icon={ClipboardIcon} position="right" />
290 </Menu.Item>
291 </Menu.Group>
292 )}
293 </Menu.Outer>
294
295 <SendViaChatDialog
296 control={sendViaChatControl}
297 onSelectChat={onSelectChatToShareTo}
298 />
299 </>
300 )
301}
302ShareMenuItems = memo(ShareMenuItems)
303export {ShareMenuItems}