Replace the navigation model with react-navigation
This commit is contained in:
@@ -1,2 +1 @@
|
|||||||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||||
export const TABS_ENABLED = false
|
|
||||||
|
|||||||
+2
-1
@@ -16,7 +16,8 @@ export function init(store: RootStoreModel) {
|
|||||||
store.log.debug('Notifee foreground event', {type})
|
store.log.debug('Notifee foreground event', {type})
|
||||||
if (type === EventType.PRESS) {
|
if (type === EventType.PRESS) {
|
||||||
store.log.debug('User pressed a notifee, opening notifications')
|
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
|
notifee.onBackgroundEvent(async _e => {}) // notifee requires this but we handle it with onForegroundEvent
|
||||||
|
|||||||
@@ -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) {
|
export function getCurrentRoute(state: State) {
|
||||||
let node = state.routes[state.index]
|
let node = state.routes[state.index]
|
||||||
@@ -15,7 +29,7 @@ export function isTab(current: string, route: string) {
|
|||||||
// -prf
|
// -prf
|
||||||
return (
|
return (
|
||||||
current === route ||
|
current === route ||
|
||||||
current === `${route}Stack` ||
|
current === `${route}Tab` ||
|
||||||
current === `${route}Inner`
|
current === `${route}Inner`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -1,4 +1,5 @@
|
|||||||
import {NavigationState, PartialState} from '@react-navigation/native'
|
import {NavigationState, PartialState} from '@react-navigation/native'
|
||||||
|
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
@@ -13,15 +14,31 @@ export type CommonNavigatorParams = {
|
|||||||
Debug: undefined
|
Debug: undefined
|
||||||
Log: undefined
|
Log: undefined
|
||||||
}
|
}
|
||||||
export type HomeStackNavigatorParams = CommonNavigatorParams & {
|
|
||||||
|
export type HomeTabNavigatorParams = CommonNavigatorParams & {
|
||||||
Home: undefined
|
Home: undefined
|
||||||
}
|
}
|
||||||
export type NotificationsStackNavigatorParams = CommonNavigatorParams & {
|
|
||||||
|
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||||
Notifications: undefined
|
Notifications: undefined
|
||||||
}
|
}
|
||||||
export type SearchStackNavigatorParams = CommonNavigatorParams & {
|
|
||||||
|
export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
||||||
Search: undefined
|
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 =
|
export type State =
|
||||||
| NavigationState
|
| NavigationState
|
||||||
| Omit<PartialState<NavigationState>, 'stale'>
|
| Omit<PartialState<NavigationState>, 'stale'>
|
||||||
|
|||||||
@@ -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, string> = {
|
|
||||||
[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
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import {z} from 'zod'
|
|||||||
import {isObj, hasProp} from 'lib/type-guards'
|
import {isObj, hasProp} from 'lib/type-guards'
|
||||||
import {LogModel} from './log'
|
import {LogModel} from './log'
|
||||||
import {SessionModel} from './session'
|
import {SessionModel} from './session'
|
||||||
import {NavigationModel} from './navigation'
|
|
||||||
import {ShellUiModel} from './shell-ui'
|
import {ShellUiModel} from './shell-ui'
|
||||||
import {ProfilesViewModel} from './profiles-view'
|
import {ProfilesViewModel} from './profiles-view'
|
||||||
import {LinkMetasViewModel} from './link-metas-view'
|
import {LinkMetasViewModel} from './link-metas-view'
|
||||||
@@ -31,7 +30,6 @@ export class RootStoreModel {
|
|||||||
appInfo?: AppInfo
|
appInfo?: AppInfo
|
||||||
log = new LogModel()
|
log = new LogModel()
|
||||||
session = new SessionModel(this)
|
session = new SessionModel(this)
|
||||||
nav = new NavigationModel(this)
|
|
||||||
shell = new ShellUiModel(this)
|
shell = new ShellUiModel(this)
|
||||||
me = new MeModel(this)
|
me = new MeModel(this)
|
||||||
profiles = new ProfilesViewModel(this)
|
profiles = new ProfilesViewModel(this)
|
||||||
@@ -82,7 +80,6 @@ export class RootStoreModel {
|
|||||||
log: this.log.serialize(),
|
log: this.log.serialize(),
|
||||||
session: this.session.serialize(),
|
session: this.session.serialize(),
|
||||||
me: this.me.serialize(),
|
me: this.me.serialize(),
|
||||||
nav: this.nav.serialize(),
|
|
||||||
shell: this.shell.serialize(),
|
shell: this.shell.serialize(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,9 +98,6 @@ export class RootStoreModel {
|
|||||||
if (hasProp(v, 'me')) {
|
if (hasProp(v, 'me')) {
|
||||||
this.me.hydrate(v.me)
|
this.me.hydrate(v.me)
|
||||||
}
|
}
|
||||||
if (hasProp(v, 'nav')) {
|
|
||||||
this.nav.hydrate(v.nav)
|
|
||||||
}
|
|
||||||
if (hasProp(v, 'session')) {
|
if (hasProp(v, 'session')) {
|
||||||
this.session.hydrate(v.session)
|
this.session.hydrate(v.session)
|
||||||
}
|
}
|
||||||
@@ -144,7 +138,7 @@ export class RootStoreModel {
|
|||||||
*/
|
*/
|
||||||
async handleSessionDrop() {
|
async handleSessionDrop() {
|
||||||
this.log.debug('RootStoreModel:handleSessionDrop')
|
this.log.debug('RootStoreModel:handleSessionDrop')
|
||||||
this.nav.clear()
|
// this.nav.clear() TODO
|
||||||
this.me.clear()
|
this.me.clear()
|
||||||
this.emitSessionDropped()
|
this.emitSessionDropped()
|
||||||
}
|
}
|
||||||
@@ -155,7 +149,7 @@ export class RootStoreModel {
|
|||||||
clearAllSessionState() {
|
clearAllSessionState() {
|
||||||
this.log.debug('RootStoreModel:clearAllSessionState')
|
this.log.debug('RootStoreModel:clearAllSessionState')
|
||||||
this.session.clear()
|
this.session.clear()
|
||||||
this.nav.clear()
|
// this.nav.clear() TODO
|
||||||
this.me.clear()
|
this.me.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function Component({}: {}) {
|
|||||||
token: confirmCode,
|
token: confirmCode,
|
||||||
})
|
})
|
||||||
Toast.show('Your account has been deleted')
|
Toast.show('Your account has been deleted')
|
||||||
store.nav.tab.fixedTabReset()
|
// store.nav.tab.fixedTabReset() TODO
|
||||||
store.session.clear()
|
store.session.clear()
|
||||||
store.shell.closeModal()
|
store.shell.closeModal()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||||
import {CenteredView, FlatList} from '../util/Views'
|
import {CenteredView, FlatList} from '../util/Views'
|
||||||
@@ -18,10 +19,10 @@ import {FeedModel} from 'state/models/feed-view'
|
|||||||
import {FeedItem} from './FeedItem'
|
import {FeedItem} from './FeedItem'
|
||||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {MagnifyingGlassIcon} from 'lib/icons'
|
import {MagnifyingGlassIcon} from 'lib/icons'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
|
||||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||||
const ERROR_FEED_ITEM = {_reactKey: '__error__'}
|
const ERROR_FEED_ITEM = {_reactKey: '__error__'}
|
||||||
@@ -47,9 +48,9 @@ export const Feed = observer(function Feed({
|
|||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palInverted = usePalette('inverted')
|
const palInverted = usePalette('inverted')
|
||||||
const store = useStores()
|
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
const data = React.useMemo(() => {
|
const data = React.useMemo(() => {
|
||||||
let feedItems: any[] = []
|
let feedItems: any[] = []
|
||||||
@@ -112,7 +113,12 @@ export const Feed = observer(function Feed({
|
|||||||
<Button
|
<Button
|
||||||
type="inverted"
|
type="inverted"
|
||||||
style={styles.emptyBtn}
|
style={styles.emptyBtn}
|
||||||
onPress={() => store.nav.navigate('/search')}>
|
onPress={
|
||||||
|
() =>
|
||||||
|
navigation.navigate(
|
||||||
|
'SearchTab',
|
||||||
|
) /* TODO make sure it goes to root of the tab */
|
||||||
|
}>
|
||||||
<Text type="lg-medium" style={palInverted.text}>
|
<Text type="lg-medium" style={palInverted.text}>
|
||||||
Find accounts
|
Find accounts
|
||||||
</Text>
|
</Text>
|
||||||
@@ -134,7 +140,7 @@ export const Feed = observer(function Feed({
|
|||||||
}
|
}
|
||||||
return <FeedItem item={item} showFollowBtn={showPostFollowBtn} />
|
return <FeedItem item={item} showFollowBtn={showPostFollowBtn} />
|
||||||
},
|
},
|
||||||
[feed, onPressTryAgain, showPostFollowBtn, pal, palInverted, store.nav],
|
[feed, onPressTryAgain, showPostFollowBtn, pal, palInverted, navigation],
|
||||||
)
|
)
|
||||||
|
|
||||||
const FeedFooter = React.useCallback(
|
const FeedFooter = React.useCallback(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {BlurView} from '../util/BlurView'
|
import {BlurView} from '../util/BlurView'
|
||||||
import {ProfileViewModel} from 'state/models/profile-view'
|
import {ProfileViewModel} from 'state/models/profile-view'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
@@ -28,6 +29,7 @@ import {UserAvatar} from '../util/UserAvatar'
|
|||||||
import {UserBanner} from '../util/UserBanner'
|
import {UserBanner} from '../util/UserBanner'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
|
||||||
const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
|
const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
|
||||||
|
|
||||||
@@ -40,16 +42,17 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const onPressBack = () => {
|
const onPressBack = React.useCallback(() => {
|
||||||
store.nav.tab.goBack()
|
navigation.goBack()
|
||||||
}
|
}, [navigation])
|
||||||
const onPressAvi = () => {
|
const onPressAvi = React.useCallback(() => {
|
||||||
if (view.avatar) {
|
if (view.avatar) {
|
||||||
store.shell.openLightbox(new ProfileImageLightbox(view))
|
store.shell.openLightbox(new ProfileImageLightbox(view))
|
||||||
}
|
}
|
||||||
}
|
}, [store, view])
|
||||||
const onPressToggleFollow = () => {
|
const onPressToggleFollow = React.useCallback(() => {
|
||||||
view?.toggleFollowing().then(
|
view?.toggleFollowing().then(
|
||||||
() => {
|
() => {
|
||||||
Toast.show(
|
Toast.show(
|
||||||
@@ -60,28 +63,28 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
},
|
},
|
||||||
err => store.log.error('Failed to toggle follow', err),
|
err => store.log.error('Failed to toggle follow', err),
|
||||||
)
|
)
|
||||||
}
|
}, [view, store])
|
||||||
const onPressEditProfile = () => {
|
const onPressEditProfile = React.useCallback(() => {
|
||||||
track('ProfileHeader:EditProfileButtonClicked')
|
track('ProfileHeader:EditProfileButtonClicked')
|
||||||
store.shell.openModal({
|
store.shell.openModal({
|
||||||
name: 'edit-profile',
|
name: 'edit-profile',
|
||||||
profileView: view,
|
profileView: view,
|
||||||
onUpdate: onRefreshAll,
|
onUpdate: onRefreshAll,
|
||||||
})
|
})
|
||||||
}
|
}, [track, store, view, onRefreshAll])
|
||||||
const onPressFollowers = () => {
|
const onPressFollowers = React.useCallback(() => {
|
||||||
track('ProfileHeader:FollowersButtonClicked')
|
track('ProfileHeader:FollowersButtonClicked')
|
||||||
store.nav.navigate(`/profile/${view.handle}/followers`)
|
navigation.push('ProfileFollowers', {name: view.handle})
|
||||||
}
|
}, [track, navigation, view])
|
||||||
const onPressFollows = () => {
|
const onPressFollows = React.useCallback(() => {
|
||||||
track('ProfileHeader:FollowsButtonClicked')
|
track('ProfileHeader:FollowsButtonClicked')
|
||||||
store.nav.navigate(`/profile/${view.handle}/follows`)
|
navigation.push('ProfileFollows', {name: view.handle})
|
||||||
}
|
}, [track, navigation, view])
|
||||||
const onPressShare = () => {
|
const onPressShare = React.useCallback(() => {
|
||||||
track('ProfileHeader:ShareButtonClicked')
|
track('ProfileHeader:ShareButtonClicked')
|
||||||
Share.share({url: toShareUrl(`/profile/${view.handle}`)})
|
Share.share({url: toShareUrl(`/profile/${view.handle}`)})
|
||||||
}
|
}, [track, view])
|
||||||
const onPressMuteAccount = async () => {
|
const onPressMuteAccount = React.useCallback(async () => {
|
||||||
track('ProfileHeader:MuteAccountButtonClicked')
|
track('ProfileHeader:MuteAccountButtonClicked')
|
||||||
try {
|
try {
|
||||||
await view.muteAccount()
|
await view.muteAccount()
|
||||||
@@ -90,8 +93,8 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
store.log.error('Failed to mute account', e)
|
store.log.error('Failed to mute account', e)
|
||||||
Toast.show(`There was an issue! ${e.toString()}`)
|
Toast.show(`There was an issue! ${e.toString()}`)
|
||||||
}
|
}
|
||||||
}
|
}, [track, view, store])
|
||||||
const onPressUnmuteAccount = async () => {
|
const onPressUnmuteAccount = React.useCallback(async () => {
|
||||||
track('ProfileHeader:UnmuteAccountButtonClicked')
|
track('ProfileHeader:UnmuteAccountButtonClicked')
|
||||||
try {
|
try {
|
||||||
await view.unmuteAccount()
|
await view.unmuteAccount()
|
||||||
@@ -100,14 +103,14 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
store.log.error('Failed to unmute account', e)
|
store.log.error('Failed to unmute account', e)
|
||||||
Toast.show(`There was an issue! ${e.toString()}`)
|
Toast.show(`There was an issue! ${e.toString()}`)
|
||||||
}
|
}
|
||||||
}
|
}, [track, view, store])
|
||||||
const onPressReportAccount = () => {
|
const onPressReportAccount = React.useCallback(() => {
|
||||||
track('ProfileHeader:ReportAccountButtonClicked')
|
track('ProfileHeader:ReportAccountButtonClicked')
|
||||||
store.shell.openModal({
|
store.shell.openModal({
|
||||||
name: 'report-account',
|
name: 'report-account',
|
||||||
did: view.did,
|
did: view.did,
|
||||||
})
|
})
|
||||||
}
|
}, [track, store, view])
|
||||||
|
|
||||||
// loading
|
// loading
|
||||||
// =
|
// =
|
||||||
|
|||||||
+71
-31
@@ -2,6 +2,8 @@ import React from 'react'
|
|||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
Linking,
|
Linking,
|
||||||
|
GestureResponderEvent,
|
||||||
|
Platform,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -9,11 +11,18 @@ import {
|
|||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
import {useLinkProps, useNavigation} from '@react-navigation/native'
|
||||||
import {Text} from './text/Text'
|
import {Text} from './text/Text'
|
||||||
import {TypographyVariant} from 'lib/ThemeContext'
|
import {TypographyVariant} from 'lib/ThemeContext'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
import {matchPath} from 'view/screens'
|
||||||
import {useStores, RootStoreModel} from 'state/index'
|
import {useStores, RootStoreModel} from 'state/index'
|
||||||
import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers'
|
import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers'
|
||||||
|
|
||||||
|
type Event =
|
||||||
|
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||||
|
| GestureResponderEvent
|
||||||
|
|
||||||
export const Link = observer(function Link({
|
export const Link = observer(function Link({
|
||||||
style,
|
style,
|
||||||
href,
|
href,
|
||||||
@@ -27,35 +36,30 @@ export const Link = observer(function Link({
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
noFeedback?: boolean
|
noFeedback?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
let {...props} = useLinkProps({to: href})
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const onPress = () => {
|
const navigation = useNavigation<NavigationProp>()
|
||||||
if (href) {
|
|
||||||
handleLink(store, href, false)
|
props.onPress = React.useCallback(
|
||||||
}
|
(e?: Event) => {
|
||||||
}
|
if (typeof href === 'string') {
|
||||||
const onLongPress = () => {
|
return onPressInner(store, navigation, href, e)
|
||||||
if (href) {
|
|
||||||
handleLink(store, href, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[store, navigation, href],
|
||||||
|
)
|
||||||
|
|
||||||
if (noFeedback) {
|
if (noFeedback) {
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback delayPressIn={50} {...props}>
|
||||||
onPress={onPress}
|
<View style={style} {...props}>
|
||||||
onLongPress={onLongPress}
|
|
||||||
delayPressIn={50}>
|
|
||||||
<View style={style}>
|
|
||||||
{children ? children : <Text>{title || 'link'}</Text>}
|
{children ? children : <Text>{title || 'link'}</Text>}
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity delayPressIn={50} style={style} {...props}>
|
||||||
onPress={onPress}
|
|
||||||
onLongPress={onLongPress}
|
|
||||||
delayPressIn={50}
|
|
||||||
style={style}>
|
|
||||||
{children ? children : <Text>{title || 'link'}</Text>}
|
{children ? children : <Text>{title || 'link'}</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
@@ -72,29 +76,65 @@ export const TextLink = observer(function TextLink({
|
|||||||
href: string
|
href: string
|
||||||
text: string
|
text: string
|
||||||
}) {
|
}) {
|
||||||
|
const {...props} = useLinkProps({to: href})
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const onPress = () => {
|
const navigation = useNavigation<NavigationProp>()
|
||||||
handleLink(store, href, false)
|
|
||||||
}
|
props.onPress = React.useCallback(
|
||||||
const onLongPress = () => {
|
(e?: Event) => {
|
||||||
handleLink(store, href, true)
|
return onPressInner(store, navigation, href, e)
|
||||||
}
|
},
|
||||||
|
[store, navigation, href],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text type={type} style={style} onPress={onPress} onLongPress={onLongPress}>
|
<Text type={type} style={style} {...props}>
|
||||||
{text}
|
{text}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleLink(store: RootStoreModel, href: string, longPress: boolean) {
|
// NOTE
|
||||||
|
// we can't use the onPress given by useLinkProps because it will
|
||||||
|
// match most paths to the HomeTab routes while we actually want to
|
||||||
|
// preserve the tab the app is currently in
|
||||||
|
//
|
||||||
|
// we also have some additional behaviors - closing the current modal,
|
||||||
|
// converting bsky urls, and opening http/s links in the system browser
|
||||||
|
//
|
||||||
|
// this method copies from the onPress implementation but adds our
|
||||||
|
// needed customizations
|
||||||
|
// -prf
|
||||||
|
function onPressInner(
|
||||||
|
store: RootStoreModel,
|
||||||
|
navigation: NavigationProp,
|
||||||
|
href: string,
|
||||||
|
e?: Event,
|
||||||
|
) {
|
||||||
|
let shouldHandle = false
|
||||||
|
|
||||||
|
if (Platform.OS !== 'web' || !e) {
|
||||||
|
shouldHandle = e ? !e.defaultPrevented : true
|
||||||
|
} else if (
|
||||||
|
!e.defaultPrevented && // onPress prevented default
|
||||||
|
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
|
||||||
|
(e.button == null || e.button === 0) && // ignore everything but left clicks
|
||||||
|
[undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc.
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
shouldHandle = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldHandle) {
|
||||||
href = convertBskyAppUrlIfNeeded(href)
|
href = convertBskyAppUrlIfNeeded(href)
|
||||||
if (href.startsWith('http')) {
|
if (href.startsWith('http')) {
|
||||||
Linking.openURL(href)
|
Linking.openURL(href)
|
||||||
} else if (longPress) {
|
|
||||||
store.shell.closeModal() // close any active modals
|
|
||||||
store.nav.newTab(href)
|
|
||||||
} else {
|
} else {
|
||||||
store.shell.closeModal() // close any active modals
|
store.shell.closeModal() // close any active modals
|
||||||
store.nav.navigate(href)
|
|
||||||
|
const {name, params} = matchPath(href)
|
||||||
|
// @ts-ignore we're not able to type check on this one -prf
|
||||||
|
navigation.push(name, params)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useNavigation, DrawerActions} from '@react-navigation/native'
|
||||||
import {UserAvatar} from './UserAvatar'
|
import {UserAvatar} from './UserAvatar'
|
||||||
import {Text} from './text/Text'
|
import {Text} from './text/Text'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
@@ -9,6 +10,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
|||||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {isDesktopWeb} from '../../../platform/detection'
|
import {isDesktopWeb} from '../../../platform/detection'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
|
||||||
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
||||||
|
|
||||||
@@ -23,17 +25,22 @@ export const ViewHeader = observer(function ViewHeader({
|
|||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const onPressBack = () => {
|
|
||||||
store.nav.tab.goBack()
|
const onPressBack = React.useCallback(() => {
|
||||||
}
|
navigation.goBack()
|
||||||
const onPressMenu = () => {
|
}, [navigation])
|
||||||
|
|
||||||
|
const onPressMenu = React.useCallback(() => {
|
||||||
track('ViewHeader:MenuButtonClicked')
|
track('ViewHeader:MenuButtonClicked')
|
||||||
store.shell.setMainMenuOpen(true)
|
navigation.dispatch(DrawerActions.openDrawer())
|
||||||
}
|
}, [track, navigation])
|
||||||
|
|
||||||
if (typeof canGoBack === 'undefined') {
|
if (typeof canGoBack === 'undefined') {
|
||||||
canGoBack = store.nav.tab.canGoBack
|
canGoBack = navigation.canGoBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDesktopWeb) {
|
if (isDesktopWeb) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {Button, ButtonType} from './Button'
|
|||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {TABS_ENABLED} from 'lib/build-flags'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
|
||||||
@@ -138,15 +137,6 @@ export function PostDropdownBtn({
|
|||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
|
||||||
const dropdownItems: DropdownItem[] = [
|
const dropdownItems: DropdownItem[] = [
|
||||||
TABS_ENABLED
|
|
||||||
? {
|
|
||||||
icon: ['far', 'clone'],
|
|
||||||
label: 'Open in new tab',
|
|
||||||
onPress() {
|
|
||||||
store.nav.newTab(itemHref)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
{
|
{
|
||||||
icon: 'language',
|
icon: 'language',
|
||||||
label: 'Translate...',
|
label: 'Translate...',
|
||||||
|
|||||||
+48
-46
@@ -5,9 +5,9 @@ import {createDrawerNavigator} from '@react-navigation/drawer'
|
|||||||
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
|
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HomeStackNavigatorParams,
|
HomeTabNavigatorParams,
|
||||||
NotificationsStackNavigatorParams,
|
NotificationsTabNavigatorParams,
|
||||||
SearchStackNavigatorParams,
|
SearchTabNavigatorParams,
|
||||||
State,
|
State,
|
||||||
} from 'lib/routes/types'
|
} from 'lib/routes/types'
|
||||||
|
|
||||||
@@ -28,12 +28,12 @@ import {DebugScreen} from './screens/Debug'
|
|||||||
import {LogScreen} from './screens/Log'
|
import {LogScreen} from './screens/Log'
|
||||||
|
|
||||||
const HomeDrawer = createDrawerNavigator()
|
const HomeDrawer = createDrawerNavigator()
|
||||||
const HomeStack = createNativeStackNavigator<HomeStackNavigatorParams>()
|
const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>()
|
||||||
const SearchDrawer = createDrawerNavigator()
|
const SearchDrawer = createDrawerNavigator()
|
||||||
const SearchStack = createNativeStackNavigator<SearchStackNavigatorParams>()
|
const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>()
|
||||||
const NotificationsDrawer = createDrawerNavigator()
|
const NotificationsDrawer = createDrawerNavigator()
|
||||||
const NotificationsStack =
|
const NotificationsTab =
|
||||||
createNativeStackNavigator<NotificationsStackNavigatorParams>()
|
createNativeStackNavigator<NotificationsTabNavigatorParams>()
|
||||||
const Tab = createBottomTabNavigator()
|
const Tab = createBottomTabNavigator()
|
||||||
|
|
||||||
type RouteParams = Record<string, string>
|
type RouteParams = Record<string, string>
|
||||||
@@ -82,7 +82,21 @@ const ROUTES: Record<string, Route> = {
|
|||||||
Log: r('/sys/log'),
|
Log: r('/sys/log'),
|
||||||
}
|
}
|
||||||
|
|
||||||
const LINKING = {
|
export function matchPath(path: string): {name: string; params: RouteParams} {
|
||||||
|
let name = 'Home' // TODO should be not found
|
||||||
|
let params: RouteParams = {}
|
||||||
|
for (const [screenName, matcher] of Object.entries(ROUTES)) {
|
||||||
|
const res = matcher.match(path)
|
||||||
|
if (res) {
|
||||||
|
name = screenName
|
||||||
|
params = res.params
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {name, params}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LINKING = {
|
||||||
prefixes: ['bsky://', 'https://bsky.app'],
|
prefixes: ['bsky://', 'https://bsky.app'],
|
||||||
|
|
||||||
getPathFromState(state: State) {
|
getPathFromState(state: State) {
|
||||||
@@ -101,26 +115,14 @@ const LINKING = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getStateFromPath(path: string) {
|
getStateFromPath(path: string) {
|
||||||
// match the route
|
const {name, params} = matchPath(path)
|
||||||
let match = 'Home' // TODO should be not found
|
if (name === 'Search') {
|
||||||
let params: RouteParams = {}
|
return buildStateObject('SearchTab', 'Search', params)
|
||||||
for (const [name, matcher] of Object.entries(ROUTES)) {
|
|
||||||
const res = matcher.match(path)
|
|
||||||
if (res) {
|
|
||||||
match = name
|
|
||||||
params = res.params
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
if (name === 'Notifications') {
|
||||||
|
return buildStateObject('NotificationsTab', 'Notifications', params)
|
||||||
}
|
}
|
||||||
|
return buildStateObject('HomeTab', name, params)
|
||||||
// build the state object
|
|
||||||
if (match === 'Search') {
|
|
||||||
return buildStateObject('SearchStack', 'Search', params)
|
|
||||||
}
|
|
||||||
if (match === 'Notifications') {
|
|
||||||
return buildStateObject('NotificationsStack', 'Notifications', params)
|
|
||||||
}
|
|
||||||
return buildStateObject('HomeStack', match, params)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,17 +169,17 @@ function HomeDrawerNavigator() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeStackNavigator() {
|
function HomeTabNavigator() {
|
||||||
return (
|
return (
|
||||||
<HomeStack.Navigator
|
<HomeTab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}>
|
}}>
|
||||||
<HomeStack.Screen name="Home" component={HomeDrawerNavigator} />
|
<HomeTab.Screen name="Home" component={HomeDrawerNavigator} />
|
||||||
{commonScreens(HomeStack)}
|
{commonScreens(HomeTab)}
|
||||||
</HomeStack.Navigator>
|
</HomeTab.Navigator>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,20 +197,20 @@ function NotificationsDrawerNavigator() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationsStackNavigator() {
|
function NotificationsTabNavigator() {
|
||||||
return (
|
return (
|
||||||
<NotificationsStack.Navigator
|
<NotificationsTab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}>
|
}}>
|
||||||
<NotificationsStack.Screen
|
<NotificationsTab.Screen
|
||||||
name="Notifications"
|
name="Notifications"
|
||||||
component={NotificationsDrawerNavigator}
|
component={NotificationsDrawerNavigator}
|
||||||
/>
|
/>
|
||||||
{commonScreens(NotificationsStack)}
|
{commonScreens(NotificationsTab)}
|
||||||
</NotificationsStack.Navigator>
|
</NotificationsTab.Navigator>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,17 +225,17 @@ function SearchDrawerNavigator() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchStackNavigator() {
|
function SearchTabNavigator() {
|
||||||
return (
|
return (
|
||||||
<SearchStack.Navigator
|
<SearchTab.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
fullScreenGestureEnabled: true,
|
fullScreenGestureEnabled: true,
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}>
|
}}>
|
||||||
<SearchStack.Screen name="Search" component={SearchDrawerNavigator} />
|
<SearchTab.Screen name="Search" component={SearchDrawerNavigator} />
|
||||||
{commonScreens(SearchStack)}
|
{commonScreens(SearchTab)}
|
||||||
</SearchStack.Navigator>
|
</SearchTab.Navigator>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,16 +243,16 @@ function TabsNavigator() {
|
|||||||
const tabBar = React.useCallback(props => <BottomBar {...props} />, [])
|
const tabBar = React.useCallback(props => <BottomBar {...props} />, [])
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName="HomeStack"
|
initialRouteName="HomeTab"
|
||||||
backBehavior="initialRoute"
|
backBehavior="initialRoute"
|
||||||
screenOptions={{headerShown: false}}
|
screenOptions={{headerShown: false}}
|
||||||
tabBar={tabBar}>
|
tabBar={tabBar}>
|
||||||
<Tab.Screen name="HomeStack" component={HomeStackNavigator} />
|
<Tab.Screen name="HomeTab" component={HomeTabNavigator} />
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="NotificationsStack"
|
name="NotificationsTab"
|
||||||
component={NotificationsStackNavigator}
|
component={NotificationsTabNavigator}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen name="SearchStack" component={SearchStackNavigator} />
|
<Tab.Screen name="SearchTab" component={SearchTabNavigator} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import {FlatList, View} from 'react-native'
|
|||||||
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import useAppState from 'react-native-appstate-hook'
|
import useAppState from 'react-native-appstate-hook'
|
||||||
import {
|
import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
|
||||||
NativeStackScreenProps,
|
|
||||||
HomeStackNavigatorParams,
|
|
||||||
} from 'lib/routes/types'
|
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
import {Feed} from '../com/posts/Feed'
|
import {Feed} from '../com/posts/Feed'
|
||||||
import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
|
import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
|
||||||
@@ -20,7 +17,7 @@ import {ComposeIcon2} from 'lib/icons'
|
|||||||
|
|
||||||
const HEADER_HEIGHT = 42
|
const HEADER_HEIGHT = 42
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<HomeStackNavigatorParams, 'Home'>
|
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
||||||
export const HomeScreen = observer(function Home({}: Props) {
|
export const HomeScreen = observer(function Home({}: Props) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const onMainScroll = useOnMainScroll(store)
|
const onMainScroll = useOnMainScroll(store)
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Button, StyleSheet, View} from 'react-native'
|
import {Button, StyleSheet, View} from 'react-native'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
import {Text} from '../com/util/text/Text'
|
import {Text} from '../com/util/text/Text'
|
||||||
import {useStores} from 'state/index'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
|
||||||
export const NotFound = () => {
|
export const NotFound = () => {
|
||||||
const stores = useStores()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
const onPressHome = React.useCallback(() => {
|
||||||
|
navigation.navigate('HomeTab') // TODO go fully home
|
||||||
|
}, [navigation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID="notFoundView">
|
<View testID="notFoundView">
|
||||||
<ViewHeader title="Page not found" />
|
<ViewHeader title="Page not found" />
|
||||||
@@ -14,7 +20,7 @@ export const NotFound = () => {
|
|||||||
<Button
|
<Button
|
||||||
testID="navigateHomeButton"
|
testID="navigateHomeButton"
|
||||||
title="Home"
|
title="Home"
|
||||||
onPress={() => stores.nav.navigate('/')}
|
onPress={onPressHome}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {useFocusEffect} from '@react-navigation/native'
|
|||||||
import useAppState from 'react-native-appstate-hook'
|
import useAppState from 'react-native-appstate-hook'
|
||||||
import {
|
import {
|
||||||
NativeStackScreenProps,
|
NativeStackScreenProps,
|
||||||
NotificationsStackNavigatorParams,
|
NotificationsTabNavigatorParams,
|
||||||
} from 'lib/routes/types'
|
} from 'lib/routes/types'
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
import {Feed} from '../com/notifications/Feed'
|
import {Feed} from '../com/notifications/Feed'
|
||||||
@@ -16,7 +16,7 @@ import {useAnalytics} from 'lib/analytics'
|
|||||||
const NOTIFICATIONS_POLL_INTERVAL = 15e3
|
const NOTIFICATIONS_POLL_INTERVAL = 15e3
|
||||||
|
|
||||||
export const NotificationsScreen = ({}: NativeStackScreenProps<
|
export const NotificationsScreen = ({}: NativeStackScreenProps<
|
||||||
NotificationsStackNavigatorParams,
|
NotificationsTabNavigatorParams,
|
||||||
'Notifications'
|
'Notifications'
|
||||||
>) => {
|
>) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import {ScrollView} from '../com/util/Views'
|
import {ScrollView} from '../com/util/Views'
|
||||||
import {
|
import {
|
||||||
NativeStackScreenProps,
|
NativeStackScreenProps,
|
||||||
SearchStackNavigatorParams,
|
SearchTabNavigatorParams,
|
||||||
} from 'lib/routes/types'
|
} from 'lib/routes/types'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {UserAvatar} from '../com/util/UserAvatar'
|
import {UserAvatar} from '../com/util/UserAvatar'
|
||||||
@@ -34,7 +34,7 @@ import {useAnalytics} from 'lib/analytics'
|
|||||||
const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
|
||||||
const FIVE_MIN = 5 * 60 * 1e3
|
const FIVE_MIN = 5 * 60 * 1e3
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<SearchStackNavigatorParams, 'Search'>
|
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
|
||||||
export const SearchScreen = observer(({}: Props) => {
|
export const SearchScreen = observer(({}: Props) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {ScrollView} from '../com/util/Views'
|
|||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
NativeStackScreenProps,
|
NativeStackScreenProps,
|
||||||
SearchStackNavigatorParams,
|
SearchTabNavigatorParams,
|
||||||
} from 'lib/routes/types'
|
} from 'lib/routes/types'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
@@ -16,7 +16,7 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
|||||||
|
|
||||||
const FIVE_MIN = 5 * 60 * 1e3
|
const FIVE_MIN = 5 * 60 * 1e3
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<SearchStackNavigatorParams, 'Search'>
|
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
|
||||||
export const SearchScreen = observer(({}: Props) => {
|
export const SearchScreen = observer(({}: Props) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {
|
||||||
|
useFocusEffect,
|
||||||
|
useNavigation,
|
||||||
|
StackActions,
|
||||||
|
} from '@react-navigation/native'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
@@ -26,6 +30,7 @@ import {useTheme} from 'lib/ThemeContext'
|
|||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {AccountData} from 'state/models/session'
|
import {AccountData} from 'state/models/session'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
|
||||||
export const SettingsScreen = observer(
|
export const SettingsScreen = observer(
|
||||||
function Settings({}: NativeStackScreenProps<
|
function Settings({}: NativeStackScreenProps<
|
||||||
@@ -35,6 +40,7 @@ export const SettingsScreen = observer(
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {screen, track} = useAnalytics()
|
const {screen, track} = useAnalytics()
|
||||||
const [isSwitching, setIsSwitching] = React.useState(false)
|
const [isSwitching, setIsSwitching] = React.useState(false)
|
||||||
|
|
||||||
@@ -50,13 +56,15 @@ export const SettingsScreen = observer(
|
|||||||
setIsSwitching(true)
|
setIsSwitching(true)
|
||||||
if (await store.session.resumeSession(acct)) {
|
if (await store.session.resumeSession(acct)) {
|
||||||
setIsSwitching(false)
|
setIsSwitching(false)
|
||||||
store.nav.tab.fixedTabReset()
|
navigation.navigate('HomeTab')
|
||||||
|
navigation.dispatch(StackActions.popToTop())
|
||||||
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsSwitching(false)
|
setIsSwitching(false)
|
||||||
Toast.show('Sorry! We need you to enter your password.')
|
Toast.show('Sorry! We need you to enter your password.')
|
||||||
store.nav.tab.fixedTabReset()
|
navigation.navigate('HomeTab')
|
||||||
|
navigation.dispatch(StackActions.popToTop())
|
||||||
store.session.clear()
|
store.session.clear()
|
||||||
}
|
}
|
||||||
const onPressAddAccount = () => {
|
const onPressAddAccount = () => {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
|||||||
} else if (isTab(state.routes[state.index].name, tab)) {
|
} else if (isTab(state.routes[state.index].name, tab)) {
|
||||||
navigation.dispatch(StackActions.popToTop())
|
navigation.dispatch(StackActions.popToTop())
|
||||||
} else {
|
} else {
|
||||||
navigation.navigate(`${tab}Stack`)
|
navigation.navigate(`${tab}Tab`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[store, track, navigation],
|
[store, track, navigation],
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const Drawer = observer(({navigation}: DrawerContentComponentProps) => {
|
|||||||
} else {
|
} else {
|
||||||
// wait for drawer anim to finish
|
// wait for drawer anim to finish
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigation.navigate(`${tab}Stack`)
|
navigation.navigate(`${tab}Tab`)
|
||||||
}, 250)
|
}, 250)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,30 +1,13 @@
|
|||||||
import React, {useState} from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {StatusBar, StyleSheet, useWindowDimensions, View} from 'react-native'
|
||||||
Animated,
|
|
||||||
StatusBar,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableWithoutFeedback,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {ScreenContainer, Screen} from 'react-native-screens'
|
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|
||||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {NavigationModel} from 'state/models/navigation'
|
|
||||||
import {match, MatchResult} from '../../routes'
|
|
||||||
import {Login} from '../../screens/Login'
|
import {Login} from '../../screens/Login'
|
||||||
import {Menu} from './Menu'
|
|
||||||
import {BottomBar} from './BottomBar'
|
|
||||||
import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
|
|
||||||
import {ModalsContainer} from '../../com/modals/Modal'
|
import {ModalsContainer} from '../../com/modals/Modal'
|
||||||
import {Lightbox} from '../../com/lightbox/Lightbox'
|
import {Lightbox} from '../../com/lightbox/Lightbox'
|
||||||
import {Text} from '../../com/util/text/Text'
|
import {Text} from '../../com/util/text/Text'
|
||||||
import {ErrorBoundary} from '../../com/util/ErrorBoundary'
|
|
||||||
import {Composer} from './Composer'
|
import {Composer} from './Composer'
|
||||||
import {s, colors} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
@@ -35,81 +18,6 @@ export const MobileShell: React.FC = observer(() => {
|
|||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const winDim = useWindowDimensions()
|
const winDim = useWindowDimensions()
|
||||||
const [menuSwipingDirection, setMenuSwipingDirection] = useState(0)
|
|
||||||
const swipeGestureInterp = useAnimatedValue(0)
|
|
||||||
const safeAreaInsets = useSafeAreaInsets()
|
|
||||||
const screenRenderDesc = constructScreenRenderDesc(store.nav)
|
|
||||||
|
|
||||||
// navigation swipes
|
|
||||||
// =
|
|
||||||
const isMenuActive = store.shell.isMainMenuOpen
|
|
||||||
const canSwipeLeft = store.nav.tab.canGoBack || !isMenuActive
|
|
||||||
const canSwipeRight = isMenuActive
|
|
||||||
const onNavSwipeStartDirection = (dx: number) => {
|
|
||||||
if (dx < 0 && !store.nav.tab.canGoBack) {
|
|
||||||
setMenuSwipingDirection(dx)
|
|
||||||
} else if (dx > 0 && isMenuActive) {
|
|
||||||
setMenuSwipingDirection(dx)
|
|
||||||
} else {
|
|
||||||
setMenuSwipingDirection(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const onNavSwipeEnd = (dx: number) => {
|
|
||||||
if (dx < 0) {
|
|
||||||
if (store.nav.tab.canGoBack) {
|
|
||||||
store.nav.tab.goBack()
|
|
||||||
} else {
|
|
||||||
store.shell.setMainMenuOpen(true)
|
|
||||||
}
|
|
||||||
} else if (dx > 0) {
|
|
||||||
if (isMenuActive) {
|
|
||||||
store.shell.setMainMenuOpen(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setMenuSwipingDirection(0)
|
|
||||||
}
|
|
||||||
const swipeTranslateX = Animated.multiply(
|
|
||||||
swipeGestureInterp,
|
|
||||||
winDim.width * -1,
|
|
||||||
)
|
|
||||||
const swipeTransform = store.nav.tab.canGoBack
|
|
||||||
? {transform: [{translateX: swipeTranslateX}]}
|
|
||||||
: undefined
|
|
||||||
let shouldRenderMenu = false
|
|
||||||
let menuTranslateX
|
|
||||||
const menuDrawerWidth = winDim.width - 100
|
|
||||||
if (isMenuActive) {
|
|
||||||
// menu is active, interpret swipes as closes
|
|
||||||
menuTranslateX = Animated.multiply(swipeGestureInterp, menuDrawerWidth * -1)
|
|
||||||
shouldRenderMenu = true
|
|
||||||
} else if (!store.nav.tab.canGoBack) {
|
|
||||||
// at back of history, interpret swipes as opens
|
|
||||||
menuTranslateX = Animated.subtract(
|
|
||||||
menuDrawerWidth * -1,
|
|
||||||
Animated.multiply(swipeGestureInterp, menuDrawerWidth),
|
|
||||||
)
|
|
||||||
shouldRenderMenu = true
|
|
||||||
}
|
|
||||||
const menuSwipeTransform = menuTranslateX
|
|
||||||
? {
|
|
||||||
transform: [{translateX: menuTranslateX}],
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
const swipeOpacity = {
|
|
||||||
opacity: swipeGestureInterp.interpolate({
|
|
||||||
inputRange: [-1, 0, 1],
|
|
||||||
outputRange: [0, 0.6, 0],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
const menuSwipeOpacity =
|
|
||||||
menuSwipingDirection !== 0
|
|
||||||
? {
|
|
||||||
opacity: swipeGestureInterp.interpolate({
|
|
||||||
inputRange: menuSwipingDirection > 0 ? [0, 1] : [-1, 0],
|
|
||||||
outputRange: [0.6, 0],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (store.hackUpgradeNeeded) {
|
if (store.hackUpgradeNeeded) {
|
||||||
return (
|
return (
|
||||||
@@ -158,9 +66,6 @@ export const MobileShell: React.FC = observer(() => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenBg = {
|
|
||||||
backgroundColor: theme.colorScheme === 'dark' ? colors.black : colors.gray1,
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
@@ -184,90 +89,8 @@ export const MobileShell: React.FC = observer(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* This method produces the information needed by the shell to
|
|
||||||
* render the current screens with screen-caching behaviors.
|
|
||||||
*/
|
|
||||||
type ScreenRenderDesc = MatchResult & {
|
|
||||||
key: string
|
|
||||||
navIdx: string
|
|
||||||
current: boolean
|
|
||||||
previous: boolean
|
|
||||||
isNewTab: boolean
|
|
||||||
}
|
|
||||||
function constructScreenRenderDesc(nav: NavigationModel): {
|
|
||||||
icon: IconProp
|
|
||||||
hasNewTab: boolean
|
|
||||||
screens: ScreenRenderDesc[]
|
|
||||||
} {
|
|
||||||
let hasNewTab = false
|
|
||||||
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 isPrevious = nav.isCurrentScreen(tab.id, screen.index + 1)
|
|
||||||
const matchRes = match(screen.url)
|
|
||||||
if (isCurrent) {
|
|
||||||
icon = matchRes.icon
|
|
||||||
}
|
|
||||||
hasNewTab = hasNewTab || tab.isNewTab
|
|
||||||
return Object.assign(matchRes, {
|
|
||||||
key: `t${tab.id}-s${screen.index}`,
|
|
||||||
navIdx: `${tab.id}-${screen.id}`,
|
|
||||||
current: isCurrent,
|
|
||||||
previous: isPrevious,
|
|
||||||
isNewTab: tab.isNewTab,
|
|
||||||
}) as ScreenRenderDesc
|
|
||||||
})
|
|
||||||
screens = screens.concat(parsedTabScreens)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
icon,
|
|
||||||
hasNewTab,
|
|
||||||
screens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outerContainer: {
|
outerContainer: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
innerContainer: {
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
screenContainer: {
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
screenMask: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
backgroundColor: '#000',
|
|
||||||
opacity: 0.6,
|
|
||||||
},
|
|
||||||
menuDrawer: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 100,
|
|
||||||
},
|
|
||||||
topBarProtector: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: 50, // will be overwritten by insets
|
|
||||||
backgroundColor: colors.white,
|
|
||||||
},
|
|
||||||
topBarProtectorDark: {
|
|
||||||
backgroundColor: colors.black,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user