Add web linking and proper share controls

zio/stable
Paul Frazee 2022-11-21 16:07:26 -06:00
parent 39058cd36a
commit ed146a582c
15 changed files with 50 additions and 243 deletions

View File

@ -13,6 +13,7 @@
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
5CEAE7B7A55582F96F1D5952 /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB672808307A6013805A3FE /* libPods-app.a */; }; 5CEAE7B7A55582F96F1D5952 /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB672808307A6013805A3FE /* libPods-app.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 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 */; }; E4BD704B285AD57E00A8FED9 /* AppSecureRandomModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E4BD704A285AD57E00A8FED9 /* AppSecureRandomModule.m */; };
E4BD704C285AD57E00A8FED9 /* 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 */; }; FEB90D21557517F9279AECA4 /* libPods-app-appTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BAD3BC60FA05CF2D4F6F9BA2 /* libPods-app-appTests.a */; };
@ -248,6 +249,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E4BBD590292C1F5200296224 /* app.entitlements in Resources */,
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
); );

View File

@ -3,6 +3,8 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.developer.associated-domains</key> <key>com.apple.developer.associated-domains</key>
<array/> <array>
<string>applinks:bsky.app</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -1,5 +1,6 @@
import 'react-native-url-polyfill/auto' import 'react-native-url-polyfill/auto'
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import {Linking} from 'react-native'
import {RootSiblingParent} from 'react-native-root-siblings' import {RootSiblingParent} from 'react-native-root-siblings'
import {GestureHandlerRootView} from 'react-native-gesture-handler' import {GestureHandlerRootView} from 'react-native-gesture-handler'
import SplashScreen from 'react-native-splash-screen' import SplashScreen from 'react-native-splash-screen'
@ -24,6 +25,14 @@ function App() {
.then(store => { .then(store => {
setRootStore(store) setRootStore(store)
SplashScreen.hide() SplashScreen.hide()
Linking.getInitialURL().then((url: string | null) => {
if (url) {
store.nav.handleLink(url)
}
})
Linking.addEventListener('url', ({url}) => {
store.nav.handleLink(url)
})
}) })
}, []) }, [])

View File

