Bluesky app fork with some witchin' additions 💫

Add web linking and proper share controls

+50 -243
+2
ios/app.xcodeproj/project.pbxproj
··· 13 13 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 14 14 5CEAE7B7A55582F96F1D5952 /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB672808307A6013805A3FE /* libPods-app.a */; }; 15 15 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 16 + E4BBD590292C1F5200296224 /* app.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = E4437C9E28581FA7006DA9E7 /* app.entitlements */; }; 16 17 E4BD704B285AD57E00A8FED9 /* AppSecureRandomModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E4BD704A285AD57E00A8FED9 /* AppSecureRandomModule.m */; }; 17 18 E4BD704C285AD57E00A8FED9 /* AppSecureRandomModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E4BD704A285AD57E00A8FED9 /* AppSecureRandomModule.m */; }; 18 19 FEB90D21557517F9279AECA4 /* libPods-app-appTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BAD3BC60FA05CF2D4F6F9BA2 /* libPods-app-appTests.a */; }; ··· 248 249 isa = PBXResourcesBuildPhase; 249 250 buildActionMask = 2147483647; 250 251 files = ( 252 + E4BBD590292C1F5200296224 /* app.entitlements in Resources */, 251 253 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 252 254 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 253 255 );
+3 -1
ios/app/app.entitlements
··· 3 3 <plist version="1.0"> 4 4 <dict> 5 5 <key>com.apple.developer.associated-domains</key> 6 - <array/> 6 + <array> 7 + <string>applinks:bsky.app</string> 8 + </array> 7 9 </dict> 8 10 </plist>
+9
src/App.native.tsx
··· 1 1 import 'react-native-url-polyfill/auto' 2 2 import React, {useState, useEffect} from 'react' 3 + import {Linking} from 'react-native' 3 4 import {RootSiblingParent} from 'react-native-root-siblings' 4 5 import {GestureHandlerRootView} from 'react-native-gesture-handler' 5 6 import SplashScreen from 'react-native-splash-screen' ··· 24 25 .then(store => { 25 26 setRootStore(store) 26 27 SplashScreen.hide() 28 + Linking.getInitialURL().then((url: string | null) => { 29 + if (url) { 30 + store.nav.handleLink(url) 31 + } 32 + }) 33 + Linking.addEventListener('url', ({url}) => { 34 + store.nav.handleLink(url) 35 + }) 27 36 }) 28 37 }, []) 29 38
+18
src/state/models/navigation.ts
··· 222 222 this.tabs.find(t => t.id === ptr[0])?.setTitle(ptr[1], title) 223 223 } 224 224 225 + handleLink(url: string) { 226 + let path 227 + if (url.startsWith('/')) { 228 + path = url 229 + } else if (url.startsWith('http')) { 230 + try { 231 + path = new URL(url).pathname 232 + } catch (e) { 233 + console.error('Invalid url', url, e) 234 + return 235 + } 236 + } else { 237 + console.error('Invalid url', url) 238 + return 239 + } 240 + this.navigate(path) 241 + } 242 + 225 243 // tab management 226 244 // = 227 245
-29
src/state/models/shell-ui.ts
··· 2 2 import {ProfileViewModel} from './profile-view' 3 3 import * as Post from '../../third-party/api/src/client/types/app/bsky/feed/post' 4 4 5 - export interface LinkActionsModelOpts { 6 - newTab?: boolean 7 - } 8 - export class LinkActionsModel { 9 - name = 'link-actions' 10 - newTab: boolean 11 - 12 - constructor( 13 - public href: string, 14 - public title: string, 15 - opts?: LinkActionsModelOpts, 16 - ) { 17 - makeAutoObservable(this) 18 - this.newTab = typeof opts?.newTab === 'boolean' ? opts.newTab : true 19 - } 20 - } 21 - 22 5 export class ConfirmModel { 23 6 name = 'confirm' 24 7 ··· 27 10 public message: string | (() => JSX.Element), 28 11 public onPressConfirm: () => void | Promise<void>, 29 12 ) { 30 - makeAutoObservable(this) 31 - } 32 - } 33 - 34 - export class SharePostModel { 35 - name = 'share-post' 36 - 37 - constructor(public href: string) { 38 13 makeAutoObservable(this) 39 14 } 40 15 } ··· 85 60 export class ShellUiModel { 86 61 isModalActive = false 87 62 activeModal: 88 - | LinkActionsModel 89 63 | ConfirmModel 90 - | SharePostModel 91 64 | EditProfileModel 92 65 | CreateSceneModel 93 66 | ServerInputModel ··· 101 74 102 75 openModal( 103 76 modal: 104 - | LinkActionsModel 105 77 | ConfirmModel 106 - | SharePostModel 107 78 | EditProfileModel 108 79 | CreateSceneModel 109 80 | ServerInputModel,
-72
src/view/com/modals/LinkActions.tsx
··· 1 - import React from 'react' 2 - import Toast from '../util/Toast' 3 - import Clipboard from '@react-native-clipboard/clipboard' 4 - import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' 5 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 6 - import {useStores} from '../../../state' 7 - import {s, colors} from '../../lib/styles' 8 - 9 - export const snapPoints = ['30%'] 10 - 11 - export function Component({ 12 - title, 13 - href, 14 - newTab, 15 - }: { 16 - title: string 17 - href: string 18 - newTab: boolean 19 - }) { 20 - const store = useStores() 21 - 22 - const onPressOpenNewTab = () => { 23 - store.shell.closeModal() 24 - store.nav.newTab(href) 25 - } 26 - 27 - const onPressCopy = () => { 28 - Clipboard.setString(href) 29 - store.shell.closeModal() 30 - Toast.show('Link copied', { 31 - position: Toast.positions.TOP, 32 - }) 33 - } 34 - 35 - return ( 36 - <View> 37 - <Text style={[s.textCenter, s.bold, s.mb10, s.f16]}>{title || href}</Text> 38 - <View style={s.p10}> 39 - {newTab ? ( 40 - <TouchableOpacity onPress={onPressOpenNewTab} style={styles.btn}> 41 - <FontAwesomeIcon 42 - icon="arrow-up-right-from-square" 43 - style={styles.icon} 44 - /> 45 - <Text style={[s.f16, s.black]}>Open in new tab</Text> 46 - </TouchableOpacity> 47 - ) : undefined} 48 - <TouchableOpacity onPress={onPressCopy} style={styles.btn}> 49 - <FontAwesomeIcon icon="link" style={styles.icon} /> 50 - <Text style={[s.f16, s.black]}>Copy to clipboard</Text> 51 - </TouchableOpacity> 52 - </View> 53 - </View> 54 - ) 55 - } 56 - 57 - const styles = StyleSheet.create({ 58 - btn: { 59 - flexDirection: 'row', 60 - alignItems: 'center', 61 - justifyContent: 'center', 62 - width: '100%', 63 - borderColor: colors.gray5, 64 - borderWidth: 1, 65 - borderRadius: 4, 66 - padding: 10, 67 - marginBottom: 10, 68 - }, 69 - icon: { 70 - marginRight: 8, 71 - }, 72 - })
+1 -17
src/view/com/modals/Modal.tsx
··· 7 7 8 8 import * as models from '../../../state/models/shell-ui' 9 9 10 - import * as LinkActionsModal from './LinkActions' 11 10 import * as ConfirmModal from './Confirm' 12 - import * as SharePostModal from './SharePost.native' 13 11 import * as EditProfileModal from './EditProfile' 14 12 import * as CreateSceneModal from './CreateScene' 15 13 import * as InviteToSceneModal from './InviteToScene' ··· 41 39 42 40 let snapPoints: (string | number)[] = CLOSED_SNAPPOINTS 43 41 let element 44 - if (store.shell.activeModal?.name === 'link-actions') { 45 - snapPoints = LinkActionsModal.snapPoints 46 - element = ( 47 - <LinkActionsModal.Component 48 - {...(store.shell.activeModal as models.LinkActionsModel)} 49 - /> 50 - ) 51 - } else if (store.shell.activeModal?.name === 'confirm') { 42 + if (store.shell.activeModal?.name === 'confirm') { 52 43 snapPoints = ConfirmModal.snapPoints 53 44 element = ( 54 45 <ConfirmModal.Component 55 46 {...(store.shell.activeModal as models.ConfirmModel)} 56 - /> 57 - ) 58 - } else if (store.shell.activeModal?.name === 'share-post') { 59 - snapPoints = SharePostModal.snapPoints 60 - element = ( 61 - <SharePostModal.Component 62 - {...(store.shell.activeModal as models.SharePostModel)} 63 47 /> 64 48 ) 65 49 } else if (store.shell.activeModal?.name === 'edit-profile') {
-43
src/view/com/modals/SharePost.native.tsx
··· 1 - import React from 'react' 2 - import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native' 3 - import Toast from '../util/Toast' 4 - import Clipboard from '@react-native-clipboard/clipboard' 5 - import {s} from '../../lib/styles' 6 - import {useStores} from '../../../state' 7 - 8 - export const snapPoints = ['30%'] 9 - 10 - export function Component({href}: {href: string}) { 11 - const store = useStores() 12 - const onPressCopy = () => { 13 - Clipboard.setString(href) 14 - Toast.show('Link copied', { 15 - position: Toast.positions.TOP, 16 - }) 17 - store.shell.closeModal() 18 - } 19 - const onClose = () => store.shell.closeModal() 20 - 21 - return ( 22 - <View> 23 - <Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text> 24 - <Text style={[s.textCenter, s.mb10]}>{href}</Text> 25 - <Button title="Copy to clipboard" onPress={onPressCopy} /> 26 - <View style={s.p10}> 27 - <TouchableOpacity onPress={onClose} style={styles.closeBtn}> 28 - <Text style={s.textCenter}>Close</Text> 29 - </TouchableOpacity> 30 - </View> 31 - </View> 32 - ) 33 - } 34 - 35 - const styles = StyleSheet.create({ 36 - closeBtn: { 37 - width: '100%', 38 - borderColor: '#000', 39 - borderWidth: 1, 40 - borderRadius: 4, 41 - padding: 10, 42 - }, 43 - })
-57
src/view/com/modals/SharePost.tsx
··· 1 - import React, {forwardRef, useState, useImperativeHandle} from 'react' 2 - import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native' 3 - import {Modal} from './WebModal' 4 - import Toast from '../util/Toast' 5 - import {s} from '../../lib/styles' 6 - 7 - export const ShareModal = forwardRef(function ShareModal({}: {}, ref) { 8 - const [isOpen, setIsOpen] = useState<boolean>(false) 9 - const [uri, setUri] = useState<string>('') 10 - 11 - useImperativeHandle(ref, () => ({ 12 - open(uri: string) { 13 - console.log('sharing', uri) 14 - setUri(uri) 15 - setIsOpen(true) 16 - }, 17 - })) 18 - 19 - const onPressCopy = () => { 20 - // TODO 21 - Toast.show('Link copied', { 22 - position: Toast.positions.TOP, 23 - }) 24 - } 25 - const onClose = () => { 26 - setIsOpen(false) 27 - } 28 - 29 - return ( 30 - <> 31 - {isOpen && ( 32 - <Modal onClose={onClose}> 33 - <View> 34 - <Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text> 35 - <Text style={[s.textCenter, s.mb10]}>{uri}</Text> 36 - <Button title="Copy to clipboard" onPress={onPressCopy} /> 37 - <View style={s.p10}> 38 - <TouchableOpacity onPress={onClose} style={styles.closeBtn}> 39 - <Text style={s.textCenter}>Close</Text> 40 - </TouchableOpacity> 41 - </View> 42 - </View> 43 - </Modal> 44 - )} 45 - </> 46 - ) 47 - }) 48 - 49 - const styles = StyleSheet.create({ 50 - closeBtn: { 51 - width: '100%', 52 - borderColor: '#000', 53 - borderWidth: 1, 54 - borderRadius: 4, 55 - padding: 10, 56 - }, 57 - })
+1 -11
src/view/com/post-thread/PostThread.tsx
··· 6 6 PostThreadViewPostModel, 7 7 } from '../../../state/models/post-thread-view' 8 8 import {useStores} from '../../../state' 9 - import {SharePostModel} from '../../../state/models/shell-ui' 10 9 import {PostThreadItem} from './PostThreadItem' 11 10 import {ErrorMessage} from '../util/ErrorMessage' 12 11 ··· 17 16 uri: string 18 17 view: PostThreadViewModel 19 18 }) { 20 - const store = useStores() 21 - 22 - const onPressShare = (uri: string) => { 23 - store.shell.openModal(new SharePostModel(uri)) 24 - } 25 19 const onRefresh = () => { 26 20 view?.refresh().catch(err => console.error('Failed to refresh', err)) 27 21 } ··· 55 49 // = 56 50 const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] 57 51 const renderItem = ({item}: {item: PostThreadViewPostModel}) => ( 58 - <PostThreadItem 59 - item={item} 60 - onPressShare={onPressShare} 61 - onPostReply={onRefresh} 62 - /> 52 + <PostThreadItem item={item} onPostReply={onRefresh} /> 63 53 ) 64 54 return ( 65 55 <FlatList
-2
src/view/com/post-thread/PostThreadItem.tsx
··· 21 21 22 22 export const PostThreadItem = observer(function PostThreadItem({ 23 23 item, 24 - onPressShare, 25 24 onPostReply, 26 25 }: { 27 26 item: PostThreadViewPostModel 28 - onPressShare: (_uri: string) => void 29 27 onPostReply: () => void 30 28 }) { 31 29 const store = useStores()
+4 -2
src/view/com/util/DropdownBtn.tsx
··· 1 1 import React, {useRef} from 'react' 2 2 import { 3 + Share, 3 4 StyleProp, 4 5 StyleSheet, 5 6 Text, ··· 12 13 import RootSiblings from 'react-native-root-siblings' 13 14 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 14 15 import {colors} from '../../lib/styles' 16 + import {toShareUrl} from '../../lib/strings' 15 17 import {useStores} from '../../../state' 16 - import {SharePostModel, ConfirmModel} from '../../../state/models/shell-ui' 18 + import {ConfirmModel} from '../../../state/models/shell-ui' 17 19 18 20 export interface DropdownItem { 19 21 icon?: IconProp ··· 93 95 icon: 'share', 94 96 label: 'Share...', 95 97 onPress() { 96 - store.shell.openModal(new SharePostModel(itemHref)) 98 + Share.share({url: toShareUrl(itemHref)}) 97 99 }, 98 100 }, 99 101 isAuthor
-1
src/view/com/util/Link.tsx
··· 10 10 } from 'react-native' 11 11 import {useStores} from '../../../state' 12 12 import {RootStoreModel} from '../../../state' 13 - import {LinkActionsModel} from '../../../state/models/shell-ui' 14 13 15 14 export const Link = observer(function Link({ 16 15 style,
+9
src/view/lib/strings.ts
··· 140 140 return url 141 141 } 142 142 } 143 + 144 + export function toShareUrl(url: string) { 145 + if (!url.startsWith('https')) { 146 + const urlp = new URL('https://bsky.app') 147 + urlp.pathname = url 148 + url = urlp.toString() 149 + } 150 + return url 151 + }
+3 -8
src/view/shell/mobile/TabsSelector.tsx
··· 2 2 import {observer} from 'mobx-react-lite' 3 3 import { 4 4 ScrollView, 5 + Share, 5 6 StyleSheet, 6 7 Text, 7 8 TouchableWithoutFeedback, ··· 20 21 import Swipeable from 'react-native-gesture-handler/Swipeable' 21 22 import {useStores} from '../../../state' 22 23 import {s, colors} from '../../lib/styles' 24 + import {toShareUrl} from '../../lib/strings' 23 25 import {match} from '../../routes' 24 - import {LinkActionsModel} from '../../../state/models/shell-ui' 25 26 26 27 const TAB_HEIGHT = 42 27 28 ··· 69 70 } 70 71 const onPressShareTab = () => { 71 72 onClose() 72 - store.shell.openModal( 73 - new LinkActionsModel( 74 - store.nav.tab.current.url, 75 - store.nav.tab.current.title || 'This Page', 76 - {newTab: false}, 77 - ), 78 - ) 73 + Share.share({url: toShareUrl(store.nav.tab.current.url)}) 79 74 } 80 75 const onPressChangeTab = (tabIndex: number) => { 81 76 store.nav.setActiveTab(tabIndex)