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 */; };
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 */,
);

View File

@ -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>

View File

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

View File

@ -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
// =

View File

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

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 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 = (

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

View File

@ -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()

View File

@ -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

View File

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

View File

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

View File

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