Improve reliability of screen titles

zio/stable
Paul Frazee 2022-11-17 16:01:29 -06:00
parent b2160ae159
commit a3bca154c4
16 changed files with 60 additions and 37 deletions

View File

@ -1,20 +1,23 @@
import {makeAutoObservable} from 'mobx' import {makeAutoObservable} from 'mobx'
import {isObj, hasProp} from '../lib/type-guards' import {isObj, hasProp} from '../lib/type-guards'
let __tabId = 0 let __id = 0
function genTabId() { function genId() {
return ++__tabId return ++__id
} }
interface HistoryItem { interface HistoryItem {
url: string url: string
ts: number ts: number
title?: string title?: string
id: number
} }
export type HistoryPtr = [number, number]
export class NavigationTabModel { export class NavigationTabModel {
id = genTabId() id = genId()
history: HistoryItem[] = [{url: '/', ts: Date.now()}] history: HistoryItem[] = [{url: '/', ts: Date.now(), id: genId()}]
index = 0 index = 0
isNewTab = false isNewTab = false
@ -47,6 +50,7 @@ export class NavigationTabModel {
url: item.url, url: item.url,
title: item.title, title: item.title,
index: start + i, index: start + i,
id: item.id,
})) }))
} }
@ -61,6 +65,7 @@ export class NavigationTabModel {
url: item.url, url: item.url,
title: item.title, title: item.title,
index: start + i, index: start + i,
id: item.id,
})) }))
} }
@ -78,7 +83,7 @@ export class NavigationTabModel {
if (this.index < this.history.length - 1) { if (this.index < this.history.length - 1) {
this.history.length = this.index + 1 this.history.length = this.index + 1
} }
this.history.push({url, title, ts: Date.now()}) this.history.push({url, title, ts: Date.now(), id: genId()})
this.index = this.history.length - 1 this.index = this.history.length - 1
} }
} }
@ -86,7 +91,12 @@ export class NavigationTabModel {
refresh() { refresh() {
this.history = [ this.history = [
...this.history.slice(0, this.index), ...this.history.slice(0, this.index),
{url: this.current.url, title: this.current.title, ts: Date.now()}, {
url: this.current.url,
title: this.current.title,
ts: Date.now(),
id: this.current.id,
},
...this.history.slice(this.index + 1), ...this.history.slice(this.index + 1),
] ]
} }
@ -109,8 +119,13 @@ export class NavigationTabModel {
} }
} }
setTitle(title: string) { setTitle(id: number, title: string) {
this.current.title = title this.history = this.history.map(h => {
if (h.id === id) {
return {...h, title}
}
return h
})
} }
setIsNewTab(v: boolean) { setIsNewTab(v: boolean) {
@ -203,8 +218,8 @@ export class NavigationModel {
this.tab.refresh() this.tab.refresh()
} }
setTitle(title: string) { setTitle(ptr: HistoryPtr, title: string) {
this.tab.setTitle(title) this.tabs.find(t => t.id === ptr[0])?.setTitle(ptr[1], title)
} }
// tab management // tab management

View File

@ -17,6 +17,7 @@ import {ProfileMembers} from './screens/ProfileMembers'
import {Settings} from './screens/Settings' import {Settings} from './screens/Settings'
export type ScreenParams = { export type ScreenParams = {
navIdx: [number, number]
params: Record<string, any> params: Record<string, any>
visible: boolean visible: boolean
scrollElRef?: MutableRefObject<FlatList<any> | undefined> scrollElRef?: MutableRefObject<FlatList<any> | undefined>

View File

@ -8,13 +8,13 @@ import {colors} from '../lib/styles'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
export const Contacts = ({visible, params}: ScreenParams) => { export const Contacts = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const selectorInterp = useSharedValue(0) const selectorInterp = useSharedValue(0)
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle(`Contacts`) store.nav.setTitle(navIdx, `Contacts`)
} }
}, [store, visible]) }, [store, visible])

View File

