Replace the navigation model with react-navigation
This commit is contained in:
@@ -1,2 +1 @@
|
||||
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})
|
||||
if (type === EventType.PRESS) {
|
||||
store.log.debug('User pressed a notifee, opening notifications')
|
||||
store.nav.switchTo(TabPurpose.Notifs, true)
|
||||
// TODO
|
||||
// store.nav.switchTo(TabPurpose.Notifs, true)
|
||||
}
|
||||
})
|
||||
notifee.onBackgroundEvent(async _e => {}) // notifee requires this but we handle it with onForegroundEvent
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import {State} from './types'
|
||||
import {State, NavigationProp} from './types'
|
||||
|
||||
// TODO needed?
|
||||
// export function getCurrentTabName(
|
||||
// navigator: NavigationProp | undefined,
|
||||
// ): string {
|
||||
// if (!navigator) {
|
||||
// throw new Error('Failed to get current tab')
|
||||
// }
|
||||
// const state = navigator.getState()
|
||||
// if (state.type !== 'tab') {
|
||||
// return getCurrentTabName(navigator.getParent())
|
||||
// }
|
||||
// return state.routes[state.index].name
|
||||
// }
|
||||
|
||||
export function getCurrentRoute(state: State) {
|
||||
let node = state.routes[state.index]
|
||||
@@ -15,7 +29,7 @@ export function isTab(current: string, route: string) {
|
||||
// -prf
|
||||
return (
|
||||
current === route ||
|
||||
current === `${route}Stack` ||
|
||||
current === `${route}Tab` ||
|
||||
current === `${route}Inner`
|
||||
)
|
||||
}
|
||||
|
||||
+20
-3
@@ -1,4 +1,5 @@
|
||||
import {NavigationState, PartialState} from '@react-navigation/native'
|
||||
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||
|
||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
@@ -13,15 +14,31 @@ export type CommonNavigatorParams = {
|
||||
Debug: undefined
|
||||
Log: undefined
|
||||
}
|
||||
export type HomeStackNavigatorParams = CommonNavigatorParams & {
|
||||
|
||||
export type HomeTabNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
}
|
||||
export type NotificationsStackNavigatorParams = CommonNavigatorParams & {
|
||||
|
||||
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||
Notifications: undefined
|
||||
}
|
||||
export type SearchStackNavigatorParams = CommonNavigatorParams & {
|
||||
|
||||
export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
||||
Search: undefined
|
||||
}
|
||||
|
||||
// NOTE
|
||||
// this isn't strictly correct but it should be close enough
|
||||
// a TS wizard might be able to get this 100%
|
||||
// -prf
|
||||
export type NavigationProp = NativeStackNavigationProp<
|
||||
CommonNavigatorParams & {
|
||||
HomeTab: undefined
|
||||
NotificationsTab: undefined
|
||||
SearchTab: undefined
|
||||
}
|
||||
>
|
||||
|
||||
export type State =
|
||||
| NavigationState
|
||||
| Omit<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 {LogModel} from './log'
|
||||
import {SessionModel} from './session'
|
||||
import {NavigationModel} from './navigation'
|
||||
import {ShellUiModel} from './shell-ui'
|
||||
import {ProfilesViewModel} from './profiles-view'
|
||||
import {LinkMetasViewModel} from './link-metas-view'
|
||||
@@ -31,7 +30,6 @@ export class RootStoreModel {
|
||||
appInfo?: AppInfo
|
||||
log = new LogModel()
|
||||
session = new SessionModel(this)
|
||||
nav = new NavigationModel(this)
|
||||
shell = new ShellUiModel(this)
|
||||
me = new MeModel(this)
|
||||
profiles = new ProfilesViewModel(this)
|
||||
@@ -82,7 +80,6 @@ export class RootStoreModel {
|
||||
log: this.log.serialize(),
|
||||
session: this.session.serialize(),
|
||||
me: this.me.serialize(),
|
||||
nav: this.nav.serialize(),
|
||||
shell: this.shell.serialize(),
|
||||
}
|
||||
}
|
||||
@@ -101,9 +98,6 @@ export class RootStoreModel {
|
||||
if (hasProp(v, 'me')) {
|
||||
this.me.hydrate(v.me)
|
||||
}
|
||||
if (hasProp(v, 'nav')) {
|
||||
this.nav.hydrate(v.nav)
|
||||
}
|
||||
if (hasProp(v, 'session')) {
|
||||
this.session.hydrate(v.session)
|
||||
}
|
||||
@@ -144,7 +138,7 @@ export class RootStoreModel {
|
||||
*/
|
||||
async handleSessionDrop() {
|
||||
this.log.debug('RootStoreModel:handleSessionDrop')
|
||||
this.nav.clear()
|
||||
// this.nav.clear() TODO
|
||||
this.me.clear()
|
||||
this.emitSessionDropped()
|
||||
}
|
||||
@@ -155,7 +149,7 @@ export class RootStoreModel {
|
||||
clearAllSessionState() {
|
||||
this.log.debug('RootStoreModel:clearAllSessionState')
|
||||
this.session.clear()
|
||||
this.nav.clear()
|
||||
// this.nav.clear() TODO
|
||||
this.me.clear()
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export function Component({}: {}) {
|
||||
token: confirmCode,
|
||||
})
|
||||
Toast.show('Your account has been deleted')
|
||||
store.nav.tab.fixedTabReset()
|
||||
// store.nav.tab.fixedTabReset() TODO
|
||||
store.session.clear()
|
||||
store.shell.closeModal()
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
StyleSheet,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||
import {CenteredView, FlatList} from '../util/Views'
|
||||
@@ -18,10 +19,10 @@ import {FeedModel} from 'state/models/feed-view'
|
||||
import {FeedItem} from './FeedItem'
|
||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||
import {s} from 'lib/styles'
|
||||
import {useStores} from 'state/index'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {MagnifyingGlassIcon} from 'lib/icons'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||
const ERROR_FEED_ITEM = {_reactKey: '__error__'}
|
||||
@@ -47,9 +48,9 @@ export const Feed = observer(function Feed({
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const store = useStores()
|
||||
const {track} = useAnalytics()
|
||||
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
let feedItems: any[] = []
|
||||
@@ -112,7 +113,12 @@ export const Feed = observer(function Feed({
|
||||
<Button
|
||||
type="inverted"
|
||||
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}>
|
||||
Find accounts
|
||||
</Text>
|
||||
@@ -134,7 +140,7 @@ export const Feed = observer(function Feed({
|
||||
}
|
||||
return <FeedItem item={item} showFollowBtn={showPostFollowBtn} />
|
||||
},
|
||||
[feed, onPressTryAgain, showPostFollowBtn, pal, palInverted, store.nav],
|
||||
[feed, onPressTryAgain, showPostFollowBtn, pal, palInverted, navigation],
|
||||
)
|
||||
|
||||
const FeedFooter = React.useCallback(
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {BlurView} from '../util/BlurView'
|
||||
import {ProfileViewModel} from 'state/models/profile-view'
|
||||
import {useStores} from 'state/index'
|
||||
@@ -28,6 +29,7 @@ import {UserAvatar} from '../util/UserAvatar'
|
||||
import {UserBanner} from '../util/UserBanner'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
||||
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 store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const onPressBack = () => {
|
||||
store.nav.tab.goBack()
|
||||
}
|
||||
const onPressAvi = () => {
|
||||
const onPressBack = React.useCallback(() => {
|
||||
navigation.goBack()
|
||||
}, [navigation])
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
if (view.avatar) {
|
||||
store.shell.openLightbox(new ProfileImageLightbox(view))
|
||||
}
|
||||
}
|
||||
const onPressToggleFollow = () => {
|
||||
}, [store, view])
|
||||
const onPressToggleFollow = React.useCallback(() => {
|
||||
view?.toggleFollowing().then(
|
||||
() => {
|
||||
Toast.show(
|
||||
@@ -60,28 +63,28 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||
},
|
||||
err => store.log.error('Failed to toggle follow', err),
|
||||
)
|
||||
}
|
||||
const onPressEditProfile = () => {
|
||||
}, [view, store])
|
||||
const onPressEditProfile = React.useCallback(() => {
|
||||
track('ProfileHeader:EditProfileButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'edit-profile',
|
||||
profileView: view,
|
||||
onUpdate: onRefreshAll,
|
||||
})
|
||||
}
|
||||
const onPressFollowers = () => {
|
||||
}, [track, store, view, onRefreshAll])
|
||||
const onPressFollowers = React.useCallback(() => {
|
||||
track('ProfileHeader:FollowersButtonClicked')
|
||||
store.nav.navigate(`/profile/${view.handle}/followers`)
|
||||
}
|
||||
const onPressFollows = () => {
|
||||
navigation.push('ProfileFollowers', {name: view.handle})
|
||||
}, [track, navigation, view])
|
||||
const onPressFollows = React.useCallback(() => {
|
||||
track('ProfileHeader:FollowsButtonClicked')
|
||||
store.nav.navigate(`/profile/${view.handle}/follows`)
|
||||
}
|
||||
const onPressShare = () => {
|
||||
navigation.push('ProfileFollows', {name: view.handle})
|
||||
}, [track, navigation, view])
|
||||
const onPressShare = React.useCallback(() => {
|
||||
track('ProfileHeader:ShareButtonClicked')
|
||||
Share.share({url: toShareUrl(`/profile/${view.handle}`)})
|
||||
}
|
||||
const onPressMuteAccount = async () => {
|
||||
}, [track, view])
|
||||
const onPressMuteAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:MuteAccountButtonClicked')
|
||||
try {
|
||||
await view.muteAccount()
|
||||
@@ -90,8 +93,8 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||
store.log.error('Failed to mute account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
}
|
||||
const onPressUnmuteAccount = async () => {
|
||||
}, [track, view, store])
|
||||
const onPressUnmuteAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:UnmuteAccountButtonClicked')
|
||||
try {
|
||||
await view.unmuteAccount()
|
||||
@@ -100,14 +103,14 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||
store.log.error('Failed to unmute account', e)
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
}
|
||||
const onPressReportAccount = () => {
|
||||
}, [track, view, store])
|
||||
const onPressReportAccount = React.useCallback(() => {
|
||||
track('ProfileHeader:ReportAccountButtonClicked')
|
||||
store.shell.openModal({
|
||||
name: 'report-account',
|
||||
did: view.did,
|
||||
})
|
||||
}
|
||||
}, [track, store, view])
|
||||
|
||||
// loading
|
||||
// =
|
||||
|
||||
+77
-37
@@ -2,6 +2,8 @@ import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
Linking,
|
||||
GestureResponderEvent,
|
||||
Platform,
|
||||
StyleProp,
|
||||
TouchableWithoutFeedback,
|
||||
TouchableOpacity,
|
||||
@@ -9,11 +11,18 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {useLinkProps, useNavigation} from '@react-navigation/native'
|
||||
import {Text} from './text/Text'
|
||||
import {TypographyVariant} from 'lib/ThemeContext'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {matchPath} from 'view/screens'
|
||||
import {useStores, RootStoreModel} from 'state/index'
|
||||
import {convertBskyAppUrlIfNeeded} from 'lib/strings/url-helpers'
|
||||
|
||||
type Event =
|
||||
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||
| GestureResponderEvent
|
||||
|
||||
export const Link = observer(function Link({
|
||||
style,
|
||||
href,
|
||||
@@ -27,35 +36,30 @@ export const Link = observer(function Link({
|
||||
children?: React.ReactNode
|
||||
noFeedback?: boolean
|
||||
}) {
|
||||
let {...props} = useLinkProps({to: href})
|
||||
const store = useStores()
|
||||
const onPress = () => {
|
||||
if (href) {
|
||||
handleLink(store, href, false)
|
||||
}
|
||||
}
|
||||
const onLongPress = () => {
|
||||
if (href) {
|
||||
handleLink(store, href, true)
|
||||
}
|
||||
}
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
props.onPress = React.useCallback(
|
||||
(e?: Event) => {
|
||||
if (typeof href === 'string') {
|
||||
return onPressInner(store, navigation, href, e)
|
||||
}
|
||||
},
|
||||
[store, navigation, href],
|
||||
)
|
||||
|
||||
if (noFeedback) {
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
delayPressIn={50}>
|
||||
<View style={style}>
|
||||
<TouchableWithoutFeedback delayPressIn={50} {...props}>
|
||||
<View style={style} {...props}>
|
||||
{children ? children : <Text>{title || 'link'}</Text>}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
delayPressIn={50}
|
||||
style={style}>
|
||||
<TouchableOpacity delayPressIn={50} style={style} {...props}>
|
||||
{children ? children : <Text>{title || 'link'}</Text>}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
@@ -72,29 +76,65 @@ export const TextLink = observer(function TextLink({
|
||||
href: string
|
||||
text: string
|
||||
}) {
|
||||
const {...props} = useLinkProps({to: href})
|
||||
const store = useStores()
|
||||
const onPress = () => {
|
||||
handleLink(store, href, false)
|
||||
}
|
||||
const onLongPress = () => {
|
||||
handleLink(store, href, true)
|
||||
}
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
props.onPress = React.useCallback(
|
||||
(e?: Event) => {
|
||||
return onPressInner(store, navigation, href, e)
|
||||
},
|
||||
[store, navigation, href],
|
||||
)
|
||||
|
||||
return (
|
||||
<Text type={type} style={style} onPress={onPress} onLongPress={onLongPress}>
|
||||
<Text type={type} style={style} {...props}>
|
||||
{text}
|
||||
</Text>
|
||||
)
|
||||
})
|
||||
|
||||
function handleLink(store: RootStoreModel, href: string, longPress: boolean) {
|
||||
href = convertBskyAppUrlIfNeeded(href)
|
||||
if (href.startsWith('http')) {
|
||||
Linking.openURL(href)
|
||||
} else if (longPress) {
|
||||
store.shell.closeModal() // close any active modals
|
||||
store.nav.newTab(href)
|
||||
} else {
|
||||
store.shell.closeModal() // close any active modals
|
||||
store.nav.navigate(href)
|
||||
// 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)
|
||||
if (href.startsWith('http')) {
|
||||
Linking.openURL(href)
|
||||
} else {
|
||||
store.shell.closeModal() // close any active modals
|
||||
|
||||
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 {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation, DrawerActions} from '@react-navigation/native'
|
||||
import {UserAvatar} from './UserAvatar'
|
||||
import {Text} from './text/Text'
|
||||
import {useStores} from 'state/index'
|
||||
@@ -9,6 +10,7 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
import {isDesktopWeb} from '../../../platform/detection'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
||||
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 store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const onPressBack = () => {
|
||||
store.nav.tab.goBack()
|
||||
}
|
||||
const onPressMenu = () => {
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
navigation.goBack()
|
||||
}, [navigation])
|
||||
|
||||
const onPressMenu = React.useCallback(() => {
|
||||
track('ViewHeader:MenuButtonClicked')
|
||||
store.shell.setMainMenuOpen(true)
|
||||
}
|
||||
navigation.dispatch(DrawerActions.openDrawer())
|
||||
}, [track, navigation])
|
||||
|
||||
if (typeof canGoBack === 'undefined') {
|
||||
canGoBack = store.nav.tab.canGoBack
|
||||
canGoBack = navigation.canGoBack()
|
||||
}
|
||||
|
||||
if (isDesktopWeb) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {Button, ButtonType} from './Button'
|
||||
import {colors} from 'lib/styles'
|
||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {useStores} from 'state/index'
|
||||
import {TABS_ENABLED} from 'lib/build-flags'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
|
||||
@@ -138,15 +137,6 @@ export function PostDropdownBtn({
|
||||
const store = useStores()
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
TABS_ENABLED
|
||||
? {
|
||||
icon: ['far', 'clone'],
|
||||
label: 'Open in new tab',
|
||||
onPress() {
|
||||
store.nav.newTab(itemHref)
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
icon: 'language',
|
||||
label: 'Translate...',
|
||||
|
||||
+48
-46
@@ -5,9 +5,9 @@ import {createDrawerNavigator} from '@react-navigation/drawer'
|
||||
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
|
||||
|
||||
import {
|
||||
HomeStackNavigatorParams,
|
||||
NotificationsStackNavigatorParams,
|
||||
SearchStackNavigatorParams,
|
||||
HomeTabNavigatorParams,
|
||||
NotificationsTabNavigatorParams,
|
||||
SearchTabNavigatorParams,
|
||||
State,
|
||||
} from 'lib/routes/types'
|
||||
|
||||
@@ -28,12 +28,12 @@ import {DebugScreen} from './screens/Debug'
|
||||
import {LogScreen} from './screens/Log'
|
||||
|
||||
const HomeDrawer = createDrawerNavigator()
|
||||
const HomeStack = createNativeStackNavigator<HomeStackNavigatorParams>()
|
||||
const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>()
|
||||
const SearchDrawer = createDrawerNavigator()
|
||||
const SearchStack = createNativeStackNavigator<SearchStackNavigatorParams>()
|
||||
const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>()
|
||||
const NotificationsDrawer = createDrawerNavigator()
|
||||
const NotificationsStack =
|
||||
createNativeStackNavigator<NotificationsStackNavigatorParams>()
|
||||
const NotificationsTab =
|
||||
createNativeStackNavigator<NotificationsTabNavigatorParams>()
|
||||
const Tab = createBottomTabNavigator()
|
||||
|
||||
type RouteParams = Record<string, string>
|
||||
@@ -82,7 +82,21 @@ const ROUTES: Record<string, Route> = {
|
||||
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'],
|
||||
|
||||
getPathFromState(state: State) {
|
||||
@@ -101,26 +115,14 @@ const LINKING = {
|
||||
},
|
||||
|
||||
getStateFromPath(path: string) {
|
||||
// match the route
|
||||
let match = 'Home' // TODO should be not found
|
||||
let params: RouteParams = {}
|
||||
for (const [name, matcher] of Object.entries(ROUTES)) {
|
||||
const res = matcher.match(path)
|
||||
if (res) {
|
||||
match = name
|
||||
params = res.params
|
||||
break
|
||||
}
|
||||
const {name, params} = matchPath(path)
|
||||
if (name === 'Search') {
|
||||
return buildStateObject('SearchTab', 'Search', params)
|
||||
}
|
||||
|
||||
// build the state object
|
||||
if (match === 'Search') {
|
||||
return buildStateObject('SearchStack', 'Search', params)
|
||||
if (name === 'Notifications') {
|
||||
return buildStateObject('NotificationsTab', 'Notifications', params)
|
||||
}
|
||||
if (match === 'Notifications') {
|
||||
return buildStateObject('NotificationsStack', 'Notifications', params)
|
||||
}
|
||||
return buildStateObject('HomeStack', match, params)
|
||||
return buildStateObject('HomeTab', name, params)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -167,17 +169,17 @@ function HomeDrawerNavigator() {
|
||||
)
|
||||
}
|
||||
|
||||
function HomeStackNavigator() {
|
||||
function HomeTabNavigator() {
|
||||
return (
|
||||
<HomeStack.Navigator
|
||||
<HomeTab.Navigator
|
||||
screenOptions={{
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
}}>
|
||||
<HomeStack.Screen name="Home" component={HomeDrawerNavigator} />
|
||||
{commonScreens(HomeStack)}
|
||||
</HomeStack.Navigator>
|
||||
<HomeTab.Screen name="Home" component={HomeDrawerNavigator} />
|
||||
{commonScreens(HomeTab)}
|
||||
</HomeTab.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -195,20 +197,20 @@ function NotificationsDrawerNavigator() {
|
||||
)
|
||||
}
|
||||
|
||||
function NotificationsStackNavigator() {
|
||||
function NotificationsTabNavigator() {
|
||||
return (
|
||||
<NotificationsStack.Navigator
|
||||
<NotificationsTab.Navigator
|
||||
screenOptions={{
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
}}>
|
||||
<NotificationsStack.Screen
|
||||
<NotificationsTab.Screen
|
||||
name="Notifications"
|
||||
component={NotificationsDrawerNavigator}
|
||||
/>
|
||||
{commonScreens(NotificationsStack)}
|
||||
</NotificationsStack.Navigator>
|
||||
{commonScreens(NotificationsTab)}
|
||||
</NotificationsTab.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -223,17 +225,17 @@ function SearchDrawerNavigator() {
|
||||
)
|
||||
}
|
||||
|
||||
function SearchStackNavigator() {
|
||||
function SearchTabNavigator() {
|
||||
return (
|
||||
<SearchStack.Navigator
|
||||
<SearchTab.Navigator
|
||||
screenOptions={{
|
||||
gestureEnabled: true,
|
||||
fullScreenGestureEnabled: true,
|
||||
headerShown: false,
|
||||
}}>
|
||||
<SearchStack.Screen name="Search" component={SearchDrawerNavigator} />
|
||||
{commonScreens(SearchStack)}
|
||||
</SearchStack.Navigator>
|
||||
<SearchTab.Screen name="Search" component={SearchDrawerNavigator} />
|
||||
{commonScreens(SearchTab)}
|
||||
</SearchTab.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -241,16 +243,16 @@ function TabsNavigator() {
|
||||
const tabBar = React.useCallback(props => <BottomBar {...props} />, [])
|
||||
return (
|
||||
<Tab.Navigator
|
||||
initialRouteName="HomeStack"
|
||||
initialRouteName="HomeTab"
|
||||
backBehavior="initialRoute"
|
||||
screenOptions={{headerShown: false}}
|
||||
tabBar={tabBar}>
|
||||
<Tab.Screen name="HomeStack" component={HomeStackNavigator} />
|
||||
<Tab.Screen name="HomeTab" component={HomeTabNavigator} />
|
||||
<Tab.Screen
|
||||
name="NotificationsStack"
|
||||
component={NotificationsStackNavigator}
|
||||
name="NotificationsTab"
|
||||
component={NotificationsTabNavigator}
|
||||
/>
|
||||
<Tab.Screen name="SearchStack" component={SearchStackNavigator} />
|
||||
<Tab.Screen name="SearchTab" component={SearchTabNavigator} />
|
||||
</Tab.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import {FlatList, View} from 'react-native'
|
||||
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import useAppState from 'react-native-appstate-hook'
|
||||
import {
|
||||
NativeStackScreenProps,
|
||||
HomeStackNavigatorParams,
|
||||
} from 'lib/routes/types'
|
||||
import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {Feed} from '../com/posts/Feed'
|
||||
import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
|
||||
@@ -20,7 +17,7 @@ import {ComposeIcon2} from 'lib/icons'
|
||||
|
||||
const HEADER_HEIGHT = 42
|
||||
|
||||
type Props = NativeStackScreenProps<HomeStackNavigatorParams, 'Home'>
|
||||
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
||||
export const HomeScreen = observer(function Home({}: Props) {
|
||||
const store = useStores()
|
||||
const onMainScroll = useOnMainScroll(store)
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import React from 'react'
|
||||
import {Button, StyleSheet, View} from 'react-native'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import {useStores} from 'state/index'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
||||
export const NotFound = () => {
|
||||
const stores = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onPressHome = React.useCallback(() => {
|
||||
navigation.navigate('HomeTab') // TODO go fully home
|
||||
}, [navigation])
|
||||
|
||||
return (
|
||||
<View testID="notFoundView">
|
||||
<ViewHeader title="Page not found" />
|
||||
@@ -14,7 +20,7 @@ export const NotFound = () => {
|
||||
<Button
|
||||
testID="navigateHomeButton"
|
||||
title="Home"
|
||||
onPress={() => stores.nav.navigate('/')}
|
||||
onPress={onPressHome}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {useFocusEffect} from '@react-navigation/native'
|
||||
import useAppState from 'react-native-appstate-hook'
|
||||
import {
|
||||
NativeStackScreenProps,
|
||||
NotificationsStackNavigatorParams,
|
||||
NotificationsTabNavigatorParams,
|
||||
} from 'lib/routes/types'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {Feed} from '../com/notifications/Feed'
|
||||
@@ -16,7 +16,7 @@ import {useAnalytics} from 'lib/analytics'
|
||||
const NOTIFICATIONS_POLL_INTERVAL = 15e3
|
||||
|
||||
export const NotificationsScreen = ({}: NativeStackScreenProps<
|
||||
NotificationsStackNavigatorParams,
|
||||
NotificationsTabNavigatorParams,
|
||||
'Notifications'
|
||||
>) => {
|
||||
const store = useStores()
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import {ScrollView} from '../com/util/Views'
|
||||
import {
|
||||
NativeStackScreenProps,
|
||||
SearchStackNavigatorParams,
|
||||
SearchTabNavigatorParams,
|
||||
} from 'lib/routes/types'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
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 FIVE_MIN = 5 * 60 * 1e3
|
||||
|
||||
type Props = NativeStackScreenProps<SearchStackNavigatorParams, 'Search'>
|
||||
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
|
||||
export const SearchScreen = observer(({}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
|
||||
@@ -5,7 +5,7 @@ import {ScrollView} from '../com/util/Views'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
NativeStackScreenProps,
|
||||
SearchStackNavigatorParams,
|
||||
SearchTabNavigatorParams,
|
||||
} from 'lib/routes/types'
|
||||
import {useStores} from 'state/index'
|
||||
import {s} from 'lib/styles'
|
||||
@@ -16,7 +16,7 @@ import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
||||
|
||||
const FIVE_MIN = 5 * 60 * 1e3
|
||||
|
||||
type Props = NativeStackScreenProps<SearchStackNavigatorParams, 'Search'>
|
||||
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
|
||||
export const SearchScreen = observer(({}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
StackActions,
|
||||
} from '@react-navigation/native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
@@ -26,6 +30,7 @@ import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {AccountData} from 'state/models/session'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
||||
export const SettingsScreen = observer(
|
||||
function Settings({}: NativeStackScreenProps<
|
||||
@@ -35,6 +40,7 @@ export const SettingsScreen = observer(
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {screen, track} = useAnalytics()
|
||||
const [isSwitching, setIsSwitching] = React.useState(false)
|
||||
|
||||
@@ -50,13 +56,15 @@ export const SettingsScreen = observer(
|
||||
setIsSwitching(true)
|
||||
if (await store.session.resumeSession(acct)) {
|
||||
setIsSwitching(false)
|
||||
store.nav.tab.fixedTabReset()
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
||||
return
|
||||
}
|
||||
setIsSwitching(false)
|
||||
Toast.show('Sorry! We need you to enter your password.')
|
||||
store.nav.tab.fixedTabReset()
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
store.session.clear()
|
||||
}
|
||||
const onPressAddAccount = () => {
|
||||
|
||||
@@ -66,7 +66,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
||||
} else if (isTab(state.routes[state.index].name, tab)) {
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
} else {
|
||||
navigation.navigate(`${tab}Stack`)
|
||||
navigation.navigate(`${tab}Tab`)
|
||||
}
|
||||
},
|
||||
[store, track, navigation],
|
||||
|
||||
@@ -68,7 +68,7 @@ export const Drawer = observer(({navigation}: DrawerContentComponentProps) => {
|
||||
} else {
|
||||
// wait for drawer anim to finish
|
||||
setTimeout(() => {
|
||||
navigation.navigate(`${tab}Stack`)
|
||||
navigation.navigate(`${tab}Tab`)
|
||||
}, 250)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
import React, {useState} from 'react'
|
||||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
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 {StatusBar, StyleSheet, useWindowDimensions, View} from 'react-native'
|
||||
import {useStores} from 'state/index'
|
||||
import {NavigationModel} from 'state/models/navigation'
|
||||
import {match, MatchResult} from '../../routes'
|
||||
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 {Lightbox} from '../../com/lightbox/Lightbox'
|
||||
import {Text} from '../../com/util/text/Text'
|
||||
import {ErrorBoundary} from '../../com/util/ErrorBoundary'
|
||||
import {Composer} from './Composer'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||
import {s} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
||||
@@ -35,81 +18,6 @@ export const MobileShell: React.FC = observer(() => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
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) {
|
||||
return (
|
||||
@@ -158,9 +66,6 @@ export const MobileShell: React.FC = observer(() => {
|
||||
)
|
||||
}
|
||||
|
||||
const screenBg = {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? colors.black : colors.gray1,
|
||||
}
|
||||
return (
|
||||
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
||||
<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({
|
||||
outerContainer: {
|
||||
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