@ -222,6 +222,24 @@ export class NavigationModel {
this.tabs.find(t => t.id === ptr[0])?.setTitle(ptr[1], title) 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 // tab management
// = // =

View File

@ -2,23 +2,6 @@ import {makeAutoObservable} from 'mobx'
import {ProfileViewModel} from './profile-view' import {ProfileViewModel} from './profile-view'
import * as Post from '../../third-party/api/src/client/types/app/bsky/feed/post' 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 { export class ConfirmModel {
name = 'confirm' name = 'confirm'
@ -31,14 +14,6 @@ export class ConfirmModel {
} }
} }
export class SharePostModel {
name = 'share-post'
constructor(public href: string) {
makeAutoObservable(this)
}
}
export class EditProfileModel { export class EditProfileModel {
name = 'edit-profile' name = 'edit-profile'
@ -85,9 +60,7 @@ export interface ComposerOpts {
export class ShellUiModel { export class ShellUiModel {
isModalActive = false isModalActive = false
activeModal: activeModal:
| LinkActionsModel
| ConfirmModel | ConfirmModel
| SharePostModel
| EditProfileModel | EditProfileModel
| CreateSceneModel | CreateSceneModel
| ServerInputModel | ServerInputModel
@ -101,9 +74,7 @@ export class ShellUiModel {
openModal( openModal(
modal: modal:
| LinkActionsModel
| ConfirmModel | ConfirmModel
| SharePostModel
| EditProfileModel | EditProfileModel
| CreateSceneModel | CreateSceneModel
| ServerInputModel, | ServerInputModel,

View File

@ -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,
},
})

View File

@ -7,9 +7,7 @@ import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as models from '../../../state/models/shell-ui' import * as models from '../../../state/models/shell-ui'
import * as LinkActionsModal from './LinkActions'
import * as ConfirmModal from './Confirm' import * as ConfirmModal from './Confirm'
import * as SharePostModal from './SharePost.native'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as CreateSceneModal from './CreateScene' import * as CreateSceneModal from './CreateScene'
import * as InviteToSceneModal from './InviteToScene' import * as InviteToSceneModal from './InviteToScene'
@ -41,27 +39,13 @@ export const Modal = observer(function Modal() {
let snapPoints: (string | number)[] = CLOSED_SNAPPOINTS let snapPoints: (string | number)[] = CLOSED_SNAPPOINTS
let element let element
if (store.shell.activeModal?.name === 'link-actions') { if (store.shell.activeModal?.name === 'confirm') {
snapPoints = LinkActionsModal.snapPoints
element = (
<LinkActionsModal.Component
{...(store.shell.activeModal as models.LinkActionsModel)}
/>
)
} else if (store.shell.activeModal?.name === 'confirm') {
snapPoints = ConfirmModal.snapPoints snapPoints = ConfirmModal.snapPoints
element = ( element = (
<ConfirmModal.Component <ConfirmModal.Component
{...(store.shell.activeModal as models.ConfirmModel)} {...(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') { } else if (store.shell.activeModal?.name === 'edit-profile') {
snapPoints = EditProfileModal.snapPoints snapPoints = EditProfileModal.snapPoints
element = ( element = (

View File

@ -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,
},
})

View File

@ -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,
},
})

View File

@ -6,7 +6,6 @@ import {
PostThreadViewPostModel, PostThreadViewPostModel,
} from '../../../state/models/post-thread-view' } from '../../../state/models/post-thread-view'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {SharePostModel} from '../../../state/models/shell-ui'
import {PostThreadItem} from './PostThreadItem' import {PostThreadItem} from './PostThreadItem'
import {ErrorMessage} from '../util/ErrorMessage' import {ErrorMessage} from '../util/ErrorMessage'
@ -17,11 +16,6 @@ export const PostThread = observer(function PostThread({
uri: string uri: string
view: PostThreadViewModel view: PostThreadViewModel
}) { }) {
const store = useStores()
const onPressShare = (uri: string) => {
store.shell.openModal(new SharePostModel(uri))
}
const onRefresh = () => { const onRefresh = () => {
view?.refresh().catch(err => console.error('Failed to refresh', err)) 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 posts = view.thread ? Array.from(flattenThread(view.thread)) : []
const renderItem = ({item}: {item: PostThreadViewPostModel}) => ( const renderItem = ({item}: {item: PostThreadViewPostModel}) => (
<PostThreadItem <PostThreadItem item={item} onPostReply={onRefresh} />
item={item}
onPressShare={onPressShare}
onPostReply={onRefresh}
/>
) )
return ( return (
<FlatList <FlatList

View File

@ -21,11 +21,9 @@ const PARENT_REPLY_LINE_LENGTH = 8
export const PostThreadItem = observer(function PostThreadItem({ export const PostThreadItem = observer(function PostThreadItem({
item, item,
onPressShare,
onPostReply, onPostReply,
}: { }: {
item: PostThreadViewPostModel item: PostThreadViewPostModel
onPressShare: (_uri: string) => void
onPostReply: () => void onPostReply: () => void
}) { }) {
const store = useStores() const store = useStores()

View File

@ -1,5 +1,6 @@
import React, {useRef} from 'react' import React, {useRef} from 'react'
import { import {
Share,
StyleProp, StyleProp,
StyleSheet, StyleSheet,
Text, Text,
@ -12,8 +13,9 @@ import {IconProp} from '@fortawesome/fontawesome-svg-core'
import RootSiblings from 'react-native-root-siblings' import RootSiblings from 'react-native-root-siblings'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors} from '../../lib/styles' import {colors} from '../../lib/styles'
import {toShareUrl} from '../../lib/strings'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {SharePostModel, ConfirmModel} from '../../../state/models/shell-ui' import {ConfirmModel} from '../../../state/models/shell-ui'
export interface DropdownItem { export interface DropdownItem {
icon?: IconProp icon?: IconProp
@ -93,7 +95,7 @@ export function PostDropdownBtn({
icon: 'share', icon: 'share',
label: 'Share...', label: 'Share...',
onPress() { onPress() {
store.shell.openModal(new SharePostModel(itemHref)) Share.share({url: toShareUrl(itemHref)})
}, },
}, },
isAuthor isAuthor

View File

@ -10,7 +10,6 @@ import {
} from 'react-native' } from 'react-native'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {RootStoreModel} from '../../../state' import {RootStoreModel} from '../../../state'
import {LinkActionsModel} from '../../../state/models/shell-ui'
export const Link = observer(function Link({ export const Link = observer(function Link({
style, style,

View File

@ -140,3 +140,12 @@ export function toNiceDomain(url: string): string {
return url 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
}

View File

@ -2,6 +2,7 @@ import React, {createRef, useRef, useMemo, useEffect, useState} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {
ScrollView, ScrollView,
Share,
StyleSheet, StyleSheet,
Text, Text,
TouchableWithoutFeedback, TouchableWithoutFeedback,
@ -20,8 +21,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import Swipeable from 'react-native-gesture-handler/Swipeable' import Swipeable from 'react-native-gesture-handler/Swipeable'
import {useStores} from '../../../state' import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {toShareUrl} from '../../lib/strings'
import {match} from '../../routes' import {match} from '../../routes'
import {LinkActionsModel} from '../../../state/models/shell-ui'
const TAB_HEIGHT = 42 const TAB_HEIGHT = 42
@ -69,13 +70,7 @@ export const TabsSelector = observer(
} }
const onPressShareTab = () => { const onPressShareTab = () => {
onClose() onClose()
store.shell.openModal( Share.share({url: toShareUrl(store.nav.tab.current.url)})
new LinkActionsModel(
store.nav.tab.current.url,
store.nav.tab.current.title || 'This Page',
{newTab: false},
),
)
} }
const onPressChangeTab = (tabIndex: number) => { const onPressChangeTab = (tabIndex: number) => {
store.nav.setActiveTab(tabIndex) store.nav.setActiveTab(tabIndex)