@ -12,6 +12,7 @@ import {ScreenParams} from '../routes'
import {s, colors} from '../lib/styles' import {s, colors} from '../lib/styles'
export const Home = observer(function Home({ export const Home = observer(function Home({
navIdx,
visible, visible,
scrollElRef, scrollElRef,
}: ScreenParams) { }: ScreenParams) {
@ -51,7 +52,7 @@ export const Home = observer(function Home({
console.log('Updating home feed') console.log('Updating home feed')
defaultFeedView.update() defaultFeedView.update()
} else { } else {
store.nav.setTitle('Home') store.nav.setTitle(navIdx, 'Home')
console.log('Fetching home feed') console.log('Fetching home feed')
defaultFeedView.setup().then(() => { defaultFeedView.setup().then(() => {
if (aborted) return if (aborted) return

View File

@ -7,7 +7,7 @@ import {useStores} from '../../state'
import {NotificationsViewModel} from '../../state/models/notifications-view' import {NotificationsViewModel} from '../../state/models/notifications-view'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
export const Notifications = ({visible}: ScreenParams) => { export const Notifications = ({navIdx, visible}: ScreenParams) => {
const [hasSetup, setHasSetup] = useState<boolean>(false) const [hasSetup, setHasSetup] = useState<boolean>(false)
const [notesView, setNotesView] = useState< const [notesView, setNotesView] = useState<
NotificationsViewModel | undefined NotificationsViewModel | undefined
@ -24,7 +24,7 @@ export const Notifications = ({visible}: ScreenParams) => {
console.log('Updating notifications feed') console.log('Updating notifications feed')
notesView?.update() notesView?.update()
} else { } else {
store.nav.setTitle('Notifications') store.nav.setTitle(navIdx, 'Notifications')
const newNotesView = new NotificationsViewModel(store, {}) const newNotesView = new NotificationsViewModel(store, {})
setNotesView(newNotesView) setNotesView(newNotesView)
newNotesView.setup().then(() => { newNotesView.setup().then(() => {

View File

@ -6,14 +6,14 @@ import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
import {makeRecordUri} from '../lib/strings' import {makeRecordUri} from '../lib/strings'
export const PostDownvotedBy = ({visible, params}: ScreenParams) => { export const PostDownvotedBy = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name, rkey} = params const {name, rkey} = params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle('Downvoted by') store.nav.setTitle(navIdx, 'Downvoted by')
} }
}, [store, visible]) }, [store, visible])

View File

@ -6,14 +6,14 @@ import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
import {makeRecordUri} from '../lib/strings' import {makeRecordUri} from '../lib/strings'
export const PostRepostedBy = ({visible, params}: ScreenParams) => { export const PostRepostedBy = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name, rkey} = params const {name, rkey} = params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle('Reposted by') store.nav.setTitle(navIdx, 'Reposted by')
} }
}, [store, visible]) }, [store, visible])

View File

@ -6,14 +6,14 @@ import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
export const PostThread = ({visible, params}: ScreenParams) => { export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name, rkey} = params const {name, rkey} = params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle(`Post by ${name}`) store.nav.setTitle(navIdx, `Post by ${name}`)
} }
}, [visible, store.nav, name]) }, [visible, store.nav, name])

View File

@ -6,14 +6,14 @@ import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
import {makeRecordUri} from '../lib/strings' import {makeRecordUri} from '../lib/strings'
export const PostUpvotedBy = ({visible, params}: ScreenParams) => { export const PostUpvotedBy = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name, rkey} = params const {name, rkey} = params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle('Upvoted by') store.nav.setTitle(navIdx, 'Upvoted by')
} }
}, [store, visible]) }, [store, visible])

View File

@ -23,7 +23,7 @@ const LOADING_ITEM = {_reactKey: '__loading__'}
const END_ITEM = {_reactKey: '__end__'} const END_ITEM = {_reactKey: '__end__'}
const EMPTY_ITEM = {_reactKey: '__empty__'} const EMPTY_ITEM = {_reactKey: '__empty__'}
export const Profile = observer(({visible, params}: ScreenParams) => { export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const [hasSetup, setHasSetup] = useState<boolean>(false) const [hasSetup, setHasSetup] = useState<boolean>(false)
const uiState = useMemo( const uiState = useMemo(
@ -41,7 +41,7 @@ export const Profile = observer(({visible, params}: ScreenParams) => {
uiState.update() uiState.update()
} else { } else {
console.log('Fetching profile for', params.name) console.log('Fetching profile for', params.name)
store.nav.setTitle(params.name) store.nav.setTitle(navIdx, params.name)
uiState.setup().then(() => { uiState.setup().then(() => {
if (aborted) return if (aborted) return
setHasSetup(true) setHasSetup(true)

View File

@ -5,13 +5,13 @@ import {ProfileFollowers as ProfileFollowersComponent} from '../com/profile/Prof
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
export const ProfileFollowers = ({visible, params}: ScreenParams) => { export const ProfileFollowers = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name} = params const {name} = params
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle(`Followers of ${name}`) store.nav.setTitle(navIdx, `Followers of ${name}`)
} }
}, [store, visible, name]) }, [store, visible, name])

View File

@ -5,13 +5,13 @@ import {ProfileFollows as ProfileFollowsComponent} from '../com/profile/ProfileF
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
export const ProfileFollows = ({visible, params}: ScreenParams) => { export const ProfileFollows = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name} = params const {name} = params
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle(`Followed by ${name}`) store.nav.setTitle(navIdx, `Followed by ${name}`)
} }
}, [store, visible, name]) }, [store, visible, name])

