Bluesky app fork with some witchin' additions 💫

Compare changes

Choose any two refs to compare.

Changed files
+82 -1
src
components
PostControls
ShareMenu
lib
strings
+41 -1
src/components/PostControls/ShareMenu/ShareMenuItems.tsx
··· 9 import {makeProfileLink} from '#/lib/routes/links' 10 import {type NavigationProp} from '#/lib/routes/types' 11 import {shareText, shareUrl} from '#/lib/sharing' 12 - import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers' 13 import {logger} from '#/logger' 14 import {isIOS} from '#/platform/detection' 15 import {useProfileShadow} from '#/state/cache/profile-shadow' ··· 97 onShareProp() 98 } 99 100 const onSelectChatToShareTo = (conversation: string) => { 101 navigation.navigate('MessagesConversation', { 102 conversation, ··· 215 </Menu.ItemText> 216 <Menu.ItemIcon icon={ChainLinkIcon} position="right" /> 217 </Menu.Item> 218 </Menu.Group> 219 220 {hideInPWI && (
··· 9 import {makeProfileLink} from '#/lib/routes/links' 10 import {type NavigationProp} from '#/lib/routes/types' 11 import {shareText, shareUrl} from '#/lib/sharing' 12 + import {toShareUrl, toShareUrlBsky, toShareUrlAturi} from '#/lib/strings/url-helpers' 13 import {logger} from '#/logger' 14 import {isIOS} from '#/platform/detection' 15 import {useProfileShadow} from '#/state/cache/profile-shadow' ··· 97 onShareProp() 98 } 99 100 + const onSharePostAturi = () => { 101 + logger.metric('share:press:nativeShare', {}, {statsig: true}) 102 + const url = toShareUrlAturi(href) 103 + shareUrl(url) 104 + onShareProp() 105 + } 106 + 107 + const onCopyLinkAturi = async () => { 108 + logger.metric('share:press:copyLink', {}, {statsig: true}) 109 + const url = toShareUrlAturi(href) 110 + if (isIOS) { 111 + // iOS only 112 + await ExpoClipboard.setUrlAsync(url) 113 + } else { 114 + await ExpoClipboard.setStringAsync(url) 115 + } 116 + Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 117 + onShareProp() 118 + } 119 + 120 const onSelectChatToShareTo = (conversation: string) => { 121 navigation.navigate('MessagesConversation', { 122 conversation, ··· 235 </Menu.ItemText> 236 <Menu.ItemIcon icon={ChainLinkIcon} position="right" /> 237 </Menu.Item> 238 + 239 + <Menu.Item 240 + testID="postDropdownShareAturiBtn" 241 + label={_(msg`Share via aturi.to...`)} 242 + onPress={onSharePostAturi}> 243 + <Menu.ItemText> 244 + <Trans>Share via aturi.to...</Trans> 245 + </Menu.ItemText> 246 + <Menu.ItemIcon icon={ArrowOutOfBoxIcon} position="right" /> 247 + </Menu.Item> 248 + 249 + <Menu.Item 250 + testID="postDropdownCopyAturiBtn" 251 + label={_(msg`Copy aturi.to link`)} 252 + onPress={onCopyLinkAturi}> 253 + <Menu.ItemText> 254 + <Trans>Copy aturi.to link</Trans> 255 + </Menu.ItemText> 256 + <Menu.ItemIcon icon={ChainLinkIcon} position="right" /> 257 + </Menu.Item> 258 </Menu.Group> 259 260 {hideInPWI && (
+41
src/lib/strings/url-helpers.ts
··· 99 return url 100 } 101 102 export function toBskyAppUrl(url: string): string { 103 return new URL(url, BSKY_APP_HOST).toString() 104 }
··· 99 return url 100 } 101 102 + export function toShareUrlAturi(url: string): string { 103 + // Convert witchsky URL to aturi.to format 104 + // Expected input format: /profile/handle/post/rkey 105 + try { 106 + if (!url.startsWith('https')) { 107 + const urlp = new URL('https://witchsky.app') 108 + urlp.pathname = url 109 + url = urlp.toString() 110 + } 111 + 112 + const urlp = new URL(url) 113 + const pathname = urlp.pathname 114 + 115 + // Extract components from /profile/identifier/post/rkey 116 + if (pathname.startsWith('/profile/')) { 117 + const parts = pathname.substring(9).split('/') // Remove "/profile/" 118 + 119 + if (parts.length === 3 && parts[1] === 'post') { 120 + // Post: /profile/identifier/post/rkey 121 + const identifier = parts[0] 122 + const rkey = parts[2] 123 + return `https://aturi.to/${identifier}/app.bsky.feed.post/${rkey}` 124 + } else if (parts.length === 3 && parts[1] === 'lists') { 125 + // List: /profile/identifier/lists/rkey 126 + const identifier = parts[0] 127 + const rkey = parts[2] 128 + return `https://aturi.to/${identifier}/app.bsky.graph.list/${rkey}` 129 + } else if (parts.length === 1) { 130 + // Profile only: /profile/identifier 131 + return `https://aturi.to/${parts[0]}` 132 + } 133 + } 134 + 135 + // Fallback to original URL if we can't parse it 136 + return url 137 + } catch (e) { 138 + console.error('Error converting to aturi.to URL:', e) 139 + return url 140 + } 141 + } 142 + 143 export function toBskyAppUrl(url: string): string { 144 return new URL(url, BSKY_APP_HOST).toString() 145 }