Move to expo and react-navigation (#288)
* WIP - adding expo * WIP - adding expo 2 * Fix tsc * Finish adding expo * Disable the 'require cycle' warning * Tweak plist * Modify some dependency versions to make expo happy * Fix icon fill * Get Web compiling for expo * 1.7 * Switch to react-navigation in expo2 (#287) * WIP Switch to react-navigation * WIP Switch to react-navigation 2 * WIP Switch to react-navigation 3 * Convert all screens to react navigation * Update BottomBar for react navigation * Update mobile menu to be react-native drawer * Fixes to drawer and bottombar * Factor out some helpers * Replace the navigation model with react-navigation * Restructure the shell folder and fix the header positioning * Restore the error boundary * Fix tsc * Implement not-found page * Remove react-native-gesture-handler (no longer used) * Handle notifee card presses * Handle all navigations from the state layer * Fix drawer behaviors * Fix two linking issues * Switch to our react-native-progress fork to fix an svg rendering issue * Get Web working with react-navigation * Refactor routes and navigation for a bit more clarity * Remove dead code * Rework Web shell to left/right nav to make this easier * Fix ViewHeader for desktop web * Hide profileheader back btn on desktop web * Move the compose button to the left nav * Implement reply prompt in threads for desktop web * Composer refactors * Factor out all platform-specific text input behaviors from the composer * Small fix * Update the web build to use tiptap for the composer * Tune up the mention autocomplete dropdown * Simplify the default avatar and banner * Fixes to link cards in web composer * Fix dropdowns on web * Tweak load latest on desktop * Add web beta message and feedback link * Fix up links in desktop web
This commit is contained in:
parent
503e03d91e
commit
56cf890deb
222 changed files with 8705 additions and 6338 deletions
|
@ -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,12 +11,12 @@ 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'
|
||||
import {NotificationsViewItemModel} from './notifications-view'
|
||||
import {MeModel} from './me'
|
||||
import {resetToTab} from '../../Navigation'
|
||||
|
||||
export const appInfo = z.object({
|
||||
build: z.string(),
|
||||
|
@ -31,7 +31,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 +81,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 +99,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 +139,7 @@ export class RootStoreModel {
|
|||
*/
|
||||
async handleSessionDrop() {
|
||||
this.log.debug('RootStoreModel:handleSessionDrop')
|
||||
this.nav.clear()
|
||||
resetToTab('HomeTab')
|
||||
this.me.clear()
|
||||
this.emitSessionDropped()
|
||||
}
|
||||
|
@ -155,7 +150,7 @@ export class RootStoreModel {
|
|||
clearAllSessionState() {
|
||||
this.log.debug('RootStoreModel:clearAllSessionState')
|
||||
this.session.clear()
|
||||
this.nav.clear()
|
||||
resetToTab('HomeTab')
|
||||
this.me.clear()
|
||||
}
|
||||
|
||||
|
@ -203,6 +198,7 @@ export class RootStoreModel {
|
|||
}
|
||||
|
||||
// the current screen has changed
|
||||
// TODO is this still needed?
|
||||
onNavigation(handler: () => void): EmitterSubscription {
|
||||
return DeviceEventEmitter.addListener('navigation', handler)
|
||||
}
|
||||
|
|
|
@ -108,7 +108,6 @@ export interface ComposerOptsQuote {
|
|||
}
|
||||
}
|
||||
export interface ComposerOpts {
|
||||
imagesOpen?: boolean
|
||||
replyTo?: ComposerOptsPostRef
|
||||
onPost?: () => void
|
||||
quote?: ComposerOptsQuote
|
||||
|
@ -117,7 +116,7 @@ export interface ComposerOpts {
|
|||
export class ShellUiModel {
|
||||
darkMode = false
|
||||
minimalShellMode = false
|
||||
isMainMenuOpen = false
|
||||
isDrawerOpen = false
|
||||
isModalActive = false
|
||||
activeModals: Modal[] = []
|
||||
isLightboxActive = false
|
||||
|
@ -156,8 +155,12 @@ export class ShellUiModel {
|
|||
this.minimalShellMode = v
|
||||
}
|
||||
|
||||
setMainMenuOpen(v: boolean) {
|
||||
this.isMainMenuOpen = v
|
||||
openDrawer() {
|
||||
this.isDrawerOpen = true
|
||||
}
|
||||
|
||||
closeDrawer() {
|
||||
this.isDrawerOpen = false
|
||||
}
|
||||
|
||||
openModal(modal: Modal) {
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import {PhotoIdentifier} from './../../../node_modules/@react-native-camera-roll/camera-roll/src/CameraRoll'
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {CameraRoll} from '@react-native-camera-roll/camera-roll'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export type {PhotoIdentifier} from './../../../node_modules/@react-native-camera-roll/camera-roll/src/CameraRoll'
|
||||
|
||||
export class UserLocalPhotosModel {
|
||||
// state
|
||||
photos: PhotoIdentifier[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this, {
|
||||
rootStore: false,
|
||||
})
|
||||
}
|
||||
|
||||
async setup() {
|
||||
const r = await CameraRoll.getPhotos({first: 20})
|
||||
runInAction(() => {
|
||||
this.photos = r.edges
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue