diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-11-21 16:07:26 -0600 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-11-21 16:07:26 -0600 |
commit | ed146a582c140b9a472298390dafbc07bd06cf60 (patch) | |
tree | bd540e4a84244fdbdbdf5fde412fc4a179b6dae5 /src | |
parent | 39058cd36a9839df0e0c7e30ba486a09e30f169c (diff) | |
download | voidsky-ed146a582c140b9a472298390dafbc07bd06cf60.tar.zst |
Add web linking and proper share controls
Diffstat (limited to 'src')
-rw-r--r-- | src/App.native.tsx | 9 | ||||
-rw-r--r-- | src/state/models/navigation.ts | 18 | ||||
-rw-r--r-- | src/state/models/shell-ui.ts | 29 | ||||
-rw-r--r-- | src/view/com/modals/LinkActions.tsx | 72 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 18 | ||||
-rw-r--r-- | src/view/com/modals/SharePost.native.tsx | 43 | ||||
-rw-r--r-- | src/view/com/modals/SharePost.tsx | 57 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 12 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/DropdownBtn.tsx | 6 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 1 | ||||
-rw-r--r-- | src/view/lib/strings.ts | 9 | ||||
-rw-r--r-- | src/view/shell/mobile/TabsSelector.tsx | 11 |
13 files changed, 45 insertions, 242 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index 3d3e5f1b0..65a77c3dc 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 0ec097afc..a4d7d443b 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 73b1bd56e..13d720730 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 deb1518ec..000000000 --- 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 ( - <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, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 3317fef08..9d7033411 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 = ( - <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 = ( diff --git a/src/view/com/modals/SharePost.native.tsx b/src/view/com/modals/SharePost.native.tsx deleted file mode 100644 index 01692fb74..000000000 --- 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 ( - <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, - }, -}) diff --git a/src/view/com/modals/SharePost.tsx b/src/view/com/modals/SharePost.tsx deleted file mode 100644 index d6d4bb5c1..000000000 --- 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<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, - }, -}) diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 6b7f96e06..d819cc76c 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}) => ( - <PostThreadItem - item={item} - onPressShare={onPressShare} - onPostReply={onRefresh} - /> + <PostThreadItem item={item} onPostReply={onRefresh} /> ) return ( <FlatList diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 0c4565c51..665c87b46 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -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() diff --git a/src/view/com/util/DropdownBtn.tsx b/src/view/com/util/DropdownBtn.tsx index bef193f1d..99cc00bd2 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 ff3d25cb5..8f94115e1 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 b12fbd398..84ee07f37 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 1210da91f..4b246728f 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) |