chore: app group optimizations (#1027)

This commit is contained in:
Mo
2022-05-16 21:14:18 -05:00
committed by GitHub
parent 754a189532
commit 62cf34e894
108 changed files with 1796 additions and 1187 deletions

View File

@@ -0,0 +1,23 @@
import { DeinitSource } from '@standardnotes/snjs'
import { WebApplication } from '../Application'
export function isStateDealloced(state: AbstractState): boolean {
return state.dealloced == undefined || state.dealloced === true
}
export abstract class AbstractState {
application: WebApplication
appState?: AbstractState
dealloced = false
constructor(application: WebApplication, appState?: AbstractState) {
this.application = application
this.appState = appState
}
deinit(_source: DeinitSource): void {
this.dealloced = true
;(this.application as unknown) = undefined
;(this.appState as unknown) = undefined
}
}

View File

@@ -1,8 +1,9 @@
import { isDev } from '@/Utils'
import { destroyAllObjectProperties, isDev } from '@/Utils'
import { action, computed, makeObservable, observable, runInAction } from 'mobx'
import { ApplicationEvent, ContentType, SNNote, SNTag } from '@standardnotes/snjs'
import { ApplicationEvent, ContentType, DeinitSource, SNNote, SNTag } from '@standardnotes/snjs'
import { WebApplication } from '@/UIModels/Application'
import { AccountMenuPane } from '@/Components/AccountMenu'
import { AbstractState } from './AbstractState'
type StructuredItemsCount = {
notes: number
@@ -11,7 +12,7 @@ type StructuredItemsCount = {
archived: number
}
export class AccountMenuState {
export class AccountMenuState extends AbstractState {
show = false
signingOut = false
otherSessionsSignOut = false
@@ -26,7 +27,15 @@ export class AccountMenuState {
shouldAnimateCloseMenu = false
currentPane = AccountMenuPane.GeneralMenu
constructor(private application: WebApplication, private appEventListeners: (() => void)[]) {
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.notesAndTags as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, private appEventListeners: (() => void)[]) {
super(application)
makeObservable(this, {
show: observable,
signingOut: observable,

View File

@@ -1,7 +1,7 @@
import { storage, StorageKey } from '@/Services/LocalStorage'
import { WebApplication, WebAppEvent } from '@/UIModels/Application'
import { AccountMenuState } from '@/UIModels/AppState/AccountMenuState'
import { isDesktopApplication } from '@/Utils'
import { destroyAllObjectProperties, isDesktopApplication } from '@/Utils'
import {
ApplicationEvent,
ContentType,
@@ -33,6 +33,7 @@ import { SubscriptionState } from './SubscriptionState'
import { SyncState } from './SyncState'
import { TagsState } from './TagsState'
import { FilePreviewModalState } from './FilePreviewModalState'
import { AbstractState } from './AbstractState'
export enum AppStateEvent {
TagChanged,
@@ -57,35 +58,34 @@ export enum EventSource {
type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void>
export class AppState {
export class AppState extends AbstractState {
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
application: WebApplication
observers: ObserverCallback[] = []
locked = true
unsubApp: any
unsubAppEventObserver!: () => void
webAppEventDisposer?: () => void
onVisibilityChange: any
onVisibilityChange: () => void
showBetaWarning: boolean
private multiEditorSupport = false
readonly quickSettingsMenu = new QuickSettingsState()
readonly accountMenu: AccountMenuState
readonly actionsMenu = new ActionsMenuState()
readonly features: FeaturesState
readonly filePreviewModal = new FilePreviewModalState()
readonly files: FilesState
readonly noAccountWarning: NoAccountWarningState
readonly notes: NotesState
readonly notesView: NotesViewState
readonly noteTags: NoteTagsState
readonly preferences = new PreferencesState()
readonly purchaseFlow: PurchaseFlowState
readonly noAccountWarning: NoAccountWarningState
readonly noteTags: NoteTagsState
readonly sync = new SyncState()
readonly quickSettingsMenu = new QuickSettingsState()
readonly searchOptions: SearchOptionsState
readonly notes: NotesState
readonly features: FeaturesState
readonly tags: TagsState
readonly notesView: NotesViewState
readonly subscription: SubscriptionState
readonly files: FilesState
readonly filePreviewModal = new FilePreviewModalState()
readonly sync = new SyncState()
readonly tags: TagsState
isSessionsModalVisible = false
@@ -94,7 +94,8 @@ export class AppState {
private readonly tagChangedDisposer: IReactionDisposer
constructor(application: WebApplication, private device: WebOrDesktopDeviceInterface) {
this.application = application
super(application)
this.notes = new NotesState(
application,
this,
@@ -103,6 +104,7 @@ export class AppState {
},
this.appEventObserverRemovers,
)
this.noteTags = new NoteTagsState(application, this, this.appEventObserverRemovers)
this.features = new FeaturesState(application, this.appEventObserverRemovers)
this.tags = new TagsState(application, this.appEventObserverRemovers, this.features)
@@ -144,41 +146,72 @@ export class AppState {
this.tagChangedDisposer = this.tagChangedNotifier()
}
deinit(source: DeinitSource): void {
override deinit(source: DeinitSource): void {
super.deinit(source)
if (source === DeinitSource.SignOut) {
storage.remove(StorageKey.ShowBetaWarning)
this.noAccountWarning.reset()
}
;(this.application as unknown) = undefined
this.actionsMenu.reset()
this.unsubApp?.()
this.unsubApp = undefined
this.unsubAppEventObserver?.()
;(this.unsubAppEventObserver as unknown) = undefined
this.observers.length = 0
this.appEventObserverRemovers.forEach((remover) => remover())
this.appEventObserverRemovers.length = 0
;(this.features as unknown) = undefined
;(this.device as unknown) = undefined
this.webAppEventDisposer?.()
this.webAppEventDisposer = undefined
;(this.quickSettingsMenu as unknown) = undefined
;(this.accountMenu as unknown) = undefined
;(this.actionsMenu as unknown) = undefined
;(this.filePreviewModal as unknown) = undefined
;(this.preferences as unknown) = undefined
;(this.purchaseFlow as unknown) = undefined
;(this.noteTags as unknown) = undefined
;(this.quickSettingsMenu as unknown) = undefined
;(this.sync as unknown) = undefined
;(this.searchOptions as unknown) = undefined
;(this.notes as unknown) = undefined
this.actionsMenu.reset()
;(this.actionsMenu as unknown) = undefined
this.features.deinit(source)
;(this.features as unknown) = undefined
;(this.tags as unknown) = undefined
this.accountMenu.deinit(source)
;(this.accountMenu as unknown) = undefined
this.files.deinit(source)
;(this.files as unknown) = undefined
this.noAccountWarning.deinit(source)
;(this.noAccountWarning as unknown) = undefined
this.notes.deinit(source)
;(this.notes as unknown) = undefined
this.notesView.deinit(source)
;(this.notesView as unknown) = undefined
this.noteTags.deinit(source)
;(this.noteTags as unknown) = undefined
this.purchaseFlow.deinit(source)
;(this.purchaseFlow as unknown) = undefined
this.searchOptions.deinit(source)
;(this.searchOptions as unknown) = undefined
this.subscription.deinit(source)
;(this.subscription as unknown) = undefined
this.tags.deinit(source)
;(this.tags as unknown) = undefined
document.removeEventListener('visibilitychange', this.onVisibilityChange)
this.onVisibilityChange = undefined
;(this.onVisibilityChange as unknown) = undefined
this.tagChangedDisposer()
;(this.tagChangedDisposer as unknown) = undefined
destroyAllObjectProperties(this)
}
openSessionsModal(): void {
@@ -333,7 +366,7 @@ export class AppState {
}
addAppEventObserver() {
this.unsubApp = this.application.addEventObserver(async (eventName) => {
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
switch (eventName) {
case ApplicationEvent.Started:
this.locked = true
@@ -370,11 +403,12 @@ export class AppState {
}
}
/** @returns A function that unregisters this observer */
addObserver(callback: ObserverCallback) {
addObserver(callback: ObserverCallback): () => void {
this.observers.push(callback)
const thislessObservers = this.observers
return () => {
removeFromArray(this.observers, callback)
removeFromArray(thislessObservers, callback)
}
}

View File

@@ -1,14 +1,30 @@
import { WebApplication } from '@/UIModels/Application'
import { ApplicationEvent, FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { destroyAllObjectProperties } from '@/Utils'
import { ApplicationEvent, DeinitSource, FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { action, makeObservable, observable, runInAction, when } from 'mobx'
import { AbstractState } from './AbstractState'
export class FeaturesState {
export class FeaturesState extends AbstractState {
hasFolders: boolean
hasSmartViews: boolean
hasFiles: boolean
premiumAlertFeatureName: string | undefined
constructor(private application: WebApplication, appObservers: (() => void)[]) {
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.showPremiumAlert as unknown) = undefined
;(this.closePremiumAlert as unknown) = undefined
;(this.hasFolders as unknown) = undefined
;(this.hasSmartViews as unknown) = undefined
;(this.hasFiles as unknown) = undefined
;(this.premiumAlertFeatureName as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
this.hasFolders = this.isEntitledToFolders()
this.hasSmartViews = this.isEntitledToSmartViews()
this.hasFiles = this.isEntitledToFiles()

View File

@@ -1,10 +1,10 @@
import { SNFile } from '@standardnotes/snjs/dist/@types'
import { FileItem } from '@standardnotes/snjs'
import { action, makeObservable, observable } from 'mobx'
export class FilePreviewModalState {
isOpen = false
currentFile: SNFile | undefined = undefined
otherFiles: SNFile[] = []
currentFile: FileItem | undefined = undefined
otherFiles: FileItem[] = []
constructor() {
makeObservable(this, {
@@ -18,11 +18,11 @@ export class FilePreviewModalState {
})
}
setCurrentFile = (currentFile: SNFile) => {
setCurrentFile = (currentFile: FileItem) => {
this.currentFile = currentFile
}
activate = (currentFile: SNFile, otherFiles: SNFile[]) => {
activate = (currentFile: FileItem, otherFiles: FileItem[]) => {
this.currentFile = currentFile
this.otherFiles = otherFiles
this.isOpen = true

View File

@@ -7,14 +7,12 @@ import {
ClassicFileSaver,
parseFileName,
} from '@standardnotes/filepicker'
import { ClientDisplayableError, SNFile } from '@standardnotes/snjs'
import { ClientDisplayableError, FileItem } from '@standardnotes/snjs'
import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/stylekit'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export class FilesState {
constructor(private application: WebApplication) {}
public async downloadFile(file: SNFile): Promise<void> {
export class FilesState extends AbstractState {
public async downloadFile(file: FileItem): Promise<void> {
let downloadingToastId = ''
try {
@@ -102,7 +100,7 @@ export class FilesState {
return
}
const uploadedFiles: SNFile[] = []
const uploadedFiles: FileItem[] = []
for (const file of selectedFiles) {
if (!shouldUseStreamingReader && maxFileSize && file.size >= maxFileSize) {

View File

@@ -1,10 +1,15 @@
import { storage, StorageKey } from '@/Services/LocalStorage'
import { SNApplication, ApplicationEvent } from '@standardnotes/snjs'
import { ApplicationEvent } from '@standardnotes/snjs'
import { runInAction, makeObservable, observable, action } from 'mobx'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export class NoAccountWarningState {
export class NoAccountWarningState extends AbstractState {
show: boolean
constructor(application: SNApplication, appObservers: (() => void)[]) {
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
this.show = application.hasAccount() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true
appObservers.push(

View File

@@ -1,10 +1,12 @@
import { ElementIds } from '@/ElementIDs'
import { ApplicationEvent, ContentType, PrefKey, SNNote, SNTag, UuidString } from '@standardnotes/snjs'
import { destroyAllObjectProperties } from '@/Utils'
import { ApplicationEvent, ContentType, DeinitSource, PrefKey, SNNote, SNTag, UuidString } from '@standardnotes/snjs'
import { action, computed, makeObservable, observable } from 'mobx'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
import { AppState } from './AppState'
export class NoteTagsState {
export class NoteTagsState extends AbstractState {
autocompleteInputFocused = false
autocompleteSearchQuery = ''
autocompleteTagHintFocused = false
@@ -15,7 +17,17 @@ export class NoteTagsState {
tagsContainerMaxWidth: number | 'auto' = 0
addNoteToParentFolders: boolean
constructor(private application: WebApplication, private appState: AppState, appEventListeners: (() => void)[]) {
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.tags as unknown) = undefined
;(this.autocompleteTagResults as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, override appState: AppState, appEventListeners: (() => void)[]) {
super(application, appState)
makeObservable(this, {
autocompleteInputFocused: observable,
autocompleteSearchQuery: observable,

View File

@@ -1,3 +1,4 @@
import { destroyAllObjectProperties } from '@/Utils'
import { confirmDialog } from '@/Services/AlertService'
import { KeyboardModifier } from '@/Services/IOService'
import { StringEmptyTrash, Strings, StringUtils } from '@/Strings'
@@ -10,12 +11,14 @@ import {
SNTag,
ChallengeReason,
NoteViewController,
DeinitSource,
} from '@standardnotes/snjs'
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
import { WebApplication } from '../Application'
import { AppState } from './AppState'
import { AbstractState } from './AbstractState'
export class NotesState {
export class NotesState extends AbstractState {
lastSelectedNote: SNNote | undefined
selectedNotes: Record<UuidString, SNNote> = {}
contextMenuOpen = false
@@ -28,12 +31,23 @@ export class NotesState {
showProtectedWarning = false
showRevisionHistoryModal = false
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.lastSelectedNote as unknown) = undefined
;(this.selectedNotes as unknown) = undefined
;(this.onActiveEditorChanged as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(
private application: WebApplication,
private appState: AppState,
application: WebApplication,
public override appState: AppState,
private onActiveEditorChanged: () => Promise<void>,
appEventListeners: (() => void)[],
) {
super(application, appState)
makeObservable(this, {
selectedNotes: observable,
contextMenuOpen: observable,
@@ -75,6 +89,10 @@ export class NotesState {
}
get selectedNotesCount(): number {
if (this.dealloced) {
return 0
}
return Object.keys(this.selectedNotes).length
}

View File

@@ -1,8 +1,10 @@
import { destroyAllObjectProperties } from '@/Utils'
import {
ApplicationEvent,
CollectionSort,
CollectionSortProperty,
ContentType,
DeinitSource,
findInArray,
NotesDisplayCriteria,
PrefKey,
@@ -15,6 +17,7 @@ import {
import { action, autorun, computed, makeObservable, observable, reaction } from 'mobx'
import { AppState, AppStateEvent } from '.'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
const MIN_NOTE_CELL_HEIGHT = 51.0
const DEFAULT_LIST_NUM_NOTES = 20
@@ -34,7 +37,7 @@ export type DisplayOptions = {
hideEditorIcon: boolean
}
export class NotesViewState {
export class NotesViewState extends AbstractState {
completedFullSync = false
noteFilterText = ''
notes: SNNote[] = []
@@ -59,22 +62,34 @@ export class NotesViewState {
hideEditorIcon: false,
}
constructor(private application: WebApplication, private appState: AppState, appObservers: (() => void)[]) {
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.noteFilterText as unknown) = undefined
;(this.notes as unknown) = undefined
;(this.renderedNotes as unknown) = undefined
;(this.selectedNotes as unknown) = undefined
;(window.onresize as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, override appState: AppState, appObservers: (() => void)[]) {
super(application, appState)
this.resetPagination()
appObservers.push(
application.streamItems<SNNote>(ContentType.Note, () => {
this.reloadNotes()
const activeNote = this.appState.notes.activeNoteController?.note
const activeNote = appState.notes.activeNoteController?.note
if (this.application.getAppState().notes.selectedNotesCount < 2) {
if (appState.notes.selectedNotesCount < 2) {
if (activeNote) {
const browsingTrashedNotes =
this.appState.selectedTag instanceof SmartView &&
this.appState.selectedTag?.uuid === SystemViewId.TrashedNotes
appState.selectedTag instanceof SmartView && appState.selectedTag?.uuid === SystemViewId.TrashedNotes
if (activeNote.trashed && !browsingTrashedNotes && !this.appState?.searchOptions.includeTrashed) {
if (activeNote.trashed && !browsingTrashedNotes && !appState?.searchOptions.includeTrashed) {
this.selectNextOrCreateNew()
} else if (!this.selectedNotes[activeNote.uuid]) {
this.selectNote(activeNote).catch(console.error)
@@ -91,7 +106,7 @@ export class NotesViewState {
this.reloadNotesDisplayOptions()
this.reloadNotes()
if (this.appState.selectedTag && findInArray(tags, 'uuid', this.appState.selectedTag.uuid)) {
if (appState.selectedTag && findInArray(tags, 'uuid', appState.selectedTag.uuid)) {
/** Tag title could have changed */
this.reloadPanelTitle()
}
@@ -100,7 +115,7 @@ export class NotesViewState {
this.reloadPreferences()
}, ApplicationEvent.PreferencesChanged),
application.addEventObserver(async () => {
this.appState.closeAllNoteControllers()
appState.closeAllNoteControllers()
this.selectFirstNote()
this.setCompletedFullSync(false)
}, ApplicationEvent.SignedIn),
@@ -108,20 +123,22 @@ export class NotesViewState {
this.reloadNotes()
if (
this.notes.length === 0 &&
this.appState.selectedTag instanceof SmartView &&
this.appState.selectedTag.uuid === SystemViewId.AllNotes &&
appState.selectedTag instanceof SmartView &&
appState.selectedTag.uuid === SystemViewId.AllNotes &&
this.noteFilterText === '' &&
!this.appState.notes.activeNoteController
!appState.notes.activeNoteController
) {
this.createPlaceholderNote()?.catch(console.error)
}
this.setCompletedFullSync(true)
}, ApplicationEvent.CompletedFullSync),
autorun(() => {
if (appState.notes.selectedNotes) {
this.syncSelectedNotes()
}
}),
reaction(
() => [
appState.searchOptions.includeProtectedContents,
@@ -133,6 +150,7 @@ export class NotesViewState {
this.reloadNotes()
},
),
appState.addObserver(async (eventName) => {
if (eventName === AppStateEvent.TagChanged) {
this.handleTagChange()
@@ -414,6 +432,7 @@ export class NotesViewState {
selectPreviousNote = () => {
const displayableNotes = this.notes
if (this.activeEditorNote) {
const currentIndex = displayableNotes.indexOf(this.activeEditorNote)
if (currentIndex - 1 >= 0) {
@@ -426,11 +445,13 @@ export class NotesViewState {
return false
}
}
return undefined
}
setNoteFilterText = (text: string) => {
this.noteFilterText = text
this.handleFilterTextChanged()
}
syncSelectedNotes = () => {

View File

@@ -1,17 +1,20 @@
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { action, makeObservable, observable } from 'mobx'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export enum PurchaseFlowPane {
SignIn,
CreateAccount,
}
export class PurchaseFlowState {
export class PurchaseFlowState extends AbstractState {
isOpen = false
currentPane = PurchaseFlowPane.CreateAccount
constructor(private application: WebApplication) {
constructor(application: WebApplication) {
super(application)
makeObservable(this, {
isOpen: observable,
currentPane: observable,

View File

@@ -1,13 +1,16 @@
import { ApplicationEvent } from '@standardnotes/snjs'
import { makeObservable, observable, action, runInAction } from 'mobx'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export class SearchOptionsState {
export class SearchOptionsState extends AbstractState {
includeProtectedContents = false
includeArchived = false
includeTrashed = false
constructor(private application: WebApplication, appObservers: (() => void)[]) {
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
makeObservable(this, {
includeProtectedContents: observable,
includeTrashed: observable,

View File

@@ -1,6 +1,13 @@
import { ApplicationEvent, ClientDisplayableError, convertTimestampToMilliseconds } from '@standardnotes/snjs'
import { destroyAllObjectProperties } from '@/Utils'
import {
ApplicationEvent,
ClientDisplayableError,
convertTimestampToMilliseconds,
DeinitSource,
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable } from 'mobx'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
type Subscription = {
planName: string
@@ -14,11 +21,21 @@ type AvailableSubscriptions = {
}
}
export class SubscriptionState {
export class SubscriptionState extends AbstractState {
userSubscription: Subscription | undefined = undefined
availableSubscriptions: AvailableSubscriptions | undefined = undefined
constructor(private application: WebApplication, appObservers: (() => void)[]) {
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.userSubscription as unknown) = undefined
;(this.availableSubscriptions as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
makeObservable(this, {
userSubscription: observable,
availableSubscriptions: observable,

View File

@@ -12,10 +12,13 @@ import {
UuidString,
isSystemView,
FindItem,
DeinitSource,
} from '@standardnotes/snjs'
import { action, computed, makeAutoObservable, makeObservable, observable, runInAction } from 'mobx'
import { WebApplication } from '../Application'
import { FeaturesState } from './FeaturesState'
import { AbstractState } from './AbstractState'
import { destroyAllObjectProperties } from '@/Utils'
type AnyTag = SNTag | SmartView
@@ -56,7 +59,7 @@ const isValidFutureSiblings = (application: SNApplication, futureSiblings: SNTag
return true
}
export class TagsState {
export class TagsState extends AbstractState {
tags: SNTag[] = []
smartViews: SmartView[] = []
allNotesCount_ = 0
@@ -75,7 +78,9 @@ export class TagsState {
private readonly tagsCountsState: TagsCountsState
constructor(private application: WebApplication, appEventListeners: (() => void)[], private features: FeaturesState) {
constructor(application: WebApplication, appEventListeners: (() => void)[], private features: FeaturesState) {
super(application)
this.tagsCountsState = new TagsCountsState(this.application)
this.selected_ = undefined
@@ -164,6 +169,19 @@ export class TagsState {
)
}
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.features as unknown) = undefined
;(this.tags as unknown) = undefined
;(this.smartViews as unknown) = undefined
;(this.selected_ as unknown) = undefined
;(this.previouslySelected_ as unknown) = undefined
;(this.editing_ as unknown) = undefined
;(this.addingSubtagTo as unknown) = undefined
destroyAllObjectProperties(this)
}
async createSubtagAndAssignParent(parent: SNTag, title: string) {
const hasEmptyTitle = title.length === 0