Skip to content

Commit

Permalink
New navigation model (#1)
Browse files Browse the repository at this point in the history
* Flatten all routing into a single stack

* Replace router with custom implementation

* Add shell header and titles

* Add tab selector

* Add back/forward history menus on longpress

* Fix: don't modify state during render

* Add refresh() to navigation and reroute navigations to the current location to refresh instead of add to history

* Cache screens during navigation to maintain scroll position and improve load-time for renders
  • Loading branch information
pfrazee authored Aug 31, 2022
1 parent d1470ba commit 97f52b6
Show file tree
Hide file tree
Showing 57 changed files with 1,382 additions and 1,159 deletions.
4 changes: 0 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@
"@gorhom/bottom-sheet": "^4",
"@react-native-async-storage/async-storage": "^1.17.6",
"@react-native-clipboard/clipboard": "^1.10.0",
"@react-navigation/bottom-tabs": "^6.3.1",
"@react-navigation/native": "^6.0.10",
"@react-navigation/native-stack": "^6.6.2",
"@react-navigation/stack": "^6.2.1",
"@zxing/text-encoding": "^0.9.0",
"base64-js": "^1.5.1",
"lodash.omit": "^4.5.0",
Expand Down
4 changes: 2 additions & 2 deletions src/App.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand All @@ -31,7 +31,7 @@ function App() {
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<RootStoreProvider value={rootStore}>
<Routes.Root />
<MobileShell />
</RootStoreProvider>
</RootSiblingParent>
</GestureHandlerRootView>
Expand Down
4 changes: 2 additions & 2 deletions src/App.web.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -22,7 +22,7 @@ function App() {

return (
<RootStoreProvider value={rootStore}>
<Routes.Root />
<DesktopWebShell />
<Toast.ToastContainer />
</RootStoreProvider>
)
Expand Down
251 changes: 251 additions & 0 deletions src/state/models/navigation.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
6 changes: 6 additions & 0 deletions src/state/models/root-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {})
Expand All @@ -35,6 +37,7 @@ export class RootStoreModel {
serialize(): unknown {
return {
session: this.session.serialize(),
nav: this.nav.serialize(),
}
}

Expand All @@ -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)
}
}
}
}
Expand Down
15 changes: 2 additions & 13 deletions src/view/com/feed/Feed.tsx
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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))
Expand Down
Loading

0 comments on commit 97f52b6

Please sign in to comment.