diff options
Diffstat (limited to 'src')
54 files changed, 1374 insertions, 1038 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index 4309fa3c3..a220fab3b 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -5,7 +5,7 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler' import {whenWebCrypto} from './platform/polyfills.native' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' -import * as Routes from './view/routes' +import {MobileShell} from './view/shell/mobile' function App() { const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( @@ -31,7 +31,7 @@ function App() { <GestureHandlerRootView style={{flex: 1}}> <RootSiblingParent> <RootStoreProvider value={rootStore}> - <Routes.Root /> + <MobileShell /> </RootStoreProvider> </RootSiblingParent> </GestureHandlerRootView> diff --git a/src/App.web.tsx b/src/App.web.tsx index 9a6fedd5a..06da5e4e3 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -1,7 +1,7 @@ import React, {useState, useEffect} from 'react' import * as view from './view/index' import {RootStoreModel, setupState, RootStoreProvider} from './state' -import * as Routes from './view/routes' +import {DesktopWebShell} from './view/shell/desktop-web' import Toast from './view/com/util/Toast' function App() { @@ -22,7 +22,7 @@ function App() { return ( <RootStoreProvider value={rootStore}> - <Routes.Root /> + <DesktopWebShell /> <Toast.ToastContainer /> </RootStoreProvider> ) diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts new file mode 100644 index 000000000..d5338ac05 --- /dev/null +++ b/src/state/models/navigation.ts @@ -0,0 +1,251 @@ +import {makeAutoObservable} from 'mobx' +import {isObj, hasProp} from '../lib/type-guards' + +let __tabId = 0 +function genTabId() { + return ++__tabId +} + +interface HistoryItem { + url: string + ts: number + title?: string +} + +export class NavigationTabModel { + id = genTabId() + history: HistoryItem[] = [{url: '/', ts: Date.now()}] + index = 0 + + constructor() { + makeAutoObservable(this, { + serialize: false, + hydrate: false, + }) + } + + // accessors + // = + + get current() { + return this.history[this.index] + } + + get canGoBack() { + return this.index > 0 + } + + get canGoForward() { + return this.index < this.history.length - 1 + } + + getBackList(n: number) { + const start = Math.max(this.index - n, 0) + const end = Math.min(this.index, n) + return this.history.slice(start, end).map((item, i) => ({ + url: item.url, + title: item.title, + index: start + i, + })) + } + + get backTen() { + return this.getBackList(10) + } + + getForwardList(n: number) { + const start = Math.min(this.index + 1, this.history.length) + const end = Math.min(this.index + n, this.history.length) + return this.history.slice(start, end).map((item, i) => ({ + url: item.url, + title: item.title, + index: start + i, + })) + } + + get forwardTen() { + return this.getForwardList(10) + } + + // navigation + // = + + navigate(url: string, title?: string) { + if (this.current?.url === url) { + this.refresh() + } else { + if (this.index < this.history.length - 1) { + this.history.length = this.index + 1 + } + this.history.push({url, title, ts: Date.now()}) + this.index = this.history.length - 1 + } + } + + refresh() { + this.history = [ + ...this.history.slice(0, this.index), + {url: this.current.url, title: this.current.title, ts: Date.now()}, + ...this.history.slice(this.index + 1), + ] + } + + goBack() { + if (this.canGoBack) { + this.index-- + } + } + + goForward() { + if (this.canGoForward) { + this.index++ + } + } + + goToIndex(index: number) { + if (index >= 0 && index <= this.history.length - 1) { + this.index = index + } + } + + setTitle(title: string) { + this.current.title = title + } + + // persistence + // = + + serialize(): unknown { + return { + history: this.history, + index: this.index, + } + } + + hydrate(v: unknown) { + this.history = [] + this.index = 0 + if (isObj(v)) { + if (hasProp(v, 'history') && Array.isArray(v.history)) { + for (const item of v.history) { + if ( + isObj(item) && + hasProp(item, 'url') && + typeof item.url === 'string' + ) { + let copy: HistoryItem = { + url: item.url, + ts: + hasProp(item, 'ts') && typeof item.ts === 'number' + ? item.ts + : Date.now(), + } + if (hasProp(item, 'title') && typeof item.title === 'string') { + copy.title = item.title + } + this.history.push(copy) + } + } + } + if (hasProp(v, 'index') && typeof v.index === 'number') { + this.index = v.index + } + if (this.index >= this.history.length - 1) { + this.index = this.history.length - 1 + } + } + } +} + +export class NavigationModel { + tabs: NavigationTabModel[] = [new NavigationTabModel()] + tabIndex = 0 + + constructor() { + makeAutoObservable(this, { + serialize: false, + hydrate: false, + }) + } + + // accessors + // = + + get tab() { + return this.tabs[this.tabIndex] + } + + isCurrentScreen(tabId: number, index: number) { + return this.tab.id === tabId && this.tab.index === index + } + + // navigation + // = + + navigate(url: string, title?: string) { + this.tab.navigate(url, title) + } + + refresh() { + this.tab.refresh() + } + + setTitle(title: string) { + this.tab.setTitle(title) + } + + // tab management + // = + + newTab(url: string, title?: string) { + const tab = new NavigationTabModel() + tab.navigate(url, title) + this.tabs.push(tab) + this.tabIndex = this.tabs.length - 1 + } + + setActiveTab(tabIndex: number) { + this.tabIndex = Math.max(Math.min(tabIndex, this.tabs.length - 1), 0) + } + + closeTab(tabIndex: number) { + this.tabs = [ + ...this.tabs.slice(0, tabIndex), + ...this.tabs.slice(tabIndex + 1), + ] + if (this.tabs.length === 0) { + this.newTab('/') + } else if (this.tabIndex >= this.tabs.length) { + this.tabIndex = this.tabs.length - 1 + } + } + + // persistence + // = + + serialize(): unknown { + return { + tabs: this.tabs.map(t => t.serialize()), + tabIndex: this.tabIndex, + } + } + + hydrate(v: unknown) { + this.tabs.length = 0 + this.tabIndex = 0 + if (isObj(v)) { + if (hasProp(v, 'tabs') && Array.isArray(v.tabs)) { + for (const tab of v.tabs) { + const copy = new NavigationTabModel() + copy.hydrate(tab) + if (copy.history.length) { + this.tabs.push(copy) + } + } + } + if (hasProp(v, 'tabIndex') && typeof v.tabIndex === 'number') { + this.tabIndex = v.tabIndex + } + } + } +} diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index e05c86389..d1e731328 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -7,12 +7,14 @@ import {adx, AdxClient} from '@adxp/mock-api' import {createContext, useContext} from 'react' import {isObj, hasProp} from '../lib/type-guards' import {SessionModel} from './session' +import {NavigationModel} from './navigation' import {MeModel} from './me' import {FeedViewModel} from './feed-view' import {NotificationsViewModel} from './notifications-view' export class RootStoreModel { session = new SessionModel() + nav = new NavigationModel() me = new MeModel(this) homeFeed = new FeedViewModel(this, {}) notesFeed = new NotificationsViewModel(this, {}) @@ -35,6 +37,7 @@ export class RootStoreModel { serialize(): unknown { return { session: this.session.serialize(), + nav: this.nav.serialize(), } } @@ -43,6 +46,9 @@ export class RootStoreModel { if (hasProp(v, 'session')) { this.session.hydrate(v.session) } + if (hasProp(v, 'nav')) { + this.nav.hydrate(v.nav) + } } } } diff --git a/src/view/com/feed/Feed.tsx b/src/view/com/feed/Feed.tsx index 6787b51ae..7c7fea58a 100644 --- a/src/view/com/feed/Feed.tsx +++ b/src/view/com/feed/Feed.tsx @@ -1,18 +1,11 @@ import React, {useRef} from 'react' import {observer} from 'mobx-react-lite' import {Text, View, FlatList} from 'react-native' -import {OnNavigateContent} from '../../routes/types' import {FeedViewModel, FeedViewItemModel} from '../../../state/models/feed-view' import {FeedItem} from './FeedItem' import {ShareModal} from '../modals/SharePost' -export const Feed = observer(function Feed({ - feed, - onNavigateContent, -}: { - feed: FeedViewModel - onNavigateContent: OnNavigateContent -}) { +export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) { const shareSheetRef = useRef<{open: (_uri: string) => void}>() const onPressShare = (uri: string) => { @@ -23,11 +16,7 @@ export const Feed = observer(function Feed({ // renderItem function renders components that follow React performance best practices // like PureComponent, shouldComponentUpdate, etc const renderItem = ({item}: {item: FeedViewItemModel}) => ( - <FeedItem - item={item} - onNavigateContent={onNavigateContent} - onPressShare={onPressShare} - /> + <FeedItem item={item} onPressShare={onPressShare} /> ) const onRefresh = () => { feed.refresh().catch(err => console.error('Failed to refresh', err)) diff --git a/src/view/com/feed/FeedItem.tsx b/src/view/com/feed/FeedItem.tsx index e79c15326..a63fb7a2c 100644 --- a/src/view/com/feed/FeedItem.tsx +++ b/src/view/com/feed/FeedItem.tsx @@ -3,39 +3,31 @@ import {observer} from 'mobx-react-lite' import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {bsky, AdxUri} from '@adxp/mock-api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {OnNavigateContent} from '../../routes/types' import {FeedViewItemModel} from '../../../state/models/feed-view' import {s} from '../../lib/styles' import {ago} from '../../lib/strings' import {AVIS} from '../../lib/assets' +import {useStores} from '../../../state' export const FeedItem = observer(function FeedItem({ item, - onNavigateContent, onPressShare, }: { item: FeedViewItemModel - onNavigateContent: OnNavigateContent onPressShare: (_uri: string) => void }) { + const store = useStores() const record = item.record as unknown as bsky.Post.Record const onPressOuter = () => { const urip = new AdxUri(item.uri) - onNavigateContent('PostThread', { - name: item.author.name, - recordKey: urip.recordKey, - }) + store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`) } const onPressAuthor = () => { - onNavigateContent('Profile', { - name: item.author.name, - }) + store.nav.navigate(`/profile/${item.author.name}`) } const onPressReply = () => { - onNavigateContent('Composer', { - replyTo: item.uri, - }) + store.nav.navigate('/composer') } const onPressToggleRepost = () => { item @@ -137,8 +129,11 @@ export const FeedItem = observer(function FeedItem({ const styles = StyleSheet.create({ outer: { - borderTopWidth: 1, - borderTopColor: '#e8e8e8', + // borderWidth: 1, + // borderColor: '#e8e8e8', + borderRadius: 10, + margin: 2, + marginBottom: 0, backgroundColor: '#fff', padding: 10, }, @@ -175,6 +170,7 @@ const styles = StyleSheet.create({ }, postText: { paddingBottom: 5, + fontFamily: 'Helvetica Neue', }, ctrls: { flexDirection: 'row', diff --git a/src/view/com/modals/SharePost.native.tsx b/src/view/com/modals/SharePost.native.tsx index 0e99bd4d1..6fc1d1adf 100644 --- a/src/view/com/modals/SharePost.native.tsx +++ b/src/view/com/modals/SharePost.native.tsx @@ -1,27 +1,10 @@ -import React, { - forwardRef, - useState, - useMemo, - useImperativeHandle, - useRef, -} from 'react' -import { - Button, - StyleSheet, - Text, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from 'react-native' -import BottomSheet, {BottomSheetBackdropProps} from '@gorhom/bottom-sheet' -import Animated, { - Extrapolate, - interpolate, - useAnimatedStyle, -} from 'react-native-reanimated' +import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react' +import {Button, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import BottomSheet from '@gorhom/bottom-sheet' import Toast from '../util/Toast' import Clipboard from '@react-native-clipboard/clipboard' import {s} from '../../lib/styles' +import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' export const ShareModal = forwardRef(function ShareModal({}: {}, ref) { const [isOpen, setIsOpen] = useState<boolean>(false) @@ -33,14 +16,13 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) { console.log('sharing', uri) setUri(uri) setIsOpen(true) + bottomSheetRef.current?.expand() }, })) const onPressCopy = () => { Clipboard.setString(uri) console.log('showing') - console.log(Toast) - console.log(Toast.show) Toast.show('Link copied', { position: Toast.positions.TOP, }) @@ -55,50 +37,25 @@ export const ShareModal = forwardRef(function ShareModal({}: {}, ref) { bottomSheetRef.current?.close() } - const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => { - // animated variables - const opacity = useAnimatedStyle(() => ({ - opacity: interpolate( - animatedIndex.value, // current snap index - [-1, 0], // input range - [0, 0.5], // output range - Extrapolate.CLAMP, - ), - })) - - const containerStyle = useMemo( - () => [style, {backgroundColor: '#000'}, opacity], - [style, opacity], - ) - - return ( - <TouchableWithoutFeedback onPress={onClose}> - <Animated.View style={containerStyle} /> - </TouchableWithoutFeedback> - ) - } return ( - <> - {isOpen && ( - <BottomSheet - ref={bottomSheetRef} - snapPoints={['50%']} - enablePanDownToClose - backdropComponent={CustomBackdrop} - onChange={onShareBottomSheetChange}> - <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> - </BottomSheet> - )} - </> + <BottomSheet + ref={bottomSheetRef} + index={-1} + snapPoints={['50%']} + enablePanDownToClose + backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined} + onChange={onShareBottomSheetChange}> + <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> + </BottomSheet> ) }) diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index 7c95003c7..493412e7b 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -1,7 +1,6 @@ import React from 'react' import {observer} from 'mobx-react-lite' import {Text, View, FlatList} from 'react-native' -import {OnNavigateContent} from '../../routes/types' import { NotificationsViewModel, NotificationsViewItemModel, @@ -10,17 +9,15 @@ import {FeedItem} from './FeedItem' export const Feed = observer(function Feed({ view, - onNavigateContent, }: { view: NotificationsViewModel - onNavigateContent: OnNavigateContent }) { // TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf // VirtualizedList: You have a large list that is slow to update - make sure your // renderItem function renders components that follow React performance best practices // like PureComponent, shouldComponentUpdate, etc const renderItem = ({item}: {item: NotificationsViewItemModel}) => ( - <FeedItem item={item} onNavigateContent={onNavigateContent} /> + <FeedItem item={item} /> ) const onRefresh = () => { view.refresh().catch(err => console.error('Failed to refresh', err)) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 1e0e47811..00bf6f48a 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -3,44 +3,34 @@ import {observer} from 'mobx-react-lite' import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {AdxUri} from '@adxp/mock-api' import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome' -import {OnNavigateContent} from '../../routes/types' import {NotificationsViewItemModel} from '../../../state/models/notifications-view' import {s} from '../../lib/styles' import {ago} from '../../lib/strings' import {AVIS} from '../../lib/assets' import {PostText} from '../post/PostText' import {Post} from '../post/Post' +import {useStores} from '../../../state' export const FeedItem = observer(function FeedItem({ item, - onNavigateContent, }: { item: NotificationsViewItemModel - onNavigateContent: OnNavigateContent }) { + const store = useStores() + const onPressOuter = () => { if (item.isLike || item.isRepost) { const urip = new AdxUri(item.subjectUri) - onNavigateContent('PostThread', { - name: urip.host, - recordKey: urip.recordKey, - }) + store.nav.navigate(`/profile/${urip.host}/post/${urip.recordKey}`) } else if (item.isFollow) { - onNavigateContent('Profile', { - name: item.author.name, - }) + store.nav.navigate(`/profile/${item.author.name}`) } else if (item.isReply) { const urip = new AdxUri(item.uri) - onNavigateContent('PostThread', { - name: urip.host, - recordKey: urip.recordKey, - }) + store.nav.navigate(`/profile/${urip.host}/post/${urip.recordKey}`) } } const onPressAuthor = () => { - onNavigateContent('Profile', { - name: item.author.name, - }) + store.nav.navigate(`/profile/${item.author.name}`) } let action = '' @@ -92,7 +82,7 @@ export const FeedItem = observer(function FeedItem({ </View> {item.isReply ? ( <View style={s.pt5}> - <Post uri={item.uri} onNavigateContent={onNavigateContent} /> + <Post uri={item.uri} /> </View> ) : ( <></> diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx index 678e069f6..9b5810b3b 100644 --- a/src/view/com/post-thread/PostLikedBy.tsx +++ b/src/view/com/post-thread/PostLikedBy.tsx @@ -9,7 +9,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {OnNavigateContent} from '../../routes/types' import { LikedByViewModel, LikedByViewItemModel, @@ -18,13 +17,7 @@ import {useStores} from '../../../state' import {s} from '../../lib/styles' import {AVIS} from '../../lib/assets' -export const PostLikedBy = observer(function PostLikedBy({ - uri, - onNavigateContent, -}: { - uri: string - onNavigateContent: OnNavigateContent -}) { +export const PostLikedBy = observer(function PostLikedBy({uri}: {uri: string}) { const store = useStores() const [view, setView] = useState<LikedByViewModel | undefined>() @@ -66,7 +59,7 @@ export const PostLikedBy = observer(function PostLikedBy({ // loaded // = const renderItem = ({item}: {item: LikedByViewItemModel}) => ( - <LikedByItem item={item} onNavigateContent={onNavigateContent} /> + <LikedByItem item={item} /> ) return ( <View> @@ -79,17 +72,10 @@ export const PostLikedBy = observer(function PostLikedBy({ ) }) -const LikedByItem = ({ - item, - onNavigateContent, -}: { - item: LikedByViewItemModel - onNavigateContent: OnNavigateContent -}) => { +const LikedByItem = ({item}: {item: LikedByViewItemModel}) => { + const store = useStores() const onPressOuter = () => { - onNavigateContent('Profile', { - name: item.name, - }) + store.nav.navigate(`/profile/${item.name}`) } return ( <TouchableOpacity style={styles.outer} onPress={onPressOuter}> diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index 98c24ef86..967e03940 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -9,7 +9,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {OnNavigateContent} from '../../routes/types' import { RepostedByViewModel, RepostedByViewItemModel, @@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets' export const PostRepostedBy = observer(function PostRepostedBy({ uri, - onNavigateContent, }: { uri: string - onNavigateContent: OnNavigateContent }) { const store = useStores() const [view, setView] = useState<RepostedByViewModel | undefined>() @@ -68,7 +65,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({ // loaded // = const renderItem = ({item}: {item: RepostedByViewItemModel}) => ( - <RepostedByItem item={item} onNavigateContent={onNavigateContent} /> + <RepostedByItem item={item} /> ) return ( <View> @@ -81,17 +78,10 @@ export const PostRepostedBy = observer(function PostRepostedBy({ ) }) -const RepostedByItem = ({ - item, - onNavigateContent, -}: { - item: RepostedByViewItemModel - onNavigateContent: OnNavigateContent -}) => { +const RepostedByItem = ({item}: {item: RepostedByViewItemModel}) => { + const store = useStores() const onPressOuter = () => { - onNavigateContent('Profile', { - name: item.name, - }) + store.nav.navigate(`/profile/${item.name}`) } return ( <TouchableOpacity style={styles.outer} onPress={onPressOuter}> diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 6191875c7..f7044b741 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,8 +1,6 @@ import React, {useState, useEffect, useRef} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, FlatList, Text, View} from 'react-native' -import {useFocusEffect} from '@react-navigation/native' -import {OnNavigateContent} from '../../routes/types' import { PostThreadViewModel, PostThreadViewPostModel, @@ -14,13 +12,7 @@ import {s} from '../../lib/styles' const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates -export const PostThread = observer(function PostThread({ - uri, - onNavigateContent, -}: { - uri: string - onNavigateContent: OnNavigateContent -}) { +export const PostThread = observer(function PostThread({uri}: {uri: string}) { const store = useStores() const [view, setView] = useState<PostThreadViewModel | undefined>() const [lastUpdate, setLastUpdate] = useState<number>(Date.now()) @@ -37,12 +29,13 @@ export const PostThread = observer(function PostThread({ newView.setup().catch(err => console.error('Failed to fetch thread', err)) }, [uri, view?.params.uri, store]) - useFocusEffect(() => { - if (Date.now() - lastUpdate > UPDATE_DELAY) { - view?.update() - setLastUpdate(Date.now()) - } - }) + // TODO + // useFocusEffect(() => { + // if (Date.now() - lastUpdate > UPDATE_DELAY) { + // view?.update() + // setLastUpdate(Date.now()) + // } + // }) const onPressShare = (uri: string) => { shareSheetRef.current?.open(uri) @@ -79,11 +72,7 @@ export const PostThread = observer(function PostThread({ // = const posts = view.thread ? Array.from(flattenThread(view.thread)) : [] const renderItem = ({item}: {item: PostThreadViewPostModel}) => ( - <PostThreadItem - item={item} - onNavigateContent={onNavigateContent} - onPressShare={onPressShare} - /> + <PostThreadItem item={item} onPressShare={onPressShare} /> ) return ( <View style={s.h100pct}> diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 981aab092..5430c8ef5 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -3,11 +3,11 @@ import {observer} from 'mobx-react-lite' import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {bsky, AdxUri} from '@adxp/mock-api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {OnNavigateContent} from '../../routes/types' import {PostThreadViewPostModel} from '../../../state/models/post-thread-view' import {s} from '../../lib/styles' import {ago, pluralize} from '../../lib/strings' import {AVIS} from '../../lib/assets' +import {useStores} from '../../../state' function iter<T>(n: number, fn: (_i: number) => T): Array<T> { const arr: T[] = [] @@ -19,46 +19,36 @@ function iter<T>(n: number, fn: (_i: number) => T): Array<T> { export const PostThreadItem = observer(function PostThreadItem({ item, - onNavigateContent, onPressShare, }: { item: PostThreadViewPostModel - onNavigateContent: OnNavigateContent onPressShare: (_uri: string) => void }) { + const store = useStores() const record = item.record as unknown as bsky.Post.Record const hasEngagement = item.likeCount || item.repostCount const onPressOuter = () => { const urip = new AdxUri(item.uri) - onNavigateContent('PostThread', { - name: item.author.name, - recordKey: urip.recordKey, - }) + store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`) } const onPressAuthor = () => { - onNavigateContent('Profile', { - name: item.author.name, - }) + store.nav.navigate(`/profile/${item.author.name}`) } const onPressLikes = () => { const urip = new AdxUri(item.uri) - onNavigateContent('PostLikedBy', { - name: item.author.name, - recordKey: urip.recordKey, - }) + store.nav.navigate( + `/profile/${item.author.name}/post/${urip.recordKey}/liked-by`, + ) } const onPressReposts = () => { const urip = new AdxUri(item.uri) - onNavigateContent('PostRepostedBy', { - name: item.author.name, - recordKey: urip.recordKey, - }) + store.nav.navigate( + `/profile/${item.author.name}/post/${urip.recordKey}/reposted-by`, + ) } const onPressReply = () => { - onNavigateContent('Composer', { - replyTo: item.uri, - }) + store.nav.navigate(`/composer?replyTo=${item.uri}`) } const onPressToggleRepost = () => { item @@ -227,6 +217,7 @@ const styles = StyleSheet.create({ }, postText: { paddingBottom: 5, + fontFamily: 'Helvetica Neue', }, expandedInfo: { flexDirection: 'row', diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 3cfb6a1a1..3369db518 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -10,20 +10,13 @@ import { View, } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {OnNavigateContent} from '../../routes/types' import {PostThreadViewModel} from '../../../state/models/post-thread-view' import {useStores} from '../../../state' import {s} from '../../lib/styles' import {ago} from '../../lib/strings' import {AVIS} from '../../lib/assets' -export const Post = observer(function Post({ - uri, - onNavigateContent, -}: { - uri: string - onNavigateContent: OnNavigateContent -}) { +export const Post = observer(function Post({uri}: {uri: string}) { const store = useStores() const [view, setView] = useState<PostThreadViewModel | undefined>() @@ -63,20 +56,13 @@ export const Post = observer(function Post({ const onPressOuter = () => { const urip = new AdxUri(item.uri) - onNavigateContent('PostThread', { - name: item.author.name, - recordKey: urip.recordKey, - }) + store.nav.navigate(`/profile/${item.author.name}/post/${urip.recordKey}`) } const onPressAuthor = () => { - onNavigateContent('Profile', { - name: item.author.name, - }) + store.nav.navigate(`/profile/${item.author.name}`) } const onPressReply = () => { - onNavigateContent('Composer', { - replyTo: item.uri, - }) + store.nav.navigate(`/composer?replyTo=${item.uri}`) } const onPressToggleRepost = () => { item diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index 06cc0c14d..33d0c8d55 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -9,7 +9,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {OnNavigateContent} from '../../routes/types' import { UserFollowersViewModel, FollowerItem, @@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets' export const ProfileFollowers = observer(function ProfileFollowers({ name, - onNavigateContent, }: { name: string - onNavigateContent: OnNavigateContent }) { const store = useStores() const [view, setView] = useState<UserFollowersViewModel | undefined>() @@ -67,9 +64,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({ // loaded // = - const renderItem = ({item}: {item: FollowerItem}) => ( - <User item={item} onNavigateContent={onNavigateContent} /> - ) + const renderItem = ({item}: {item: FollowerItem}) => <User item={item} /> return ( <View> <FlatList @@ -81,17 +76,10 @@ export const ProfileFollowers = observer(function ProfileFollowers({ ) }) -const User = ({ - item, - onNavigateContent, -}: { - item: FollowerItem - onNavigateContent: OnNavigateContent -}) => { +const User = ({item}: {item: FollowerItem}) => { + const store = useStores() const onPressOuter = () => { - onNavigateContent('Profile', { - name: item.name, - }) + store.nav.navigate(`/profile/${item.name}`) } return ( <TouchableOpacity style={styles.outer} onPress={onPressOuter}> diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index bb5859852..62ed7f1c3 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -9,7 +9,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {OnNavigateContent} from '../../routes/types' import { UserFollowsViewModel, FollowItem, @@ -20,10 +19,8 @@ import {AVIS} from '../../lib/assets' export const ProfileFollows = observer(function ProfileFollows({ name, - onNavigateContent, }: { name: string - onNavigateContent: OnNavigateContent }) { const store = useStores() const [view, setView] = useState<UserFollowsViewModel | undefined>() @@ -67,9 +64,7 @@ export const ProfileFollows = observer(function ProfileFollows({ // loaded // = - const renderItem = ({item}: {item: FollowItem}) => ( - <User item={item} onNavigateContent={onNavigateContent} /> - ) + const renderItem = ({item}: {item: FollowItem}) => <User item={item} /> return ( <View> <FlatList @@ -81,17 +76,10 @@ export const ProfileFollows = observer(function ProfileFollows({ ) }) -const User = ({ - item, - onNavigateContent, -}: { - item: FollowItem - onNavigateContent: OnNavigateContent -}) => { +const User = ({item}: {item: FollowItem}) => { + const store = useStores() const onPressOuter = () => { - onNavigateContent('Profile', { - name: item.name, - }) + store.nav.navigate(`/profile/${item.name}`) } return ( <TouchableOpacity style={styles.outer} onPress={onPressOuter}> diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 6a6d04140..0769a0077 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -9,7 +9,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {OnNavigateContent} from '../../routes/types' import {ProfileViewModel} from '../../../state/models/profile-view' import {useStores} from '../../../state' import {pluralize} from '../../lib/strings' @@ -19,10 +18,8 @@ import Toast from '../util/Toast' export const ProfileHeader = observer(function ProfileHeader({ user, - onNavigateContent, }: { user: string - onNavigateContent: OnNavigateContent }) { const store = useStores() const [view, setView] = useState<ProfileViewModel | undefined>() @@ -55,10 +52,10 @@ export const ProfileHeader = observer(function ProfileHeader({ ) } const onPressFollowers = () => { - onNavigateContent('ProfileFollowers', {name: user}) + store.nav.navigate(`/profile/${user}/followers`) } const onPressFollows = () => { - onNavigateContent('ProfileFollows', {name: user}) + store.nav.navigate(`/profile/${user}/follows`) } // loading diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx new file mode 100644 index 000000000..e175b33a5 --- /dev/null +++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx @@ -0,0 +1,36 @@ +import React, {useMemo} from 'react' +import {GestureResponderEvent, TouchableWithoutFeedback} from 'react-native' +import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet' +import Animated, { + Extrapolate, + interpolate, + useAnimatedStyle, +} from 'react-native-reanimated' + +export function createCustomBackdrop( + onClose?: ((event: GestureResponderEvent) => void) | undefined, +): React.FC<BottomSheetBackdropProps> { + const CustomBackdrop = ({animatedIndex, style}: BottomSheetBackdropProps) => { + // animated variables + const opacity = useAnimatedStyle(() => ({ + opacity: interpolate( + animatedIndex.value, // current snap index + [-1, 0], // input range + [0, 0.5], // output range + Extrapolate.CLAMP, + ), + })) + + const containerStyle = useMemo( + () => [style, {backgroundColor: '#000'}, opacity], + [style, opacity], + ) + + return ( + <TouchableWithoutFeedback onPress={onClose}> + <Animated.View style={containerStyle} /> + </TouchableWithoutFeedback> + ) + } + return CustomBackdrop +} diff --git a/src/view/index.ts b/src/view/index.ts index 026bea123..89db506d0 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -1,33 +1,55 @@ import {library} from '@fortawesome/fontawesome-svg-core' +import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft' +import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight' import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft' import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' +import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' +import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark' +import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark' import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck' +import {faClone} from '@fortawesome/free-regular-svg-icons/faClone' import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' +import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis' import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart' import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse' import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' +import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage' +import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare' import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' +import {faUser} from '@fortawesome/free-regular-svg-icons/faUser' +import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers' import {faX} from '@fortawesome/free-solid-svg-icons/faX' export function setup() { library.add( + faAngleLeft, + faAngleRight, faArrowLeft, faBars, faBell, + farBell, + faBookmark, + farBookmark, faCheck, + faClone, faComment, + faEllipsis, faHeart, fasHeart, faHouse, - faPlus, faMagnifyingGlass, + faMessage, + faPenNib, + faPlus, faRetweet, faShareFromSquare, + faUser, + faUsers, faX, ) } diff --git a/src/view/lib/navigation.ts b/src/view/lib/navigation.ts new file mode 100644 index 000000000..2024918e7 --- /dev/null +++ b/src/view/lib/navigation.ts @@ -0,0 +1,12 @@ +import {useEffect} from 'react' +import {useStores} from '../../state' + +type CB = () => void +/** + * This custom effect hook will trigger on every "navigation" + * Use this in screens to handle any loading behaviors needed + */ +export function useLoadEffect(cb: CB, deps: any[] = []) { + const store = useStores() + useEffect(cb, [store.nav.tab, ...deps]) +} diff --git a/src/view/routes.ts b/src/view/routes.ts new file mode 100644 index 000000000..5d8776ddc --- /dev/null +++ b/src/view/routes.ts @@ -0,0 +1,64 @@ +import React from 'react' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {Home} from './screens/Home' +import {Search} from './screens/Search' +import {Notifications} from './screens/Notifications' +import {Login} from './screens/Login' +import {Signup} from './screens/Signup' +import {NotFound} from './screens/NotFound' +import {Composer} from './screens/Composer' +import {PostThread} from './screens/PostThread' +import {PostLikedBy} from './screens/PostLikedBy' +import {PostRepostedBy} from './screens/PostRepostedBy' +import {Profile} from './screens/Profile' +import {ProfileFollowers} from './screens/ProfileFollowers' +import {ProfileFollows} from './screens/ProfileFollows' + +export type ScreenParams = { + params: Record<string, any> +} +export type Route = [React.FC<ScreenParams>, IconProp, RegExp] +export type MatchResult = { + Com: React.FC<ScreenParams> + icon: IconProp + params: Record<string, any> +} + +const r = (pattern: string) => new RegExp('^' + pattern + '([?]|$)', 'i') +export const routes: Route[] = [ + [Home, 'house', r('/')], + [Search, 'magnifying-glass', r('/search')], + [Notifications, 'bell', r('/notifications')], + [Profile, ['far', 'user'], r('/profile/(?<name>[^/]+)')], + [ProfileFollowers, 'users', r('/profile/(?<name>[^/]+)/followers')], + [ProfileFollows, 'users', r('/profile/(?<name>[^/]+)/follows')], + [ + PostThread, + ['far', 'message'], + r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)'), + ], + [ + PostLikedBy, + 'heart', + r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)/liked-by'), + ], + [ + PostRepostedBy, + 'retweet', + r('/profile/(?<name>[^/]+)/post/(?<recordKey>[^/]+)/reposted-by'), + ], + [Composer, 'pen-nib', r('/compose')], + [Login, ['far', 'user'], r('/login')], + [Signup, ['far', 'user'], r('/signup')], +] + +export function match(url: string): MatchResult { + for (const [Com, icon, pattern] of routes) { + const res = pattern.exec(url) + if (res) { + // TODO: query params + return {Com, icon, params: res.groups || {}} + } + } + return {Com: NotFound, icon: 'magnifying-glass', params: {}} +} diff --git a/src/view/routes/index.tsx b/src/view/routes/index.tsx deleted file mode 100644 index 675edb3e8..000000000 --- a/src/view/routes/index.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import React, {useEffect} from 'react' -import {Linking, Text} from 'react-native' -import { - NavigationContainer, - LinkingOptions, - RouteProp, - ParamListBase, -} from '@react-navigation/native' -import {createNativeStackNavigator} from '@react-navigation/native-stack' -import {createBottomTabNavigator} from '@react-navigation/bottom-tabs' -import {observer} from 'mobx-react-lite' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import type {RootTabsParamList} from './types' -import {useStores} from '../../state' -import * as platform from '../../platform/detection' -import {Home} from '../screens/tabroots/Home' -import {Search} from '../screens/tabroots/Search' -import {Notifications} from '../screens/tabroots/Notifications' -import {Menu} from '../screens/tabroots/Menu' -import {Login} from '../screens/tabroots/Login' -import {Signup} from '../screens/tabroots/Signup' -import {NotFound} from '../screens/tabroots/NotFound' -import {Composer} from '../screens/stacks/Composer' -import {PostThread} from '../screens/stacks/PostThread' -import {PostLikedBy} from '../screens/stacks/PostLikedBy' -import {PostRepostedBy} from '../screens/stacks/PostRepostedBy' -import {Profile} from '../screens/stacks/Profile' -import {ProfileFollowers} from '../screens/stacks/ProfileFollowers' -import {ProfileFollows} from '../screens/stacks/ProfileFollows' - -const linking: LinkingOptions<RootTabsParamList> = { - prefixes: [ - 'http://localhost:3000', // local dev - 'https://pubsq.pfrazee.com', // test server (universal links only) - 'pubsqapp://', // custom protocol (ios) - 'pubsq://app', // custom protocol (android) - ], - config: { - screens: { - HomeTab: '', - SearchTab: 'search', - NotificationsTab: 'notifications', - MenuTab: 'menu', - Profile: 'profile/:name', - ProfileFollowers: 'profile/:name/followers', - ProfileFollows: 'profile/:name/follows', - PostThread: 'profile/:name/post/:recordKey', - PostLikedBy: 'profile/:name/post/:recordKey/liked-by', - PostRepostedBy: 'profile/:name/post/:recordKey/reposted-by', - Composer: 'compose', - Login: 'login', - Signup: 'signup', - NotFound: '*', - }, - }, -} - -export const RootTabs = createBottomTabNavigator<RootTabsParamList>() -export const HomeTabStack = createNativeStackNavigator() -export const SearchTabStack = createNativeStackNavigator() -export const NotificationsTabStack = createNativeStackNavigator() - -const tabBarScreenOptions = ({ - route, -}: { - route: RouteProp<ParamListBase, string> -}) => ({ - headerShown: false, - tabBarShowLabel: false, - tabBarIcon: (state: {focused: boolean; color: string; size: number}) => { - switch (route.name) { - case 'HomeTab': - return <FontAwesomeIcon icon="house" style={{color: state.color}} /> - case 'SearchTab': - return ( - <FontAwesomeIcon - icon="magnifying-glass" - style={{color: state.color}} - /> - ) - case 'NotificationsTab': - return <FontAwesomeIcon icon="bell" style={{color: state.color}} /> - case 'MenuTab': - return <FontAwesomeIcon icon="bars" style={{color: state.color}} /> - default: - return <FontAwesomeIcon icon="bars" style={{color: state.color}} /> - } - }, -}) - -const HIDE_HEADER = {headerShown: false} -const HIDE_TAB = {tabBarButton: () => null} - -function HomeStackCom() { - return ( - <HomeTabStack.Navigator> - <HomeTabStack.Screen name="Home" component={Home} /> - <HomeTabStack.Screen name="Composer" component={Composer} /> - <HomeTabStack.Screen name="Profile" component={Profile} /> - <HomeTabStack.Screen - name="ProfileFollowers" - component={ProfileFollowers} - /> - <HomeTabStack.Screen name="ProfileFollows" component={ProfileFollows} /> - <HomeTabStack.Screen name="PostThread" component={PostThread} /> - <HomeTabStack.Screen name="PostLikedBy" component={PostLikedBy} /> - <HomeTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} /> - </HomeTabStack.Navigator> - ) -} - -function SearchStackCom() { - return ( - <SearchTabStack.Navigator> - <SearchTabStack.Screen - name="Search" - component={Search} - options={HIDE_HEADER} - /> - <SearchTabStack.Screen name="Profile" component={Profile} /> - <SearchTabStack.Screen - name="ProfileFollowers" - component={ProfileFollowers} - /> - <SearchTabStack.Screen name="ProfileFollows" component={ProfileFollows} /> - <SearchTabStack.Screen name="PostThread" component={PostThread} /> - <SearchTabStack.Screen name="PostLikedBy" component={PostLikedBy} /> - <SearchTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} /> - </SearchTabStack.Navigator> - ) -} - -function NotificationsStackCom() { - return ( - <NotificationsTabStack.Navigator> - <NotificationsTabStack.Screen - name="Notifications" - component={Notifications} - /> - <NotificationsTabStack.Screen name="Profile" component={Profile} /> - <NotificationsTabStack.Screen - name="ProfileFollowers" - component={ProfileFollowers} - /> - <NotificationsTabStack.Screen - name="ProfileFollows" - component={ProfileFollows} - /> - <NotificationsTabStack.Screen name="PostThread" component={PostThread} /> - <NotificationsTabStack.Screen - name="PostLikedBy" - component={PostLikedBy} - /> - <NotificationsTabStack.Screen - name="PostRepostedBy" - component={PostRepostedBy} - /> - </NotificationsTabStack.Navigator> - ) -} - -export const Root = observer(() => { - const store = useStores() - - useEffect(() => { - console.log('Initial link setup') - Linking.getInitialURL().then((url: string | null) => { - console.log('Initial url', url) - }) - Linking.addEventListener('url', ({url}) => { - console.log('Deep link opened with', url) - }) - }, []) - - // hide the tabbar on desktop web - const tabBar = platform.isDesktopWeb ? () => null : undefined - - return ( - <NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}> - <RootTabs.Navigator - initialRouteName={store.session.isAuthed ? 'HomeTab' : 'Login'} - screenOptions={tabBarScreenOptions} - tabBar={tabBar}> - {store.session.isAuthed ? ( - <> - <RootTabs.Screen name="HomeTab" component={HomeStackCom} /> - <RootTabs.Screen name="SearchTab" component={SearchStackCom} /> - <RootTabs.Screen - name="NotificationsTab" - component={NotificationsStackCom} - /> - <RootTabs.Screen name="MenuTab" component={Menu} /> - </> - ) : ( - <> - <RootTabs.Screen name="Login" component={Login} /> - <RootTabs.Screen name="Signup" component={Signup} /> - </> - )} - <RootTabs.Screen - name="NotFound" - component={NotFound} - options={HIDE_TAB} - /> - </RootTabs.Navigator> - </NavigationContainer> - ) -}) diff --git a/src/view/routes/types.ts b/src/view/routes/types.ts deleted file mode 100644 index a67f282a6..000000000 --- a/src/view/routes/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack' - -export type RootTabsParamList = { - HomeTab: undefined - SearchTab: undefined - NotificationsTab: undefined - MenuTab: undefined - Profile: {name: string} - ProfileFollowers: {name: string} - ProfileFollows: {name: string} - PostThread: {name: string; recordKey: string} - PostLikedBy: {name: string; recordKey: string} - PostRepostedBy: {name: string; recordKey: string} - Composer: {replyTo?: string} - Login: undefined - Signup: undefined - NotFound: undefined -} -export type RootTabsScreenProps<T extends keyof RootTabsParamList> = - StackScreenProps<RootTabsParamList, T> - -export type OnNavigateContent = ( - screen: string, - params: Record<string, string>, -) => void - -/* -NOTE -this is leftover from a nested nav implementation -keeping it around for future reference --prf - -import type {NavigatorScreenParams} from '@react-navigation/native' -import type {CompositeScreenProps} from '@react-navigation/native' -import type {BottomTabScreenProps} from '@react-navigation/bottom-tabs' - -Container: NavigatorScreenParams<PrimaryStacksParamList> -export type PrimaryStacksParamList = { - Home: undefined - Profile: {name: string} -} -export type PrimaryStacksScreenProps<T extends keyof PrimaryStacksParamList> = - CompositeScreenProps< - BottomTabScreenProps<PrimaryStacksParamList, T>, - RootTabsScreenProps<keyof RootTabsParamList> - > -*/ diff --git a/src/view/screens/Composer.tsx b/src/view/screens/Composer.tsx new file mode 100644 index 000000000..2de84583f --- /dev/null +++ b/src/view/screens/Composer.tsx @@ -0,0 +1,43 @@ +import React, {useLayoutEffect, useRef} from 'react' +// import {Text, TouchableOpacity} from 'react-native' +// import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Composer as ComposerComponent} from '../com/composer/Composer' +import {ScreenParams} from '../routes' + +export const Composer = ({params}: ScreenParams) => { + const {replyTo} = params + const ref = useRef<{publish: () => Promise<boolean>}>() + + // TODO + // useLayoutEffect(() => { + // navigation.setOptions({ + // headerShown: true, + // headerTitle: replyTo ? 'Reply' : 'New Post', + // headerLeft: () => ( + // <TouchableOpacity onPress={() => navigation.goBack()}> + // <FontAwesomeIcon icon="x" /> + // </TouchableOpacity> + // ), + // headerRight: () => ( + // <TouchableOpacity + // onPress={() => { + // if (!ref.current) { + // return + // } + // ref.current.publish().then( + // posted => { + // if (posted) { + // navigation.goBack() + // } + // }, + // err => console.error('Failed to create post', err), + // ) + // }}> + // <Text>Post</Text> + // </TouchableOpacity> + // ), + // }) + // }, [navigation, replyTo, ref]) + + return <ComposerComponent ref={ref} replyTo={replyTo} /> +} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx new file mode 100644 index 000000000..a94ffd2f7 --- /dev/null +++ b/src/view/screens/Home.tsx @@ -0,0 +1,65 @@ +import React, {useState, useEffect, useLayoutEffect} from 'react' +import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Feed} from '../com/feed/Feed' +import {useStores} from '../../state' +import {useLoadEffect} from '../lib/navigation' +import {AVIS} from '../lib/assets' +import {ScreenParams} from '../routes' + +export function Home({params}: ScreenParams) { + const [hasSetup, setHasSetup] = useState<boolean>(false) + const store = useStores() + useLoadEffect(() => { + store.nav.setTitle('Home') + console.log('Fetching home feed') + store.homeFeed.setup().then(() => setHasSetup(true)) + }, [store.nav, store.homeFeed]) + + // TODO + // useEffect(() => { + // return navigation.addListener('focus', () => { + // if (hasSetup) { + // console.log('Updating home feed') + // store.homeFeed.update() + // } + // }) + // }, [navigation, store.homeFeed, hasSetup]) + + // TODO + // useLayoutEffect(() => { + // navigation.setOptions({ + // headerShown: true, + // headerTitle: 'V I B E', + // headerLeft: () => ( + // <TouchableOpacity + // onPress={() => navigation.push('Profile', {name: 'alice.com'})}> + // <Image source={AVIS['alice.com']} style={styles.avi} /> + // </TouchableOpacity> + // ), + // headerRight: () => ( + // <TouchableOpacity + // onPress={() => { + // navigation.push('Composer', {}) + // }}> + // <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} /> + // </TouchableOpacity> + // ), + // }) + // }, [navigation]) + + return ( + <View> + <Feed feed={store.homeFeed} /> + </View> + ) +} + +const styles = StyleSheet.create({ + avi: { + width: 20, + height: 20, + borderRadius: 10, + resizeMode: 'cover', + }, +}) diff --git a/src/view/screens/tabroots/Login.tsx b/src/view/screens/Login.tsx index a5f670bdd..0857687ab 100644 --- a/src/view/screens/tabroots/Login.tsx +++ b/src/view/screens/Login.tsx @@ -1,18 +1,15 @@ import React from 'react' import {Text, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {Shell} from '../../shell' -// import type {RootTabsScreenProps} from '../routes/types' // import {useStores} from '../../state' export const Login = observer( (/*{navigation}: RootTabsScreenProps<'Login'>*/) => { // const store = useStores() return ( - <Shell> - <View style={{justifyContent: 'center', alignItems: 'center'}}> - <Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text> - {/*store.session.uiError && <Text>{store.session.uiError}</Text>} + <View style={{justifyContent: 'center', alignItems: 'center'}}> + <Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text> + {/*store.session.uiError && <Text>{store.session.uiError}</Text>} {!store.session.uiIsProcessing ? ( <> <Button title="Login" onPress={() => store.session.login()} /> @@ -24,8 +21,7 @@ export const Login = observer( ) : ( <ActivityIndicator /> )*/} - </View> - </Shell> + </View> ) }, ) diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx new file mode 100644 index 000000000..2483da1e6 --- /dev/null +++ b/src/view/screens/NotFound.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import {Text, Button, View} from 'react-native' +import {useStores} from '../../state' + +export const NotFound = () => { + const stores = useStores() + return ( + <View style={{justifyContent: 'center', alignItems: 'center'}}> + <Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text> + <Button title="Home" onPress={() => stores.nav.navigate('/')} /> + </View> + ) +} diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx new file mode 100644 index 000000000..7ebc8a7ce --- /dev/null +++ b/src/view/screens/Notifications.tsx @@ -0,0 +1,65 @@ +import React, {useState, useEffect, useLayoutEffect} from 'react' +import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Feed} from '../com/notifications/Feed' +import {useStores} from '../../state' +import {AVIS} from '../lib/assets' +import {ScreenParams} from '../routes' +import {useLoadEffect} from '../lib/navigation' + +export const Notifications = ({params}: ScreenParams) => { + const [hasSetup, setHasSetup] = useState<boolean>(false) + const store = useStores() + useLoadEffect(() => { + store.nav.setTitle('Notifications') + console.log('Fetching notifications feed') + store.notesFeed.setup().then(() => setHasSetup(true)) + }, [store.notesFeed]) + + // TODO + // useEffect(() => { + // return navigation.addListener('focus', () => { + // if (hasSetup) { + // console.log('Updating notifications feed') + // store.notesFeed.update() + // } + // }) + // }, [navigation, store.notesFeed, hasSetup]) + + // TODO + // useLayoutEffect(() => { + // navigation.setOptions({ + // headerShown: true, + // headerTitle: 'Notifications', + // headerLeft: () => ( + // <TouchableOpacity + // onPress={() => navigation.push('Profile', {name: 'alice.com'})}> + // <Image source={AVIS['alice.com']} style={styles.avi} /> + // </TouchableOpacity> + // ), + // headerRight: () => ( + // <TouchableOpacity + // onPress={() => { + // navigation.push('Composer', {}) + // }}> + // <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} /> + // </TouchableOpacity> + // ), + // }) + // }, [navigation]) + + return ( + <View> + <Feed view={store.notesFeed} /> + </View> + ) +} + +const styles = StyleSheet.create({ + avi: { + width: 20, + height: 20, + borderRadius: 10, + resizeMode: 'cover', + }, +}) diff --git a/src/view/screens/PostLikedBy.tsx b/src/view/screens/PostLikedBy.tsx new file mode 100644 index 000000000..92fae30ad --- /dev/null +++ b/src/view/screens/PostLikedBy.tsx @@ -0,0 +1,26 @@ +import React, {useLayoutEffect} from 'react' +import {TouchableOpacity} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {makeRecordUri} from '../lib/strings' +import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' +import {ScreenParams} from '../routes' + +export const PostLikedBy = ({params}: ScreenParams) => { + const {name, recordKey} = params + const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey) + + // TODO + // useLayoutEffect(() => { + // navigation.setOptions({ + // headerShown: true, + // headerTitle: 'Liked By', + // headerLeft: () => ( + // <TouchableOpacity onPress={() => navigation.goBack()}> + // <FontAwesomeIcon icon="arrow-left" /> + // </TouchableOpacity> + // ), + // }) + // }, [navigation]) + + return <PostLikedByComponent uri={uri} /> +} diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx new file mode 100644 index 000000000..81014a7c7 --- /dev/null +++ b/src/view/screens/PostRepostedBy.tsx @@ -0,0 +1,26 @@ +import React, {useLayoutEffect} from 'react' +import {TouchableOpacity} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {makeRecordUri} from '../lib/strings' +import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy' +import {ScreenParams} from '../routes' + +export const PostRepostedBy = ({params}: ScreenParams) => { + const {name, recordKey} = params + const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey) + + // TODO + // useLayoutEffect(() => { + // navigation.setOptions({ + // headerShown: true, + // headerTitle: 'Reposted By', + // headerLeft: () => ( + // <TouchableOpacity onPress={() => navigation.goBack()}> + // <FontAwesomeIcon icon="arrow-left" /> + // </TouchableOpacity> + // ), + // }) + // }, [navigation]) + + return <PostRepostedByComponent uri={uri} /> +} diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx new file mode 100644 index 000000000..1003a40e1 --- /dev/null +++ b/src/view/screens/PostThread.tsx @@ -0,0 +1,32 @@ +import React, {useEffect, useLayoutEffect} from 'react' +import {TouchableOpacity} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {makeRecordUri} from '../lib/strings' +import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' +import {ScreenParams} from '../routes' +import {useStores} from '../../state' +import {useLoadEffect} from '../lib/navigation' + +export const PostThread = ({params}: ScreenParams) => { + const store = useStores() + const {name, recordKey} = params + const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey) + useLoadEffect(() => { + store.nav.setTitle(`Post by ${name}`) + }, [store.nav, name]) + + // TODO + // useLayoutEffect(() => { + // navigation.setOptions({ + // headerShown: true, + // headerTitle: 'Thread', + // headerLeft: () => ( + // <TouchableOpacity onPress={() => navigation.goBack()}> + // <FontAwesomeIcon icon="arrow-left" /> + // </TouchableOpacity> + // ), + // }) + // }, [navigation]) + + return <PostThreadComponent uri={uri} /> +} diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx new file mode 100644 index 000000000..84ff63f5a --- /dev/null +++ b/src/view/screens/Profile.tsx @@ -0,0 +1,58 @@ +import React, {useState, useEffect} from 'react' +import {View, StyleSheet} from 'react-native' +import {FeedViewModel} from '../../state/models/feed-view' +import {useStores} from '../../state' +import {ProfileHeader} from '../com/profile/ProfileHeader' +import {Feed} from '../com/feed/Feed' +import {ScreenParams} from '../routes' +import {useLoadEffect} from '../lib/navigation' + +export const Profile = ({params}: ScreenParams) => { + const store = useStores() + const [hasSetup, setHasSetup] = useState<string>('') + const [feedView, setFeedView] = useState<FeedViewModel | undefined>() + + useLoadEffect(() => { + const author = params.name + if (feedView?.params.author === author) { + return // no change needed? or trigger refresh? + } + console.log('Fetching profile feed', author) + const newFeedView = new FeedViewModel(store, {author}) + setFeedView(newFeedView) + newFeedView + .setup() + .catch(err => console.error('Failed to fetch feed', err)) + .then(() => { + setHasSetup(author) + store.nav.setTitle(author) + }) + }, [params.name, feedView?.params.author, store]) + + // TODO + // useEffect(() => { + // return navigation.addListener('focus', () => { + // if (hasSetup === feedView?.params.author) { + // console.log('Updating profile feed', hasSetup) + // feedView?.update() + // } + // }) + // }, [navigation, feedView, hasSetup]) + + return ( + <View style={styles.container}> + <ProfileHeader user={params.name} /> + <View style={styles.feed}>{feedView && <Feed feed={feedView} />}</View> + </View> + ) +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'column', + height: '100%', + }, + feed: { + flex: 1, + }, +}) diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx new file mode 100644 index 000000000..c8e752685 --- /dev/null +++ b/src/view/screens/ProfileFollowers.tsx @@ -0,0 +1,24 @@ +import React, {useLayoutEffect} from 'react' +import {TouchableOpacity} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/ProfileFollowers' +import {ScreenParams} from '../routes' + +export const ProfileFollowers = ({params}: ScreenParams) => { + const {name} = params + + // TODO + // useLayoutEffect(() => { + // navigation.setOptions({ + // headerShown: true, + // headerTitle: 'Followers', + // headerLeft: () => ( + // <TouchableOpacity onPress={() => navigation.goBack()}> + // <FontAwesomeIcon icon="arrow-left" /> + // </TouchableOpacity> + // ), + // }) + // }, [navigation]) + + return <ProfileFollowersComponent name={name} /> +} diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx new file mode 100644 index 000000000..96ce60ddd --- /dev/null +++ b/src/view/screens/ProfileFollows.tsx @@ -0,0 +1,24 @@ +import React, {useLayoutEffect} from 'react' +import {TouchableOpacity} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileFollows' +import {ScreenParams} from '../routes' + +export const ProfileFollows = ({params}: ScreenParams) => { + const {name} = params + + // TODO + // useLayoutEffect(() => { + // navigation.setOptions({ + // headerShown: true, + // headerTitle: 'Following', + // headerLeft: () => ( + // <TouchableOpacity onPress={() => navigation.goBack()}> + // <FontAwesomeIcon icon="arrow-left" /> + // </TouchableOpacity> + // ), + // }) + // }, [navigation]) + + return <ProfileFollowsComponent name={name} /> +} diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx new file mode 100644 index 000000000..aea54051e --- /dev/null +++ b/src/view/screens/Search.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import {Text, View} from 'react-native' +import {ScreenParams} from '../routes' + +export const Search = ({params}: ScreenParams) => { + return ( + <View style={{justifyContent: 'center', alignItems: 'center'}}> + <Text style={{fontSize: 20, fontWeight: 'bold'}}>Search</Text> + </View> + ) +} diff --git a/src/view/screens/tabroots/Signup.tsx b/src/view/screens/Signup.tsx index dc2af2b1e..a34cd5727 100644 --- a/src/view/screens/tabroots/Signup.tsx +++ b/src/view/screens/Signup.tsx @@ -1,18 +1,15 @@ import React from 'react' import {Text, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {Shell} from '../../shell' -// import type {RootTabsScreenProps} from '../routes/types' // import {useStores} from '../../state' export const Signup = observer( (/*{navigation}: RootTabsScreenProps<'Signup'>*/) => { // const store = useStores() return ( - <Shell> - <View style={{justifyContent: 'center', alignItems: 'center'}}> - <Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text> - {/*store.session.uiError ?? <Text>{store.session.uiError}</Text>} + <View style={{justifyContent: 'center', alignItems: 'center'}}> + <Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text> + {/*store.session.uiError ?? <Text>{store.session.uiError}</Text>} {!store.session.uiIsProcessing ? ( <> <Button @@ -27,8 +24,7 @@ export const Signup = observer( ) : ( <ActivityIndicator /> )*/} - </View> - </Shell> + </View> ) }, ) diff --git a/src/view/screens/stacks/Composer.tsx b/src/view/screens/stacks/Composer.tsx deleted file mode 100644 index e1b36567a..000000000 --- a/src/view/screens/stacks/Composer.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, {useLayoutEffect, useRef} from 'react' -import {Text, TouchableOpacity} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Shell} from '../../shell' -import type {RootTabsScreenProps} from '../../routes/types' -import {Composer as ComposerComponent} from '../../com/composer/Composer' - -export const Composer = ({ - navigation, - route, -}: RootTabsScreenProps<'Composer'>) => { - const {replyTo} = route.params - const ref = useRef<{publish: () => Promise<boolean>}>() - - useLayoutEffect(() => { - navigation.setOptions({ - headerShown: true, - headerTitle: replyTo ? 'Reply' : 'New Post', - headerLeft: () => ( - <TouchableOpacity onPress={() => navigation.goBack()}> - <FontAwesomeIcon icon="x" /> - </TouchableOpacity> - ), - headerRight: () => ( - <TouchableOpacity - onPress={() => { - if (!ref.current) { - return - } - ref.current.publish().then( - posted => { - if (posted) { - navigation.goBack() - } - }, - err => console.error('Failed to create post', err), - ) - }}> - <Text>Post</Text> - </TouchableOpacity> - ), - }) - }, [navigation, replyTo, ref]) - - return ( - <Shell> - <ComposerComponent ref={ref} replyTo={replyTo} /> - </Shell> - ) -} diff --git a/src/view/screens/stacks/PostLikedBy.tsx b/src/view/screens/stacks/PostLikedBy.tsx deleted file mode 100644 index f12990141..000000000 --- a/src/view/screens/stacks/PostLikedBy.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, {useLayoutEffect} from 'react' -import {TouchableOpacity} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {makeRecordUri} from '../../lib/strings' -import {Shell} from '../../shell' -import type {RootTabsScreenProps} from '../../routes/types' -import {PostLikedBy as PostLikedByComponent} from '../../com/post-thread/PostLikedBy' - -export const PostLikedBy = ({ - navigation, - route, -}: RootTabsScreenProps<'PostLikedBy'>) => { - const {name, recordKey} = route.params - const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey) - - useLayoutEffect(() => { - navigation.setOptions({ - headerShown: true, - headerTitle: 'Liked By', - headerLeft: () => ( - <TouchableOpacity onPress={() => navigation.goBack()}> - <FontAwesomeIcon icon="arrow-left" /> - </TouchableOpacity> - ), - }) - }, [navigation]) - - const onNavigateContent = (screen: string, props: Record<string, string>) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.push(screen, props) - } - - return ( - <Shell> - <PostLikedByComponent uri={uri} onNavigateContent={onNavigateContent} /> - </Shell> - ) -} diff --git a/src/view/screens/stacks/PostRepostedBy.tsx b/src/view/screens/stacks/PostRepostedBy.tsx deleted file mode 100644 index 000c1a7fc..000000000 --- a/src/view/screens/stacks/PostRepostedBy.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, {useLayoutEffect} from 'react' -import {TouchableOpacity} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {makeRecordUri} from '../../lib/strings' -import {Shell} from '../../shell' -import type {RootTabsScreenProps} from '../../routes/types' -import {PostRepostedBy as PostRepostedByComponent} from '../../com/post-thread/PostRepostedBy' - -export const PostRepostedBy = ({ - navigation, - route, -}: RootTabsScreenProps<'PostRepostedBy'>) => { - const {name, recordKey} = route.params - const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey) - - useLayoutEffect(() => { - navigation.setOptions({ - headerShown: true, - headerTitle: 'Reposted By', - headerLeft: () => ( - <TouchableOpacity onPress={() => navigation.goBack()}> - <FontAwesomeIcon icon="arrow-left" /> - </TouchableOpacity> - ), - }) - }, [navigation]) - - const onNavigateContent = (screen: string, props: Record<string, string>) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.push(screen, props) - } - - return ( - <Shell> - <PostRepostedByComponent - uri={uri} - onNavigateContent={onNavigateContent} - /> - </Shell> - ) -} diff --git a/src/view/screens/stacks/PostThread.tsx b/src/view/screens/stacks/PostThread.tsx deleted file mode 100644 index 485a2e49a..000000000 --- a/src/view/screens/stacks/PostThread.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, {useLayoutEffect} from 'react' -import {TouchableOpacity} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {makeRecordUri} from '../../lib/strings' -import {Shell} from '../../shell' -import type {RootTabsScreenProps} from '../../routes/types' -import {PostThread as PostThreadComponent} from '../../com/post-thread/PostThread' - -export const PostThread = ({ - navigation, - route, -}: RootTabsScreenProps<'PostThread'>) => { - const {name, recordKey} = route.params - const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey) - - useLayoutEffect(() => { - navigation.setOptions({ - headerShown: true, - headerTitle: 'Thread', - headerLeft: () => ( - <TouchableOpacity onPress={() => navigation.goBack()}> - <FontAwesomeIcon icon="arrow-left" /> - </TouchableOpacity> - ), - }) - }, [navigation]) - - const onNavigateContent = (screen: string, props: Record<string, string>) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.push(screen, props) - } - - return ( - <Shell> - <PostThreadComponent uri={uri} onNavigateContent={onNavigateContent} /> - </Shell> - ) -} diff --git a/src/view/screens/stacks/Profile.tsx b/src/view/screens/stacks/Profile.tsx deleted file mode 100644 index d8de12436..000000000 --- a/src/view/screens/stacks/Profile.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, {useState, useEffect} from 'react' -import {View, StyleSheet} from 'react-native' -import {Shell} from '../../shell' -import type {RootTabsScreenProps} from '../../routes/types' -import {FeedViewModel} from '../../../state/models/feed-view' -import {useStores} from '../../../state' -import {ProfileHeader} from '../../com/profile/ProfileHeader' -import {Feed} from '../../com/feed/Feed' - -export const Profile = ({ - navigation, - route, -}: RootTabsScreenProps<'Profile'>) => { - const store = useStores() - const [hasSetup, setHasSetup] = useState<string>('') - const [feedView, setFeedView] = useState<FeedViewModel | undefined>() - - useEffect(() => { - const author = route.params.name - if (feedView?.params.author === author) { - return // no change needed? or trigger refresh? - } - console.log('Fetching profile feed', author) - const newFeedView = new FeedViewModel(store, {author}) - setFeedView(newFeedView) - newFeedView - .setup() - .catch(err => console.error('Failed to fetch feed', err)) - .then(() => setHasSetup(author)) - }, [route.params.name, feedView?.params.author, store]) - - useEffect(() => { - return navigation.addListener('focus', () => { - if (hasSetup === feedView?.params.author) { - console.log('Updating profile feed', hasSetup) - feedView?.update() - } - }) - }, [navigation, feedView, hasSetup]) - - const onNavigateContent = (screen: string, props: Record<string, string>) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.push(screen, props) - } - - return ( - <Shell> - <View style={styles.container}> - <ProfileHeader - user={route.params.name} - onNavigateContent={onNavigateContent} - /> - <View style={styles.feed}> - {feedView && ( - <Feed feed={feedView} onNavigateContent={onNavigateContent} /> - )} - </View> - </View> - </Shell> - ) -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'column', - height: '100%', - }, - feed: { - flex: 1, - }, -}) diff --git a/src/view/screens/stacks/ProfileFollowers.tsx b/src/view/screens/stacks/ProfileFollowers.tsx deleted file mode 100644 index 48fbb4e13..000000000 --- a/src/view/screens/stacks/ProfileFollowers.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, {useLayoutEffect} from 'react' -import {TouchableOpacity} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Shell} from '../../shell' -import type {RootTabsScreenProps} from '../../routes/types' -import {ProfileFollowers as ProfileFollowersComponent} from '../../com/profile/ProfileFollowers' - -export const ProfileFollowers = ({ - navigation, - route, -}: RootTabsScreenProps<'ProfileFollowers'>) => { - const {name} = route.params - - useLayoutEffect(() => { - navigation.setOptions({ - headerShown: true, - headerTitle: 'Followers', - headerLeft: () => ( - <TouchableOpacity onPress={() => navigation.goBack()}> - <FontAwesomeIcon icon="arrow-left" /> - </TouchableOpacity> - ), - }) - }, [navigation]) - - const onNavigateContent = (screen: string, props: Record<string, string>) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.push(screen, props) - } - - return ( - <Shell> - <ProfileFollowersComponent - name={name} - onNavigateContent={onNavigateContent} - /> - </Shell> - ) -} diff --git a/src/view/screens/stacks/ProfileFollows.tsx b/src/view/screens/stacks/ProfileFollows.tsx deleted file mode 100644 index 6fce3d798..000000000 --- a/src/view/screens/stacks/ProfileFollows.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, {useLayoutEffect} from 'react' -import {TouchableOpacity} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Shell} from '../../shell' -import type {RootTabsScreenProps} from '../../routes/types' -import {ProfileFollows as ProfileFollowsComponent} from '../../com/profile/ProfileFollows' - -export const ProfileFollows = ({ - navigation, - route, -}: RootTabsScreenProps<'ProfileFollows'>) => { - const {name} = route.params - - useLayoutEffect(() => { - navigation.setOptions({ - headerShown: true, - headerTitle: 'Following', - headerLeft: () => ( - <TouchableOpacity onPress={() => navigation.goBack()}> - <FontAwesomeIcon icon="arrow-left" /> - </TouchableOpacity> - ), - }) - }, [navigation]) - - const onNavigateContent = (screen: string, props: Record<string, string>) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.push(screen, props) - } - - return ( - <Shell> - <ProfileFollowsComponent - name={name} - onNavigateContent={onNavigateContent} - /> - </Shell> - ) -} diff --git a/src/view/screens/tabroots/Home.tsx b/src/view/screens/tabroots/Home.tsx deleted file mode 100644 index a9c952473..000000000 --- a/src/view/screens/tabroots/Home.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, {useState, useEffect, useLayoutEffect} from 'react' -import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Shell} from '../../shell' -import {Feed} from '../../com/feed/Feed' -import type {RootTabsScreenProps} from '../../routes/types' -import {useStores} from '../../../state' -import {AVIS} from '../../lib/assets' - -export function Home({navigation}: RootTabsScreenProps<'HomeTab'>) { - const [hasSetup, setHasSetup] = useState<boolean>(false) - const store = useStores() - useEffect(() => { - console.log('Fetching home feed') - store.homeFeed.setup().then(() => setHasSetup(true)) - }, [store.homeFeed]) - - const onNavigateContent = (screen: string, props: Record<string, string>) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.navigate(screen, props) - } - - useEffect(() => { - return navigation.addListener('focus', () => { - if (hasSetup) { - console.log('Updating home feed') - store.homeFeed.update() - } - }) - }, [navigation, store.homeFeed, hasSetup]) - - useLayoutEffect(() => { - navigation.setOptions({ - headerShown: true, - headerTitle: 'V I B E', - headerLeft: () => ( - <TouchableOpacity - onPress={() => navigation.push('Profile', {name: 'alice.com'})}> - <Image source={AVIS['alice.com']} style={styles.avi} /> - </TouchableOpacity> - ), - headerRight: () => ( - <TouchableOpacity - onPress={() => { - navigation.push('Composer', {}) - }}> - <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} /> - </TouchableOpacity> - ), - }) - }, [navigation]) - - return ( - <Shell> - <View> - <Feed feed={store.homeFeed} onNavigateContent={onNavigateContent} /> - </View> - </Shell> - ) -} - -const styles = StyleSheet.create({ - avi: { - width: 20, - height: 20, - borderRadius: 10, - resizeMode: 'cover', - }, -}) diff --git a/src/view/screens/tabroots/Menu.tsx b/src/view/screens/tabroots/Menu.tsx deleted file mode 100644 index dca5ad33b..000000000 --- a/src/view/screens/tabroots/Menu.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import {Shell} from '../../shell' -import {ScrollView, Text, View} from 'react-native' -import type {RootTabsScreenProps} from '../../routes/types' - -export const Menu = (_props: RootTabsScreenProps<'MenuTab'>) => { - return ( - <Shell> - <ScrollView contentInsetAdjustmentBehavior="automatic"> - <View style={{justifyContent: 'center', alignItems: 'center'}}> - <Text style={{fontSize: 20, fontWeight: 'bold'}}>Menu</Text> - </View> - </ScrollView> - </Shell> - ) -} diff --git a/src/view/screens/tabroots/NotFound.tsx b/src/view/screens/tabroots/NotFound.tsx deleted file mode 100644 index a35808cbc..000000000 --- a/src/view/screens/tabroots/NotFound.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import {Shell} from '../../shell' -import {Text, Button, View} from 'react-native' -import type {RootTabsScreenProps} from '../../routes/types' - -export const NotFound = ({navigation}: RootTabsScreenProps<'NotFound'>) => { - return ( - <Shell> - <View style={{justifyContent: 'center', alignItems: 'center'}}> - <Text style={{fontSize: 20, fontWeight: 'bold'}}>Page not found</Text> - <Button title="Home" onPress={() => navigation.navigate('HomeTab')} /> - </View> - </Shell> - ) -} diff --git a/src/view/screens/tabroots/Notifications.tsx b/src/view/screens/tabroots/Notifications.tsx deleted file mode 100644 index ea7576799..000000000 --- a/src/view/screens/tabroots/Notifications.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, {useState, useEffect, useLayoutEffect} from 'react' -import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Shell} from '../../shell' -import {Feed} from '../../com/notifications/Feed' -import type {RootTabsScreenProps} from '../../routes/types' -import {useStores} from '../../../state' -import {AVIS} from '../../lib/assets' - -export const Notifications = ({ - navigation, -}: RootTabsScreenProps<'NotificationsTab'>) => { - const [hasSetup, setHasSetup] = useState<boolean>(false) - const store = useStores() - useEffect(() => { - console.log('Fetching home feed') - store.notesFeed.setup().then(() => setHasSetup(true)) - }, [store.notesFeed]) - - const onNavigateContent = (screen: string, props: Record<string, string>) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.navigate(screen, props) - } - - useEffect(() => { - return navigation.addListener('focus', () => { - if (hasSetup) { - console.log('Updating home feed') - store.notesFeed.update() - } - }) - }, [navigation, store.notesFeed, hasSetup]) - - useLayoutEffect(() => { - navigation.setOptions({ - headerShown: true, - headerTitle: 'Notifications', - headerLeft: () => ( - <TouchableOpacity - onPress={() => navigation.push('Profile', {name: 'alice.com'})}> - <Image source={AVIS['alice.com']} style={styles.avi} /> - </TouchableOpacity> - ), - headerRight: () => ( - <TouchableOpacity - onPress={() => { - navigation.push('Composer', {}) - }}> - <FontAwesomeIcon icon="plus" style={{color: '#006bf7'}} /> - </TouchableOpacity> - ), - }) - }, [navigation]) - - return ( - <Shell> - <View> - <Feed view={store.notesFeed} onNavigateContent={onNavigateContent} /> - </View> - </Shell> - ) -} - -const styles = StyleSheet.create({ - avi: { - width: 20, - height: 20, - borderRadius: 10, - resizeMode: 'cover', - }, -}) diff --git a/src/view/screens/tabroots/Search.tsx b/src/view/screens/tabroots/Search.tsx deleted file mode 100644 index 044ca749c..000000000 --- a/src/view/screens/tabroots/Search.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import {Shell} from '../../shell' -import {Text, View} from 'react-native' -import type {RootTabsScreenProps} from '../../routes/types' - -export const Search = (_props: RootTabsScreenProps<'SearchTab'>) => { - return ( - <Shell> - <View style={{justifyContent: 'center', alignItems: 'center'}}> - <Text style={{fontSize: 20, fontWeight: 'bold'}}>Search</Text> - </View> - </Shell> - ) -} diff --git a/src/view/shell/desktop-web/shell.tsx b/src/view/shell/desktop-web/index.tsx index 13acbbfed..13acbbfed 100644 --- a/src/view/shell/desktop-web/shell.tsx +++ b/src/view/shell/desktop-web/index.tsx diff --git a/src/view/shell/desktop-web/left-column.tsx b/src/view/shell/desktop-web/left-column.tsx index 082231ec9..fabb8bc94 100644 --- a/src/view/shell/desktop-web/left-column.tsx +++ b/src/view/shell/desktop-web/left-column.tsx @@ -1,13 +1,11 @@ import React from 'react' import {Pressable, View, StyleSheet} from 'react-native' -import {Link} from '@react-navigation/native' -import {useRoute} from '@react-navigation/native' export const NavItem: React.FC<{label: string; screen: string}> = ({ label, screen, }) => { - const route = useRoute() + const Link = <></> // TODO return ( <View> <Pressable @@ -18,7 +16,7 @@ export const NavItem: React.FC<{label: string; screen: string}> = ({ <Link style={[ styles.navItemLink, - route.name === screen && styles.navItemLinkSelected, + false /* TODO route.name === screen*/ && styles.navItemLinkSelected, ]} to={{screen, params: {}}}> {label} diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx deleted file mode 100644 index db60ed149..000000000 --- a/src/view/shell/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import {SafeAreaView} from 'react-native' -import {isDesktopWeb} from '../../platform/detection' -import {DesktopWebShell} from './desktop-web/shell' - -export const Shell: React.FC = ({children}) => { - return isDesktopWeb ? ( - <DesktopWebShell>{children}</DesktopWebShell> - ) : ( - <SafeAreaView>{children}</SafeAreaView> - ) -} diff --git a/src/view/shell/mobile/history-menu.tsx b/src/view/shell/mobile/history-menu.tsx new file mode 100644 index 000000000..b625162d4 --- /dev/null +++ b/src/view/shell/mobile/history-menu.tsx @@ -0,0 +1,99 @@ +import React from 'react' +import { + StyleSheet, + Text, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from 'react-native' +import RootSiblings from 'react-native-root-siblings' +import {NavigationTabModel} from '../../../state/models/navigation' + +export function createBackMenu(tab: NavigationTabModel): RootSiblings { + const onPressItem = (index: number) => { + sibling.destroy() + tab.goToIndex(index) + } + const onOuterPress = () => sibling.destroy() + const sibling = new RootSiblings( + ( + <> + <TouchableWithoutFeedback onPress={onOuterPress}> + <View style={styles.bg} /> + </TouchableWithoutFeedback> + <View style={[styles.menu, styles.back]}> + {tab.backTen.map((item, i) => ( + <TouchableOpacity + key={item.index} + style={[styles.menuItem, i !== 0 && styles.menuItemBorder]} + onPress={() => onPressItem(item.index)}> + <Text>{item.title || item.url}</Text> + </TouchableOpacity> + ))} + </View> + </> + ), + ) + return sibling +} +export function createForwardMenu(tab: NavigationTabModel): RootSiblings { + const onPressItem = (index: number) => { + sibling.destroy() + tab.goToIndex(index) + } + const onOuterPress = () => sibling.destroy() + const sibling = new RootSiblings( + ( + <> + <TouchableWithoutFeedback onPress={onOuterPress}> + <View style={styles.bg} /> + </TouchableWithoutFeedback> + <View style={[styles.menu, styles.forward]}> + {tab.forwardTen.reverse().map((item, i) => ( + <TouchableOpacity + key={item.index} + style={[styles.menuItem, i !== 0 && styles.menuItemBorder]} + onPress={() => onPressItem(item.index)}> + <Text>{item.title || item.url}</Text> + </TouchableOpacity> + ))} + </View> + </> + ), + ) + return sibling +} + +const styles = StyleSheet.create({ + bg: { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + backgroundColor: '#000', + opacity: 0.1, + }, + menu: { + position: 'absolute', + bottom: 80, + backgroundColor: '#fff', + borderRadius: 8, + opacity: 1, + }, + back: { + left: 10, + }, + forward: { + left: 60, + }, + menuItem: { + paddingVertical: 10, + paddingLeft: 15, + paddingRight: 30, + }, + menuItemBorder: { + borderTopWidth: 1, + borderTopColor: '#ddd', + }, +}) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx new file mode 100644 index 000000000..7b0098c51 --- /dev/null +++ b/src/view/shell/mobile/index.tsx @@ -0,0 +1,235 @@ +import React, {useRef} from 'react' +import {observer} from 'mobx-react-lite' +import { + GestureResponderEvent, + SafeAreaView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native' +import {ScreenContainer, Screen} from 'react-native-screens' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {useStores} from '../../../state' +import {NavigationModel} from '../../../state/models/navigation' +import {match, MatchResult} from '../../routes' +import {TabsSelectorModal} from './tabs-selector' +import {createBackMenu, createForwardMenu} from './history-menu' + +const Location = ({icon, title}: {icon: IconProp; title?: string}) => { + return ( + <TouchableOpacity style={styles.location}> + {title ? ( + <FontAwesomeIcon size={16} style={styles.locationIcon} icon={icon} /> + ) : ( + <FontAwesomeIcon + size={16} + style={styles.locationIconLight} + icon="magnifying-glass" + /> + )} + <Text style={title ? styles.locationText : styles.locationTextLight}> + {title || 'Search'} + </Text> + </TouchableOpacity> + ) +} + +const Btn = ({ + icon, + inactive, + onPress, + onLongPress, +}: { + icon: IconProp + inactive?: boolean + onPress?: (event: GestureResponderEvent) => void + onLongPress?: (event: GestureResponderEvent) => void +}) => { + if (inactive) { + return ( + <View style={styles.ctrl}> + <FontAwesomeIcon + size={18} + style={[styles.ctrlIcon, styles.inactive]} + icon={icon} + /> + </View> + ) + } + return ( + <TouchableOpacity + style={styles.ctrl} + onPress={onPress} + onLongPress={onLongPress}> + <FontAwesomeIcon size={18} style={styles.ctrlIcon} icon={icon} /> + </TouchableOpacity> + ) +} + +export const MobileShell: React.FC = observer(() => { + const stores = useStores() + const tabSelectorRef = useRef<{open: () => void}>() + const screenRenderDesc = constructScreenRenderDesc(stores.nav) + + const onPressBack = () => stores.nav.tab.goBack() + const onPressForward = () => stores.nav.tab.goForward() + const onPressHome = () => stores.nav.navigate('/') + const onPressNotifications = () => stores.nav.navigate('/notifications') + const onPressTabs = () => tabSelectorRef.current?.open() + + const onLongPressBack = () => createBackMenu(stores.nav.tab) + const onLongPressForward = () => createForwardMenu(stores.nav.tab) + + const onNewTab = () => stores.nav.newTab('/') + const onChangeTab = (tabIndex: number) => stores.nav.setActiveTab(tabIndex) + const onCloseTab = (tabIndex: number) => stores.nav.closeTab(tabIndex) + + return ( + <View style={styles.outerContainer}> + <View style={styles.topBar}> + <Location + icon={screenRenderDesc.icon} + title={stores.nav.tab.current.title} + /> + </View> + <SafeAreaView style={styles.innerContainer}> + <ScreenContainer> + {screenRenderDesc.screens.map(({Com, params, key, activityState}) => ( + <Screen + key={key} + style={{backgroundColor: '#fff'}} + activityState={activityState}> + <Com params={params} /> + </Screen> + ))} + </ScreenContainer> + </SafeAreaView> + <View style={styles.bottomBar}> + <Btn + icon="angle-left" + inactive={!stores.nav.tab.canGoBack} + onPress={onPressBack} + onLongPress={onLongPressBack} + /> + <Btn + icon="angle-right" + inactive={!stores.nav.tab.canGoForward} + onPress={onPressForward} + onLongPress={onLongPressForward} + /> + <Btn icon="house" onPress={onPressHome} /> + <Btn icon={['far', 'bell']} onPress={onPressNotifications} /> + <Btn icon={['far', 'clone']} onPress={onPressTabs} /> + </View> + <TabsSelectorModal + ref={tabSelectorRef} + tabs={stores.nav.tabs} + currentTabIndex={stores.nav.tabIndex} + onNewTab={onNewTab} + onChangeTab={onChangeTab} + onCloseTab={onCloseTab} + /> + </View> + ) +}) + +/** + * This method produces the information needed by the shell to + * render the current screens with screen-caching behaviors. + */ +type ScreenRenderDesc = MatchResult & {key: string; activityState: 0 | 1 | 2} +function constructScreenRenderDesc(nav: NavigationModel): { + icon: IconProp + screens: ScreenRenderDesc[] +} { + let icon: IconProp = 'magnifying-glass' + let screens: ScreenRenderDesc[] = [] + for (const tab of nav.tabs) { + const tabScreens = [ + ...tab.getBackList(5), + Object.assign({}, tab.current, {index: tab.index}), + ] + const parsedTabScreens = tabScreens.map(screen => { + const isCurrent = nav.isCurrentScreen(tab.id, screen.index) + const matchRes = match(screen.url) + if (isCurrent) { + icon = matchRes.icon + } + return Object.assign(matchRes, { + key: `t${tab.id}-s${screen.index}`, + activityState: isCurrent ? 2 : 0, + }) + }) + screens = screens.concat(parsedTabScreens) + } + return { + icon, + screens, + } +} + +const styles = StyleSheet.create({ + outerContainer: { + height: '100%', + }, + innerContainer: { + flex: 1, + }, + topBar: { + flexDirection: 'row', + backgroundColor: '#fff', + borderBottomWidth: 1, + borderBottomColor: '#ccc', + paddingLeft: 10, + paddingRight: 10, + paddingTop: 40, + paddingBottom: 5, + }, + location: { + flex: 1, + flexDirection: 'row', + borderRadius: 4, + paddingLeft: 10, + paddingRight: 6, + paddingTop: 6, + paddingBottom: 6, + backgroundColor: '#F8F3F3', + }, + locationIcon: { + color: '#DB00FF', + marginRight: 8, + }, + locationIconLight: { + color: '#909090', + marginRight: 8, + }, + locationText: { + color: '#000', + }, + locationTextLight: { + color: '#868788', + }, + bottomBar: { + flexDirection: 'row', + backgroundColor: '#fff', + borderTopWidth: 1, + borderTopColor: '#ccc', + paddingLeft: 5, + paddingRight: 15, + paddingBottom: 20, + }, + ctrl: { + flex: 1, + paddingTop: 15, + paddingBottom: 15, + }, + ctrlIcon: { + marginLeft: 'auto', + marginRight: 'auto', + }, + inactive: { + color: '#888', + }, +}) diff --git a/src/view/shell/mobile/tabs-selector.tsx b/src/view/shell/mobile/tabs-selector.tsx new file mode 100644 index 000000000..10651ba1f --- /dev/null +++ b/src/view/shell/mobile/tabs-selector.tsx @@ -0,0 +1,158 @@ +import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react' +import {StyleSheet, Text, TouchableWithoutFeedback, View} from 'react-native' +import BottomSheet from '@gorhom/bottom-sheet' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {s} from '../../lib/styles' +import {NavigationTabModel} from '../../../state/models/navigation' +import {createCustomBackdrop} from '../../com/util/BottomSheetCustomBackdrop' +import {match} from '../../routes' + +const TAB_HEIGHT = 38 +const TAB_SPACING = 5 +const BOTTOM_MARGIN = 70 + +export const TabsSelectorModal = forwardRef(function TabsSelectorModal( + { + onNewTab, + onChangeTab, + onCloseTab, + tabs, + currentTabIndex, + }: { + onNewTab: () => void + onChangeTab: (tabIndex: number) => void + onCloseTab: (tabIndex: number) => void + tabs: NavigationTabModel[] + currentTabIndex: number + }, + ref, +) { + const [isOpen, setIsOpen] = useState<boolean>(false) + const [snapPoints, setSnapPoints] = useState<number[]>([100]) + const bottomSheetRef = useRef<BottomSheet>(null) + + useImperativeHandle(ref, () => ({ + open() { + setIsOpen(true) + setSnapPoints([ + (tabs.length + 1) * (TAB_HEIGHT + TAB_SPACING) + BOTTOM_MARGIN, + ]) + bottomSheetRef.current?.expand() + }, + })) + + const onShareBottomSheetChange = (snapPoint: number) => { + if (snapPoint === -1) { + setIsOpen(false) + } + } + const onPressNewTab = () => { + onNewTab() + onClose() + } + const onPressChangeTab = (tabIndex: number) => { + onChangeTab(tabIndex) + onClose() + } + const onClose = () => { + setIsOpen(false) + bottomSheetRef.current?.close() + } + return ( + <BottomSheet + ref={bottomSheetRef} + index={-1} + snapPoints={snapPoints} + enablePanDownToClose + backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined} + onChange={onShareBottomSheetChange}> + <View style={s.p10}> + {tabs.map((tab, tabIndex) => { + const {icon} = match(tab.current.url) + const isActive = tabIndex === currentTabIndex + return ( + <View + key={tabIndex} + style={[styles.tab, styles.existing, isActive && styles.active]}> + <TouchableWithoutFeedback + onPress={() => onPressChangeTab(tabIndex)}> + <View style={styles.tabIcon}> + <FontAwesomeIcon size={16} icon={icon} /> + </View> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback + onPress={() => onPressChangeTab(tabIndex)}> + <Text + style={[styles.tabText, isActive && styles.tabTextActive]}> + {tab.current.title || tab.current.url} + </Text> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback onPress={() => onCloseTab(tabIndex)}> + <View style={styles.tabClose}> + <FontAwesomeIcon + size={16} + icon="x" + style={styles.tabCloseIcon} + /> + </View> + </TouchableWithoutFeedback> + </View> + ) + })} + <TouchableWithoutFeedback onPress={onPressNewTab}> + <View style={[styles.tab, styles.create]}> + <View style={styles.tabIcon}> + <FontAwesomeIcon size={16} icon="plus" /> + </View> + <Text style={styles.tabText}>New tab</Text> + </View> + </TouchableWithoutFeedback> + </View> + </BottomSheet> + ) +}) + +const styles = StyleSheet.create({ + tab: { + flexDirection: 'row', + width: '100%', + borderRadius: 4, + height: TAB_HEIGHT, + marginBottom: TAB_SPACING, + }, + existing: { + borderColor: '#000', + borderWidth: 1, + }, + create: { + backgroundColor: '#F8F3F3', + }, + active: { + backgroundColor: '#faf0f0', + borderColor: '#f00', + borderWidth: 1, + }, + tabIcon: { + paddingTop: 10, + paddingBottom: 10, + paddingLeft: 15, + paddingRight: 10, + }, + tabText: { + flex: 1, + paddingTop: 10, + paddingBottom: 10, + }, + tabTextActive: { + fontWeight: 'bold', + }, + tabClose: { + paddingTop: 10, + paddingBottom: 10, + paddingLeft: 10, + paddingRight: 15, + }, + tabCloseIcon: { + color: '#655', + }, +}) |