Sync browser navigation with app

zio/stable
Paul Frazee 2023-02-24 17:47:53 -06:00
parent dcd396153c
commit 55a8a8fa4c
5 changed files with 68 additions and 30 deletions

View File

@ -16,6 +16,7 @@ function App() {
view.setup() view.setup()
setupState().then(store => { setupState().then(store => {
setRootStore(store) setRootStore(store)
store.nav.bindWebNavigation()
getInitialURL().then(url => { getInitialURL().then(url => {
if (url) { if (url) {
store.nav.handleLink(url) store.nav.handleLink(url)

View File

@ -7,6 +7,7 @@ const segmentClient = createClient({
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
trackAppLifecycleEvents: false, trackAppLifecycleEvents: false,
}) })
export const track = segmentClient.track
export {useAnalytics} from '@segment/analytics-react-native' export {useAnalytics} from '@segment/analytics-react-native'

View File

@ -6,6 +6,7 @@ const _analytics = {
screen(_name: string) {}, screen(_name: string) {},
track(_name: string, _opts: any) {}, track(_name: string, _opts: any) {},
} }
export const track = _analytics.track
export function useAnalytics() { export function useAnalytics() {
return _analytics return _analytics
} }

View File

@ -1,5 +1,4 @@
import {Linking} from 'react-native' import {Linking} from 'react-native'
import {createBrowserHistory, createMemoryHistory} from 'history'
import {isNative, isWeb} from './detection' import {isNative, isWeb} from './detection'
export async function getInitialURL(): Promise<string | undefined> { export async function getInitialURL(): Promise<string | undefined> {
@ -24,11 +23,3 @@ export function clearHash() {
window.location.hash = '' window.location.hash = ''
} }
} }
export function getHistory() {
if (isWeb) {
return createBrowserHistory()
} else {
return createMemoryHistory()
}
}

View File

@ -1,8 +1,8 @@
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import {makeAutoObservable} from 'mobx' import {makeAutoObservable} from 'mobx'
import {TABS_ENABLED} from 'lib/build-flags' import {TABS_ENABLED} from 'lib/build-flags'
import {segmentClient} from 'lib/analytics' import * as analytics from 'lib/analytics'
import {getHistory} from 'platform/urls' import {isNative} from 'platform/detection'
let __id = 0 let __id = 0
function genId() { function genId() {
@ -42,7 +42,6 @@ export type HistoryPtr = string // `{tabId}-{historyId}`
export class NavigationTabModel { export class NavigationTabModel {
id = genId() id = genId()
history: HistoryItem[] history: HistoryItem[]
browserHistory: any
index = 0 index = 0
isNewTab = false isNewTab = false
@ -50,13 +49,11 @@ export class NavigationTabModel {
this.history = [ this.history = [
{url: TabPurposeMainPath[fixedTabPurpose], ts: Date.now(), id: genId()}, {url: TabPurposeMainPath[fixedTabPurpose], ts: Date.now(), id: genId()},
] ]
this.browserHistory = getHistory()
makeAutoObservable(this, { makeAutoObservable(this, {
serialize: false, serialize: false,
hydrate: false, hydrate: false,
}) })
} }
// accessors // accessors
// = // =
@ -108,7 +105,7 @@ export class NavigationTabModel {
navigate(url: string, title?: string) { navigate(url: string, title?: string) {
try { try {
const path = url.split('/')[1] const path = url.split('/')[1]
segmentClient.track('Navigation', { analytics.track('Navigation', {
path, path,
}) })
} catch (error) {} } catch (error) {}
@ -125,8 +122,10 @@ export class NavigationTabModel {
this.history.push({url: fixedUrl, ts: Date.now(), id: genId()}) this.history.push({url: fixedUrl, ts: Date.now(), id: genId()})
} }
this.history.push({url, title, ts: Date.now(), id: genId()}) this.history.push({url, title, ts: Date.now(), id: genId()})
this.browserHistory.push(url)
this.index = this.history.length - 1 this.index = this.history.length - 1
if (!isNative) {
window.history.pushState({hindex: this.index, hurl: url}, '', url)
}
} }
} }
@ -146,7 +145,9 @@ export class NavigationTabModel {
goBack() { goBack() {
if (this.canGoBack) { if (this.canGoBack) {
this.index-- this.index--
this.browserHistory.back() if (!isNative) {
window.history.back()
}
} }
} }
@ -160,14 +161,19 @@ export class NavigationTabModel {
goForward() { goForward() {
if (this.canGoForward) { if (this.canGoForward) {
this.index++ this.index++
this.browserHistory.forward() if (!isNative) {
window.history.forward()
}
} }
} }
goToIndex(index: number) { goToIndex(index: number) {
if (index >= 0 && index <= this.history.length - 1) { if (index >= 0 && index <= this.history.length - 1) {
const delta = index - this.index
this.index = index this.index = index
this.browserHistory.go(index) if (!isNative) {
window.history.go(delta)
}
} }
} }
@ -184,6 +190,15 @@ export class NavigationTabModel {
this.isNewTab = v 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 // persistence
// = // =
@ -229,11 +244,13 @@ export class NavigationTabModel {
} }
export class NavigationModel { export class NavigationModel {
tabs: NavigationTabModel[] = [ tabs: NavigationTabModel[] = isNative
new NavigationTabModel(TabPurpose.Default), ? [
new NavigationTabModel(TabPurpose.Search), new NavigationTabModel(TabPurpose.Default),
new NavigationTabModel(TabPurpose.Notifs), new NavigationTabModel(TabPurpose.Search),
] new NavigationTabModel(TabPurpose.Notifs),
]
: [new NavigationTabModel(TabPurpose.Default)]
tabIndex = 0 tabIndex = 0
constructor(public rootStore: RootStoreModel) { constructor(public rootStore: RootStoreModel) {
@ -244,12 +261,39 @@ export class NavigationModel {
}) })
} }
/**
* 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() { clear() {
this.tabs = [ this.tabs = isNative
new NavigationTabModel(TabPurpose.Default), ? [
new NavigationTabModel(TabPurpose.Search), new NavigationTabModel(TabPurpose.Default),
new NavigationTabModel(TabPurpose.Notifs), new NavigationTabModel(TabPurpose.Search),
] new NavigationTabModel(TabPurpose.Notifs),
]
: [new NavigationTabModel(TabPurpose.Default)]
this.tabIndex = 0 this.tabIndex = 0
} }
@ -372,7 +416,7 @@ export class NavigationModel {
hydrate(_v: unknown) { hydrate(_v: unknown) {
// TODO fixme // TODO fixme
this.clear() this.clear()
/*if (isObj(v)) { /*if (isObj(v)) {
if (hasProp(v, 'tabs') && Array.isArray(v.tabs)) { if (hasProp(v, 'tabs') && Array.isArray(v.tabs)) {
for (const tab of v.tabs) { for (const tab of v.tabs) {
const copy = new NavigationTabModel() const copy = new NavigationTabModel()