View File

@ -5,13 +5,13 @@ import {ProfileMembers as ProfileMembersComponent} from '../com/profile/ProfileM
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
export const ProfileMembers = ({visible, params}: ScreenParams) => { export const ProfileMembers = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name} = params const {name} = params
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle(`Members of ${name}`) store.nav.setTitle(navIdx, `Members of ${name}`)
} }
}, [store, visible, name]) }, [store, visible, name])

View File

@ -7,13 +7,13 @@ import {ScreenParams} from '../routes'
import {useStores} from '../../state' import {useStores} from '../../state'
import {colors} from '../lib/styles' import {colors} from '../lib/styles'
export const Search = ({visible, params}: ScreenParams) => { export const Search = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const {name} = params const {name} = params
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
store.nav.setTitle(`Search`) store.nav.setTitle(navIdx, `Search`)
} }
}, [store, visible, name]) }, [store, visible, name])
const onComposePress = () => { const onComposePress = () => {

View File

@ -8,14 +8,17 @@ import {ViewHeader} from '../com/util/ViewHeader'
import {Link} from '../com/util/Link' import {Link} from '../com/util/Link'
import {UserAvatar} from '../com/util/UserAvatar' import {UserAvatar} from '../com/util/UserAvatar'
export const Settings = observer(function Settings({visible}: ScreenParams) { export const Settings = observer(function Settings({
navIdx,
visible,
}: ScreenParams) {
const store = useStores() const store = useStores()
useEffect(() => { useEffect(() => {
if (!visible) { if (!visible) {
return return
} }
store.nav.setTitle('Settings') store.nav.setTitle(navIdx, 'Settings')
}, [visible, store]) }, [visible, store])
const onPressSignout = () => { const onPressSignout = () => {

View File

@ -268,7 +268,7 @@ export const MobileShell: React.FC = observer(() => {
<GestureDetector gesture={swipeGesture}> <GestureDetector gesture={swipeGesture}>
<ScreenContainer style={styles.screenContainer}> <ScreenContainer style={styles.screenContainer}>
{screenRenderDesc.screens.map( {screenRenderDesc.screens.map(
({Com, params, key, current, previous}) => { ({Com, navIdx, params, key, current, previous}) => {
return ( return (
<Screen <Screen
key={key} key={key}
@ -293,6 +293,7 @@ export const MobileShell: React.FC = observer(() => {
]}> ]}>
<Com <Com
params={params} params={params}
navIdx={navIdx}
visible={current} visible={current}
scrollElRef={current ? scrollElRef : undefined} scrollElRef={current ? scrollElRef : undefined}
/> />
@ -361,6 +362,7 @@ export const MobileShell: React.FC = observer(() => {
*/ */
type ScreenRenderDesc = MatchResult & { type ScreenRenderDesc = MatchResult & {
key: string key: string
navIdx: [number, number]
current: boolean current: boolean
previous: boolean previous: boolean
isNewTab: boolean isNewTab: boolean
@ -388,6 +390,7 @@ function constructScreenRenderDesc(nav: NavigationModel): {
hasNewTab = hasNewTab || tab.isNewTab hasNewTab = hasNewTab || tab.isNewTab
return Object.assign(matchRes, { return Object.assign(matchRes, {
key: `t${tab.id}-s${screen.index}`, key: `t${tab.id}-s${screen.index}`,
navIdx: [tab.id, screen.id],
current: isCurrent, current: isCurrent,
previous: isPrevious, previous: isPrevious,
isNewTab: tab.isNewTab, isNewTab: tab.isNewTab,