Add web linking and proper share controls
parent
39058cd36a
commit
ed146a582c
|
@ -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 */,
|
||||
);
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array/>
|
||||
<array>
|
||||
<string>applinks:bsky.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
|
|
@ -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
|
||||
// =
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
<View>
|
||||
<Text style={[s.textCenter, s.bold, s.mb10, s.f16]}>{title || href}</Text>
|
||||
<View style={s.p10}>
|
||||
{newTab ? (
|
||||
<TouchableOpacity onPress={onPressOpenNewTab} style={styles.btn}>
|
||||
<FontAwesomeIcon
|
||||
icon="arrow-up-right-from-square"
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={[s.f16, s.black]}>Open in new tab</Text>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
<TouchableOpacity onPress={onPressCopy} style={styles.btn}>
|
||||
<FontAwesomeIcon icon="link" style={styles.icon} />
|
||||
<Text style={[s.f16, s.black]}>Copy to clipboard</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
|
@ -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 = (
|
||||
<LinkActionsModal.Component
|
||||
{...(store.shell.activeModal as models.LinkActionsModel)}
|
||||
/>
|
||||
)
|
||||
} else if (store.shell.activeModal?.name === 'confirm') {
|
||||
if (store.shell.activeModal?.name === 'confirm') {
|
||||
snapPoints = ConfirmModal.snapPoints
|
||||
element = (
|
||||
<ConfirmModal.Component
|
||||
{...(store.shell.activeModal as models.ConfirmModel)}
|
||||
/>
|
||||
)
|
||||
} else if (store.shell.activeModal?.name === 'share-post') {
|
||||
snapPoints = SharePostModal.snapPoints
|
||||
element = (
|
||||
<SharePostModal.Component
|
||||
{...(store.shell.activeModal as models.SharePostModel)}
|
||||
/>
|
||||
)
|
||||
} else if (store.shell.activeModal?.name === 'edit-profile') {
|
||||
snapPoints = EditProfileModal.snapPoints
|
||||
element = (
|
||||
|
|
|
@ -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 (
|
||||
<View>
|
||||
<Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text>
|
||||
<Text style={[s.textCenter, s.mb10]}>{href}</Text>
|
||||
<Button title="Copy to clipboard" onPress={onPressCopy} />
|
||||
<View style={s.p10}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
|
||||
<Text style={s.textCenter}>Close</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
closeBtn: {
|
||||
width: '100%',
|
||||
borderColor: '#000',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
padding: 10,
|
||||
},
|
||||
})
|
|
@ -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<boolean>(false)
|
||||
const [uri, setUri] = useState<string>('')
|
||||
|
||||
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 && (
|
||||
<Modal onClose={onClose}>
|
||||
<View>
|
||||
<Text style={[s.textCenter, s.bold, s.mb10]}>Share this post</Text>
|
||||
<Text style={[s.textCenter, s.mb10]}>{uri}</Text>
|
||||
<Button title="Copy to clipboard" onPress={onPressCopy} />
|
||||
<View style={s.p10}>
|
||||
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
|
||||
<Text style={s.textCenter}>Close</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
closeBtn: {
|
||||
width: '100%',
|
||||
borderColor: '#000',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
padding: 10,
|
||||
},
|
||||
})
|
|
@ -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}) => (
|
||||
<PostThreadItem
|
||||
item={item}
|
||||
onPressShare={onPressShare}
|
||||
onPostReply={onRefresh}
|
||||
/>
|
||||
<PostThreadItem item={item} onPostReply={onRefresh} />
|
||||
)
|
||||
return (
|
||||
<FlatList
|
||||
|
|
|
@ -21,11 +21,9 @@ const PARENT_REPLY_LINE_LENGTH = 8
|
|||
|
||||
export const PostThreadItem = observer(function PostThreadItem({
|
||||
item,
|
||||
onPressShare,
|
||||
onPostReply,
|
||||
}: {
|
||||
item: PostThreadViewPostModel
|
||||
onPressShare: (_uri: string) => void
|
||||
onPostReply: () => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue