From 3b0d72b776a6bab315572ea1b3f36a1b3b31a6ba Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 10 Mar 2023 01:01:51 -0600 Subject: [PATCH] Replace the navigation model with react-navigation --- src/lib/build-flags.ts | 1 - src/lib/notifee.ts | 3 +- src/lib/routes/helpers.ts | 18 +- src/lib/routes/types.ts | 23 +- src/state/models/navigation.ts | 434 --------------------- src/state/models/root-store.ts | 10 +- src/view/com/modals/DeleteAccount.tsx | 2 +- src/view/com/posts/Feed.tsx | 14 +- src/view/com/profile/ProfileHeader.tsx | 49 +-- src/view/com/util/Link.tsx | 114 ++++-- src/view/com/util/ViewHeader.tsx | 21 +- src/view/com/util/forms/DropdownButton.tsx | 10 - src/view/screens.tsx | 94 ++--- src/view/screens/Home.tsx | 7 +- src/view/screens/NotFound.tsx | 12 +- src/view/screens/Notifications.tsx | 4 +- src/view/screens/Search.tsx | 4 +- src/view/screens/Search.web.tsx | 4 +- src/view/screens/Settings.tsx | 14 +- src/view/shell/BottomBar.tsx | 2 +- src/view/shell/Drawer.tsx | 2 +- src/view/shell/mobile/index.tsx | 183 +-------- 22 files changed, 249 insertions(+), 776 deletions(-) delete mode 100644 src/state/models/navigation.ts diff --git a/src/lib/build-flags.ts b/src/lib/build-flags.ts index 155230e5d7..28b650b6fb 100644 --- a/src/lib/build-flags.ts +++ b/src/lib/build-flags.ts @@ -1,2 +1 @@ export const LOGIN_INCLUDE_DEV_SERVERS = true -export const TABS_ENABLED = false diff --git a/src/lib/notifee.ts b/src/lib/notifee.ts index fb0afdd605..f40e3da706 100644 --- a/src/lib/notifee.ts +++ b/src/lib/notifee.ts @@ -16,7 +16,8 @@ export function init(store: RootStoreModel) { store.log.debug('Notifee foreground event', {type}) if (type === EventType.PRESS) { store.log.debug('User pressed a notifee, opening notifications') - store.nav.switchTo(TabPurpose.Notifs, true) + // TODO + // store.nav.switchTo(TabPurpose.Notifs, true) } }) notifee.onBackgroundEvent(async _e => {}) // notifee requires this but we handle it with onForegroundEvent diff --git a/src/lib/routes/helpers.ts b/src/lib/routes/helpers.ts index 98c7f03f08..6cb5404a64 100644 --- a/src/lib/routes/helpers.ts +++ b/src/lib/routes/helpers.ts @@ -1,4 +1,18 @@ -import {State} from './types' +import {State, NavigationProp} from './types' + +// TODO needed? +// export function getCurrentTabName( +// navigator: NavigationProp | undefined, +// ): string { +// if (!navigator) { +// throw new Error('Failed to get current tab') +// } +// const state = navigator.getState() +// if (state.type !== 'tab') { +// return getCurrentTabName(navigator.getParent()) +// } +// return state.routes[state.index].name +// } export function getCurrentRoute(state: State) { let node = state.routes[state.index] @@ -15,7 +29,7 @@ export function isTab(current: string, route: string) { // -prf return ( current === route || - current === `${route}Stack` || + current === `${route}Tab` || current === `${route}Inner` ) } diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index d0f75b0ea7..5506ad9e1a 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -1,4 +1,5 @@ import {NavigationState, PartialState} from '@react-navigation/native' +import type {NativeStackNavigationProp} from '@react-navigation/native-stack' export type {NativeStackScreenProps} from '@react-navigation/native-stack' @@ -13,15 +14,31 @@ export type CommonNavigatorParams = { Debug: undefined Log: undefined } -export type HomeStackNavigatorParams = CommonNavigatorParams & { + +export type HomeTabNavigatorParams = CommonNavigatorParams & { Home: undefined } -export type NotificationsStackNavigatorParams = CommonNavigatorParams & { + +export type NotificationsTabNavigatorParams = CommonNavigatorParams & { Notifications: undefined } -export type SearchStackNavigatorParams = CommonNavigatorParams & { + +export type SearchTabNavigatorParams = CommonNavigatorParams & { Search: undefined } + +// NOTE +// this isn't strictly correct but it should be close enough +// a TS wizard might be able to get this 100% +// -prf +export type NavigationProp = NativeStackNavigationProp< + CommonNavigatorParams & { + HomeTab: undefined + NotificationsTab: undefined + SearchTab: undefined + } +> + export type State = | NavigationState | Omit, 'stale'> diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts deleted file mode 100644 index 11af659124..0000000000 --- a/src/state/models/navigation.ts +++ /dev/null @@ -1,434 +0,0 @@ -import {RootStoreModel} from './root-store' -import {makeAutoObservable} from 'mobx' -import {TABS_ENABLED} from 'lib/build-flags' -import * as analytics from 'lib/analytics' -import {isNative} from 'platform/detection' - -let __id = 0 -function genId() { - return String(++__id) -} - -// NOTE -// this model was originally built for a freeform "tabs" concept like a browser -// we've since decided to pause that idea and do something more traditional -// until we're fully sure what that is, the tabs are being repurposed into a fixed topology -// - Tab 0: The "Default" tab -// - Tab 1: The "Search" tab -// - Tab 2: The "Notifications" tab -// These tabs always retain the first item in their history. -// -prf -export enum TabPurpose { - Default = 0, - Search = 1, - Notifs = 2, -} - -export const TabPurposeMainPath: Record = { - [TabPurpose.Default]: '/', - [TabPurpose.Search]: '/search', - [TabPurpose.Notifs]: '/notifications', -} - -interface HistoryItem { - url: string - ts: number - title?: string - id: string -} - -export type HistoryPtr = string // `{tabId}-{historyId}` - -export class NavigationTabModel { - id = genId() - history: HistoryItem[] - index = 0 - isNewTab = false - - constructor(public fixedTabPurpose: TabPurpose) { - this.history = [ - {url: TabPurposeMainPath[fixedTabPurpose], ts: Date.now(), id: genId()}, - ] - 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 = this.index - return this.history.slice(start, end).map((item, i) => ({ - url: item.url, - title: item.title, - index: start + i, - id: item.id, - })) - } - - 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 + 1, this.history.length) - return this.history.slice(start, end).map((item, i) => ({ - url: item.url, - title: item.title, - index: start + i, - id: item.id, - })) - } - - get forwardTen() { - return this.getForwardList(10) - } - - // navigation - // = - - navigate(url: string, title?: string) { - try { - const path = url.split('/')[1] - analytics.track('Navigation', { - path, - }) - } catch (error) {} - - if (this.current?.url === url) { - this.refresh() - } else { - if (this.index < this.history.length - 1) { - this.history.length = this.index + 1 - } - // TEMP ensure the tab has its purpose's main view -prf - if (this.history.length < 1) { - const fixedUrl = TabPurposeMainPath[this.fixedTabPurpose] - this.history.push({url: fixedUrl, ts: Date.now(), id: genId()}) - } - this.history.push({url, title, ts: Date.now(), id: genId()}) - this.index = this.history.length - 1 - if (!isNative) { - window.history.pushState({hindex: this.index, hurl: url}, '', url) - } - } - } - - refresh() { - this.history = [ - ...this.history.slice(0, this.index), - { - url: this.current.url, - title: this.current.title, - ts: Date.now(), - id: this.current.id, - }, - ...this.history.slice(this.index + 1), - ] - } - - goBack() { - if (this.canGoBack) { - this.index-- - if (!isNative) { - window.history.back() - } - } - } - - // TEMP - // a helper to bring the tab back to its base state - // -prf - fixedTabReset() { - this.index = 0 - } - - goForward() { - if (this.canGoForward) { - this.index++ - if (!isNative) { - window.history.forward() - } - } - } - - goToIndex(index: number) { - if (index >= 0 && index <= this.history.length - 1) { - const delta = index - this.index - this.index = index - if (!isNative) { - window.history.go(delta) - } - } - } - - setTitle(id: string, title: string) { - this.history = this.history.map(h => { - if (h.id === id) { - return {...h, title} - } - return h - }) - } - - setIsNewTab(v: boolean) { - this.isNewTab = v - } - - // browser only - // = - - resetTo(url: string) { - this.index = 0 - this.history.push({url, title: '', ts: Date.now(), id: genId()}) - this.index = this.history.length - 1 - } - - // persistence - // = - - serialize(): unknown { - return { - history: this.history, - index: this.index, - } - } - - hydrate(_v: unknown) { - // TODO fixme - // 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[] = isNative - ? [ - new NavigationTabModel(TabPurpose.Default), - new NavigationTabModel(TabPurpose.Search), - new NavigationTabModel(TabPurpose.Notifs), - ] - : [new NavigationTabModel(TabPurpose.Default)] - tabIndex = 0 - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this, { - rootStore: false, - serialize: false, - hydrate: false, - }) - } - - /** - * Used only in the web build to sync with browser history state - */ - bindWebNavigation() { - if (!isNative) { - window.addEventListener('popstate', e => { - const {hindex, hurl} = e.state - if (hindex >= 0 && hindex <= this.tab.history.length - 1) { - this.tab.index = hindex - } - if (this.tab.current.url !== hurl) { - // desynced because they went back to an old tab session- - // do a reset to match that - this.tab.resetTo(hurl) - } - - // sanity check - if (this.tab.current.url !== window.location.pathname) { - // state has completely desynced, reload - window.location.reload() - } - }) - } - } - - clear() { - this.tabs = isNative - ? [ - new NavigationTabModel(TabPurpose.Default), - new NavigationTabModel(TabPurpose.Search), - new NavigationTabModel(TabPurpose.Notifs), - ] - : [new NavigationTabModel(TabPurpose.Default)] - this.tabIndex = 0 - } - - // accessors - // = - - get tab() { - return this.tabs[this.tabIndex] - } - - get tabCount() { - return this.tabs.length - } - - isCurrentScreen(tabId: string, index: number) { - return this.tab.id === tabId && this.tab.index === index - } - - // navigation - // = - - navigate(url: string, title?: string) { - this.rootStore.emitNavigation() - this.tab.navigate(url, title) - } - - refresh() { - this.tab.refresh() - } - - setTitle(ptr: HistoryPtr, title: string) { - const [tid, hid] = ptr.split('-') - this.tabs.find(t => t.id === tid)?.setTitle(hid, title) - } - - handleLink(url: string) { - let path - if (url.startsWith('/')) { - path = url - } else if (url.startsWith('http')) { - try { - path = new URL(url).pathname - } catch (e) { - console.error('Invalid url', url, e) - return - } - } else { - console.error('Invalid url', url) - return - } - this.navigate(path) - } - - // tab management - // = - - // TEMP - // fixed tab helper function - // -prf - switchTo(purpose: TabPurpose, reset: boolean) { - this.rootStore.emitNavigation() - switch (purpose) { - case TabPurpose.Notifs: - this.tabIndex = 2 - break - case TabPurpose.Search: - this.tabIndex = 1 - break - default: - this.tabIndex = 0 - } - if (reset) { - this.tab.fixedTabReset() - } - } - - newTab(url: string, title?: string) { - if (!TABS_ENABLED) { - return this.navigate(url) - } - const tab = new NavigationTabModel(TabPurpose.Default) - tab.navigate(url, title) - tab.isNewTab = true - this.tabs.push(tab) - this.tabIndex = this.tabs.length - 1 - } - - setActiveTab(tabIndex: number) { - if (!TABS_ENABLED) { - return - } - this.tabIndex = Math.max(Math.min(tabIndex, this.tabs.length - 1), 0) - } - - closeTab(tabIndex: number) { - if (!TABS_ENABLED) { - return - } - 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) { - // TODO fixme - this.clear() - /*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 4b62f501e6..54be125d8a 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -11,7 +11,6 @@ import {z} from 'zod' import {isObj, hasProp} from 'lib/type-guards' import {LogModel} from './log' import {SessionModel} from './session' -import {NavigationModel} from './navigation' import {ShellUiModel} from './shell-ui' import {ProfilesViewModel} from './profiles-view' import {LinkMetasViewModel} from './link-metas-view' @@ -31,7 +30,6 @@ export class RootStoreModel { appInfo?: AppInfo log = new LogModel() session = new SessionModel(this) - nav = new NavigationModel(this) shell = new ShellUiModel(this) me = new MeModel(this) profiles = new ProfilesViewModel(this) @@ -82,7 +80,6 @@ export class RootStoreModel { log: this.log.serialize(), session: this.session.serialize(), me: this.me.serialize(), - nav: this.nav.serialize(), shell: this.shell.serialize(), } } @@ -101,9 +98,6 @@ export class RootStoreModel { if (hasProp(v, 'me')) { this.me.hydrate(v.me) } - if (hasProp(v, 'nav')) { - this.nav.hydrate(v.nav) - } if (hasProp(v, 'session')) { this.session.hydrate(v.session) } @@ -144,7 +138,7 @@ export class RootStoreModel { */ async handleSessionDrop() { this.log.debug('RootStoreModel:handleSessionDrop') - this.nav.clear() + // this.nav.clear() TODO this.me.clear() this.emitSessionDropped() } @@ -155,7 +149,7 @@ export class RootStoreModel { clearAllSessionState() { this.log.debug('RootStoreModel:clearAllSessionState') this.session.clear() - this.nav.clear() + // this.nav.clear() TODO this.me.clear() } diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index de29e728d2..45348d44c6 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -46,7 +46,7 @@ export function Component({}: {}) { token: confirmCode, }) Toast.show('Your account has been deleted') - store.nav.tab.fixedTabReset() + // store.nav.tab.fixedTabReset() TODO store.session.clear() store.shell.closeModal() } catch (e: any) { diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 5751faa68b..8f57900b54 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -7,6 +7,7 @@ import { StyleSheet, ViewStyle, } from 'react-native' +import {useNavigation} from '@react-navigation/native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {CenteredView, FlatList} from '../util/Views' @@ -18,10 +19,10 @@ import {FeedModel} from 'state/models/feed-view' import {FeedItem} from './FeedItem' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {s} from 'lib/styles' -import {useStores} from 'state/index' import {useAnalytics} from 'lib/analytics' import {usePalette} from 'lib/hooks/usePalette' import {MagnifyingGlassIcon} from 'lib/icons' +import {NavigationProp} from 'lib/routes/types' const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const ERROR_FEED_ITEM = {_reactKey: '__error__'} @@ -47,9 +48,9 @@ export const Feed = observer(function Feed({ }) { const pal = usePalette('default') const palInverted = usePalette('inverted') - const store = useStores() const {track} = useAnalytics() const [isRefreshing, setIsRefreshing] = React.useState(false) + const navigation = useNavigation() const data = React.useMemo(() => { let feedItems: any[] = [] @@ -112,7 +113,12 @@ export const Feed = observer(function Feed({