diff --git a/ios/app.xcodeproj/project.pbxproj b/ios/app.xcodeproj/project.pbxproj
index bc5be5f299..6ae95c2f4f 100644
--- a/ios/app.xcodeproj/project.pbxproj
+++ b/ios/app.xcodeproj/project.pbxproj
@@ -13,6 +13,7 @@
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
5CEAE7B7A55582F96F1D5952 /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB672808307A6013805A3FE /* libPods-app.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
+ E4BBD590292C1F5200296224 /* app.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = E4437C9E28581FA7006DA9E7 /* app.entitlements */; };
E4BD704B285AD57E00A8FED9 /* AppSecureRandomModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E4BD704A285AD57E00A8FED9 /* AppSecureRandomModule.m */; };
E4BD704C285AD57E00A8FED9 /* AppSecureRandomModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E4BD704A285AD57E00A8FED9 /* AppSecureRandomModule.m */; };
FEB90D21557517F9279AECA4 /* libPods-app-appTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BAD3BC60FA05CF2D4F6F9BA2 /* libPods-app-appTests.a */; };
@@ -248,6 +249,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ E4BBD590292C1F5200296224 /* app.entitlements in Resources */,
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
);
diff --git a/ios/app/app.entitlements b/ios/app/app.entitlements
index 314291360d..05af7cbda5 100644
--- a/ios/app/app.entitlements
+++ b/ios/app/app.entitlements
@@ -3,6 +3,8 @@
com.apple.developer.associated-domains
-
+
+ applinks:bsky.app
+
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 3d3e5f1b0c..65a77c3dce 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -1,5 +1,6 @@
import 'react-native-url-polyfill/auto'
import React, {useState, useEffect} from 'react'
+import {Linking} from 'react-native'
import {RootSiblingParent} from 'react-native-root-siblings'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import SplashScreen from 'react-native-splash-screen'
@@ -24,6 +25,14 @@ function App() {
.then(store => {
setRootStore(store)
SplashScreen.hide()
+ Linking.getInitialURL().then((url: string | null) => {
+ if (url) {
+ store.nav.handleLink(url)
+ }
+ })
+ Linking.addEventListener('url', ({url}) => {
+ store.nav.handleLink(url)
+ })
})
}, [])
diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts
index 0ec097afca..a4d7d443b0 100644
--- a/src/state/models/navigation.ts
+++ b/src/state/models/navigation.ts
@@ -222,6 +222,24 @@ export class NavigationModel {
this.tabs.find(t => t.id === ptr[0])?.setTitle(ptr[1], title)
}
+ handleLink(url: string) {
+ let path
+ if (url.startsWith('/')) {
+ path = url
+ } else if (url.startsWith('http')) {
+ try {
+ path = new URL(url).pathname
+ } catch (e) {
+ console.error('Invalid url', url, e)
+ return
+ }
+ } else {
+ console.error('Invalid url', url)
+ return
+ }
+ this.navigate(path)
+ }
+
// tab management
// =
diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts
index 73b1bd56e7..13d7207302 100644
--- a/src/state/models/shell-ui.ts
+++ b/src/state/models/shell-ui.ts
@@ -2,23 +2,6 @@ import {makeAutoObservable} from 'mobx'
import {ProfileViewModel} from './profile-view'
import * as Post from '../../third-party/api/src/client/types/app/bsky/feed/post'
-export interface LinkActionsModelOpts {
- newTab?: boolean
-}
-export class LinkActionsModel {
- name = 'link-actions'
- newTab: boolean
-
- constructor(
- public href: string,
- public title: string,
- opts?: LinkActionsModelOpts,
- ) {
- makeAutoObservable(this)
- this.newTab = typeof opts?.newTab === 'boolean' ? opts.newTab : true
- }
-}
-
export class ConfirmModel {
name = 'confirm'
@@ -31,14 +14,6 @@ export class ConfirmModel {
}
}
-export class SharePostModel {
- name = 'share-post'
-
- constructor(public href: string) {
- makeAutoObservable(this)
- }
-}
-
export class EditProfileModel {
name = 'edit-profile'
@@ -85,9 +60,7 @@ export interface ComposerOpts {
export class ShellUiModel {
isModalActive = false
activeModal:
- | LinkActionsModel
| ConfirmModel
- | SharePostModel
| EditProfileModel
| CreateSceneModel
| ServerInputModel
@@ -101,9 +74,7 @@ export class ShellUiModel {
openModal(
modal:
- | LinkActionsModel
| ConfirmModel
- | SharePostModel
| EditProfileModel
| CreateSceneModel
| ServerInputModel,
diff --git a/src/view/com/modals/LinkActions.tsx b/src/view/com/modals/LinkActions.tsx
deleted file mode 100644
index deb1518ec4..0000000000
--- a/src/view/com/modals/LinkActions.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react'
-import Toast from '../util/Toast'
-import Clipboard from '@react-native-clipboard/clipboard'
-import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useStores} from '../../../state'
-import {s, colors} from '../../lib/styles'
-
-export const snapPoints = ['30%']
-
-export function Component({
- title,
- href,
- newTab,
-}: {
- title: string
- href: string
- newTab: boolean
-}) {
- const store = useStores()
-
- const onPressOpenNewTab = () => {
- store.shell.closeModal()
- store.nav.newTab(href)
- }
-
- const onPressCopy = () => {
- Clipboard.setString(href)
- store.shell.closeModal()
- Toast.show('Link copied', {
- position: Toast.positions.TOP,
- })
- }
-
- return (
-
- {title || href}
-
- {newTab ? (
-
-
- Open in new tab
-
- ) : undefined}
-
-
- Copy to clipboard
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- btn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- width: '100%',
- borderColor: colors.gray5,
- borderWidth: 1,
- borderRadius: 4,
- padding: 10,
- marginBottom: 10,
- },
- icon: {
- marginRight: 8,
- },
-})
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 3317fef085..9d70334115 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -7,9 +7,7 @@ import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as models from '../../../state/models/shell-ui'
-import * as LinkActionsModal from './LinkActions'
import * as ConfirmModal from './Confirm'
-import * as SharePostModal from './SharePost.native'
import * as EditProfileModal from './EditProfile'
import * as CreateSceneModal from './CreateScene'
import * as InviteToSceneModal from './InviteToScene'
@@ -41,27 +39,13 @@ export const Modal = observer(function Modal() {
let snapPoints: (string | number)[] = CLOSED_SNAPPOINTS
let element
- if (store.shell.activeModal?.name === 'link-actions') {
- snapPoints = LinkActionsModal.snapPoints
- element = (
-
- )
- } else if (store.shell.activeModal?.name === 'confirm') {
+ if (store.shell.activeModal?.name === 'confirm') {
snapPoints = ConfirmModal.snapPoints
element = (
)
- } else if (store.shell.activeModal?.name === 'share-post') {
- snapPoints = SharePostModal.snapPoints
- element = (
-
- )
} else if (store.shell.activeModal?.name === 'edit-profile') {
snapPoints = EditProfileModal.snapPoints
element = (
diff --git a/src/view/com/modals/SharePost.native.tsx b/src/view/com/modals/SharePost.native.tsx
deleted file mode 100644
index 01692fb74f..0000000000
--- a/src/view/com/modals/SharePost.native.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react'
-import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
-import Toast from '../util/Toast'
-import Clipboard from '@react-native-clipboard/clipboard'
-import {s} from '../../lib/styles'
-import {useStores} from '../../../state'
-
-export const snapPoints = ['30%']
-
-export function Component({href}: {href: string}) {
- const store = useStores()
- const onPressCopy = () => {
- Clipboard.setString(href)
- Toast.show('Link copied', {
- position: Toast.positions.TOP,
- })
- store.shell.closeModal()
- }
- const onClose = () => store.shell.closeModal()
-
- return (
-
- Share this post
- {href}
-
-
-
- Close
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- closeBtn: {
- width: '100%',
- borderColor: '#000',
- borderWidth: 1,
- borderRadius: 4,
- padding: 10,
- },
-})
diff --git a/src/view/com/modals/SharePost.tsx b/src/view/com/modals/SharePost.tsx
deleted file mode 100644
index d6d4bb5c18..0000000000
--- a/src/view/com/modals/SharePost.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, {forwardRef, useState, useImperativeHandle} from 'react'
-import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native'
-import {Modal} from './WebModal'
-import Toast from '../util/Toast'
-import {s} from '../../lib/styles'
-
-export const ShareModal = forwardRef(function ShareModal({}: {}, ref) {
- const [isOpen, setIsOpen] = useState(false)
- const [uri, setUri] = useState('')
-
- useImperativeHandle(ref, () => ({
- open(uri: string) {
- console.log('sharing', uri)
- setUri(uri)
- setIsOpen(true)
- },
- }))
-
- const onPressCopy = () => {
- // TODO
- Toast.show('Link copied', {
- position: Toast.positions.TOP,
- })
- }
- const onClose = () => {
- setIsOpen(false)
- }
-
- return (
- <>
- {isOpen && (
-
-
- Share this post
- {uri}
-
-
-
- Close
-
-
-
-
- )}
- >
- )
-})
-
-const styles = StyleSheet.create({
- closeBtn: {
- width: '100%',
- borderColor: '#000',
- borderWidth: 1,
- borderRadius: 4,
- padding: 10,
- },
-})
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 6b7f96e068..d819cc76c7 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -6,7 +6,6 @@ import {
PostThreadViewPostModel,
} from '../../../state/models/post-thread-view'
import {useStores} from '../../../state'
-import {SharePostModel} from '../../../state/models/shell-ui'
import {PostThreadItem} from './PostThreadItem'
import {ErrorMessage} from '../util/ErrorMessage'
@@ -17,11 +16,6 @@ export const PostThread = observer(function PostThread({
uri: string
view: PostThreadViewModel
}) {
- const store = useStores()
-
- const onPressShare = (uri: string) => {
- store.shell.openModal(new SharePostModel(uri))
- }
const onRefresh = () => {
view?.refresh().catch(err => console.error('Failed to refresh', err))
}
@@ -55,11 +49,7 @@ export const PostThread = observer(function PostThread({
// =
const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
-
+
)
return (
void
onPostReply: () => void
}) {
const store = useStores()
diff --git a/src/view/com/util/DropdownBtn.tsx b/src/view/com/util/DropdownBtn.tsx
index bef193f1d2..99cc00bd2d 100644
--- a/src/view/com/util/DropdownBtn.tsx
+++ b/src/view/com/util/DropdownBtn.tsx
@@ -1,5 +1,6 @@
import React, {useRef} from 'react'
import {
+ Share,
StyleProp,
StyleSheet,
Text,
@@ -12,8 +13,9 @@ import {IconProp} from '@fortawesome/fontawesome-svg-core'
import RootSiblings from 'react-native-root-siblings'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors} from '../../lib/styles'
+import {toShareUrl} from '../../lib/strings'
import {useStores} from '../../../state'
-import {SharePostModel, ConfirmModel} from '../../../state/models/shell-ui'
+import {ConfirmModel} from '../../../state/models/shell-ui'
export interface DropdownItem {
icon?: IconProp
@@ -93,7 +95,7 @@ export function PostDropdownBtn({
icon: 'share',
label: 'Share...',
onPress() {
- store.shell.openModal(new SharePostModel(itemHref))
+ Share.share({url: toShareUrl(itemHref)})
},
},
isAuthor
diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx
index ff3d25cb5b..8f94115e12 100644
--- a/src/view/com/util/Link.tsx
+++ b/src/view/com/util/Link.tsx
@@ -10,7 +10,6 @@ import {
} from 'react-native'
import {useStores} from '../../../state'
import {RootStoreModel} from '../../../state'
-import {LinkActionsModel} from '../../../state/models/shell-ui'
export const Link = observer(function Link({
style,
diff --git a/src/view/lib/strings.ts b/src/view/lib/strings.ts
index b12fbd3987..84ee07f37c 100644
--- a/src/view/lib/strings.ts
+++ b/src/view/lib/strings.ts
@@ -140,3 +140,12 @@ export function toNiceDomain(url: string): string {
return url
}
}
+
+export function toShareUrl(url: string) {
+ if (!url.startsWith('https')) {
+ const urlp = new URL('https://bsky.app')
+ urlp.pathname = url
+ url = urlp.toString()
+ }
+ return url
+}
diff --git a/src/view/shell/mobile/TabsSelector.tsx b/src/view/shell/mobile/TabsSelector.tsx
index 1210da91f2..4b246728f6 100644
--- a/src/view/shell/mobile/TabsSelector.tsx
+++ b/src/view/shell/mobile/TabsSelector.tsx
@@ -2,6 +2,7 @@ import React, {createRef, useRef, useMemo, useEffect, useState} from 'react'
import {observer} from 'mobx-react-lite'
import {
ScrollView,
+ Share,
StyleSheet,
Text,
TouchableWithoutFeedback,
@@ -20,8 +21,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import Swipeable from 'react-native-gesture-handler/Swipeable'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
+import {toShareUrl} from '../../lib/strings'
import {match} from '../../routes'
-import {LinkActionsModel} from '../../../state/models/shell-ui'
const TAB_HEIGHT = 42
@@ -69,13 +70,7 @@ export const TabsSelector = observer(
}
const onPressShareTab = () => {
onClose()
- store.shell.openModal(
- new LinkActionsModel(
- store.nav.tab.current.url,
- store.nav.tab.current.title || 'This Page',
- {newTab: false},
- ),
- )
+ Share.share({url: toShareUrl(store.nav.tab.current.url)})
}
const onPressChangeTab = (tabIndex: number) => {
store.nav.setActiveTab(tabIndex)