diff --git a/app/assets/javascripts/Application/Application.ts b/app/assets/javascripts/Application/Application.ts index 03a832d9f..b03157b84 100644 --- a/app/assets/javascripts/Application/Application.ts +++ b/app/assets/javascripts/Application/Application.ts @@ -17,8 +17,15 @@ import { DesktopDeviceInterface, isDesktopDevice, DeinitMode, + PrefKey, + SNTag, + ContentType, + DecryptedItemInterface, } from '@standardnotes/snjs' import { makeObservable, observable } from 'mobx' +import { PanelResizedData } from '@/Types/PanelResizedData' +import { WebAppEvent } from './WebAppEvent' +import { isDesktopApplication } from '@/Utils' type WebServices = { viewControllerManager: ViewControllerManager @@ -29,19 +36,14 @@ type WebServices = { io: IOService } -export enum WebAppEvent { - NewUpdateAvailable = 'NewUpdateAvailable', - DesktopWindowGainedFocus = 'DesktopWindowGainedFocus', - DesktopWindowLostFocus = 'DesktopWindowLostFocus', -} - -export type WebEventObserver = (event: WebAppEvent) => void +export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void export class WebApplication extends SNApplication { private webServices!: WebServices private webEventObservers: WebEventObserver[] = [] public noteControllerGroup: NoteGroupController public iconsController: IconsController + private onVisibilityChange: () => void constructor( deviceInterface: WebOrDesktopDevice, @@ -70,6 +72,16 @@ export class WebApplication extends SNApplication { deviceInterface.setApplication(this) this.noteControllerGroup = new NoteGroupController(this) this.iconsController = new IconsController() + + this.onVisibilityChange = () => { + const visible = document.visibilityState === 'visible' + const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur + this.notifyWebEvent(event) + } + + if (!isDesktopApplication()) { + document.addEventListener('visibilitychange', this.onVisibilityChange) + } } override deinit(mode: DeinitMode, source: DeinitSource): void { @@ -94,6 +106,9 @@ export class WebApplication extends SNApplication { ;(this.noteControllerGroup as unknown) = undefined this.webEventObservers.length = 0 + + document.removeEventListener('visibilitychange', this.onVisibilityChange) + ;(this.onVisibilityChange as unknown) = undefined } catch (error) { console.error('Error while deiniting application', error) } @@ -105,17 +120,26 @@ export class WebApplication extends SNApplication { public addWebEventObserver(observer: WebEventObserver): () => void { this.webEventObservers.push(observer) + return () => { removeFromArray(this.webEventObservers, observer) } } - public notifyWebEvent(event: WebAppEvent): void { + public notifyWebEvent(event: WebAppEvent, data?: unknown): void { for (const observer of this.webEventObservers) { - observer(event) + observer(event, data) } } + publishPanelDidResizeEvent(name: string, collapsed: boolean) { + const data: PanelResizedData = { + panel: name, + collapsed: collapsed, + } + this.notifyWebEvent(WebAppEvent.PanelResized, data) + } + public getViewControllerManager(): ViewControllerManager { return this.webServices.viewControllerManager } @@ -163,4 +187,23 @@ export class WebApplication extends SNApplication { return this.user.signOut() } + + isGlobalSpellcheckEnabled(): boolean { + return this.getPreference(PrefKey.EditorSpellcheck, true) + } + + public getItemTags(item: DecryptedItemInterface) { + return this.items.itemsReferencingItem(item).filter((ref) => { + return ref.content_type === ContentType.Tag + }) as SNTag[] + } + + public get version(): string { + return (this.deviceInterface as WebOrDesktopDevice).appVersion + } + + async toggleGlobalSpellcheck() { + const currentValue = this.isGlobalSpellcheckEnabled() + return this.setPreference(PrefKey.EditorSpellcheck, !currentValue) + } } diff --git a/app/assets/javascripts/Application/WebAppEvent.ts b/app/assets/javascripts/Application/WebAppEvent.ts new file mode 100644 index 000000000..ecc531981 --- /dev/null +++ b/app/assets/javascripts/Application/WebAppEvent.ts @@ -0,0 +1,9 @@ +export enum WebAppEvent { + NewUpdateAvailable = 'NewUpdateAvailable', + EditorFocused = 'EditorFocused', + BeganBackupDownload = 'BeganBackupDownload', + EndedBackupDownload = 'EndedBackupDownload', + PanelResized = 'PanelResized', + WindowDidFocus = 'WindowDidFocus', + WindowDidBlur = 'WindowDidBlur', +} diff --git a/app/assets/javascripts/Components/Abstract/PureComponent.tsx b/app/assets/javascripts/Components/Abstract/PureComponent.tsx index 831d058c2..cddb98807 100644 --- a/app/assets/javascripts/Components/Abstract/PureComponent.tsx +++ b/app/assets/javascripts/Components/Abstract/PureComponent.tsx @@ -1,6 +1,6 @@ import { ApplicationEvent } from '@standardnotes/snjs' import { WebApplication } from '@/Application/Application' -import { ViewControllerManager, ViewControllerManagerEvent } from '@/Services/ViewControllerManager' +import { ViewControllerManager } from '@/Services/ViewControllerManager' import { autorun, IReactionDisposer, IReactionPublic } from 'mobx' import { Component } from 'react' @@ -9,7 +9,6 @@ export type PureComponentProps = Partial> export abstract class PureComponent

extends Component { private unsubApp!: () => void - private unsubState!: () => void private reactionDisposers: IReactionDisposer[] = [] constructor(props: P, protected application: WebApplication) { @@ -18,18 +17,17 @@ export abstract class PureComponent

{ - this.onViewControllerManagerEvent(eventName, data) - }) - } - - onViewControllerManagerEvent(_eventName: ViewControllerManagerEvent, _data: unknown) { - /** Optional override */ - } - addAppEventObserver() { if (this.application.isStarted()) { this.onAppStart().catch(console.error) diff --git a/app/assets/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx b/app/assets/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx index e555cd259..a08cb3bc1 100644 --- a/app/assets/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx +++ b/app/assets/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx @@ -169,7 +169,7 @@ const GeneralAccountMenu: FunctionComponent = ({ Help & feedback - v{viewControllerManager.version} + v{application.version} {user ? ( <> diff --git a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx index fb492862f..20d441181 100644 --- a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -1,10 +1,10 @@ import { ApplicationGroup } from '@/Application/ApplicationGroup' import { getPlatformString, getWindowUrlParams } from '@/Utils' -import { ViewControllerManagerEvent } from '@/Services/ViewControllerManager' import { ApplicationEvent, Challenge, removeFromArray } from '@standardnotes/snjs' import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants/Constants' import { alertDialog } from '@/Services/AlertService' import { WebApplication } from '@/Application/Application' +import { WebAppEvent } from '@/Application/WebAppEvent' import Navigation from '@/Components/Navigation/Navigation' import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView' import Footer from '@/Components/Footer/Footer' @@ -120,8 +120,8 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio }, [application, onAppLaunch, onAppStart]) useEffect(() => { - const removeObserver = application.getViewControllerManager().addObserver(async (eventName, data) => { - if (eventName === ViewControllerManagerEvent.PanelResized) { + const removeObserver = application.addWebEventObserver(async (eventName, data) => { + if (eventName === WebAppEvent.PanelResized) { const { panel, collapsed } = data as PanelResizedData let appClass = '' if (panel === PANEL_NAME_NOTES && collapsed) { @@ -131,7 +131,7 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio appClass += ' collapsed-navigation' } setAppClass(appClass) - } else if (eventName === ViewControllerManagerEvent.WindowDidFocus) { + } else if (eventName === WebAppEvent.WindowDidFocus) { if (!(await application.isLocked())) { application.sync.sync().catch(console.error) } diff --git a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index 6adcdfec1..2bc3d8a72 100644 --- a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -90,7 +90,7 @@ const ChangeEditorMenu: FunctionComponent = ({ const transactions: TransactionalMutation[] = [] - await application.getViewControllerManager().contentListController.insertCurrentIfTemplate() + await application.getViewControllerManager().itemListController.insertCurrentIfTemplate() if (note.locked) { application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error) diff --git a/app/assets/javascripts/Components/ContentListView/ContentList.tsx b/app/assets/javascripts/Components/ContentListView/ContentList.tsx index 33f8b2066..8e5709e75 100644 --- a/app/assets/javascripts/Components/ContentListView/ContentList.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentList.tsx @@ -23,10 +23,10 @@ const ContentList: FunctionComponent = ({ selectedItems, paginate, }) => { - const { selectPreviousItem, selectNextItem } = viewControllerManager.contentListController + const { selectPreviousItem, selectNextItem } = viewControllerManager.itemListController const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = - viewControllerManager.contentListController.webDisplayOptions - const { sortBy } = viewControllerManager.contentListController.displayOptions + viewControllerManager.itemListController.webDisplayOptions + const { sortBy } = viewControllerManager.itemListController.displayOptions const onScroll: UIEventHandler = useCallback( (e) => { diff --git a/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx b/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx index 2c4d301c0..ab4ebcbaf 100644 --- a/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx @@ -15,7 +15,7 @@ const ContentListItem: FunctionComponent = (props) => { return [] } - const tags = props.viewControllerManager.getItemTags(props.item) + const tags = props.application.getItemTags(props.item) const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1 if (isNavigatingOnlyTag) { diff --git a/app/assets/javascripts/Components/ContentListView/ContentListView.tsx b/app/assets/javascripts/Components/ContentListView/ContentListView.tsx index 1385f550e..53bdb6884 100644 --- a/app/assets/javascripts/Components/ContentListView/ContentListView.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentListView.tsx @@ -46,7 +46,7 @@ const ContentListView: FunctionComponent = ({ application, viewController paginate, panelWidth, createNewNote, - } = viewControllerManager.contentListController + } = viewControllerManager.itemListController const { selectedItems } = viewControllerManager.selectionController @@ -143,7 +143,7 @@ const ContentListView: FunctionComponent = ({ application, viewController (width, _lastLeft, _isMaxWidth, isCollapsed) => { application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error) viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth() - viewControllerManager.panelDidResize(PANEL_NAME_NOTES, isCollapsed) + application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed) }, [viewControllerManager, application], ) diff --git a/app/assets/javascripts/Components/Footer/Footer.tsx b/app/assets/javascripts/Components/Footer/Footer.tsx index 9967719bb..f6d5a969b 100644 --- a/app/assets/javascripts/Components/Footer/Footer.tsx +++ b/app/assets/javascripts/Components/Footer/Footer.tsx @@ -1,4 +1,5 @@ -import { WebAppEvent, WebApplication } from '@/Application/Application' +import { WebApplication } from '@/Application/Application' +import { WebAppEvent } from '@/Application/WebAppEvent' import { ApplicationGroup } from '@/Application/ApplicationGroup' import { PureComponent } from '@/Components/Abstract/PureComponent' import { destroyAllObjectProperties, preventRefreshing } from '@/Utils' @@ -12,7 +13,6 @@ import { } from '@/Constants/Strings' import { alertDialog, confirmDialog } from '@/Services/AlertService' import AccountMenu from '@/Components/AccountMenu/AccountMenu' -import { ViewControllerManagerEvent } from '@/Services/ViewControllerManager' import Icon from '@/Components/Icon/Icon' import QuickSettingsMenu from '@/Components/QuickSettingsMenu/QuickSettingsMenu' import SyncResolutionMenu from '@/Components/SyncResolutionMenu/SyncResolutionMenu' @@ -64,9 +64,34 @@ class Footer extends PureComponent { showQuickSettingsMenu: false, } - this.webEventListenerDestroyer = props.application.addWebEventObserver((event) => { - if (event === WebAppEvent.NewUpdateAvailable) { - this.onNewUpdateAvailable() + this.webEventListenerDestroyer = props.application.addWebEventObserver((event, data) => { + const statusService = this.application.status + + switch (event) { + case WebAppEvent.NewUpdateAvailable: + this.onNewUpdateAvailable() + break + case WebAppEvent.EditorFocused: + if ((data as any).eventSource === EditorEventSource.UserInteraction) { + this.closeAccountMenu() + } + break + case WebAppEvent.BeganBackupDownload: + statusService.setMessage('Saving local backup…') + break + case WebAppEvent.EndedBackupDownload: { + const successMessage = 'Successfully saved backup.' + const errorMessage = 'Unable to save local backup.' + statusService.setMessage((data as any).success ? successMessage : errorMessage) + + const twoSeconds = 2000 + setTimeout(() => { + if (statusService.message === successMessage || statusService.message === errorMessage) { + statusService.setMessage('') + } + }, twoSeconds) + break + } } }) } @@ -133,33 +158,6 @@ class Footer extends PureComponent { }) } - override onViewControllerManagerEvent(eventName: ViewControllerManagerEvent, data: any) { - const statusService = this.application.status - switch (eventName) { - case ViewControllerManagerEvent.EditorFocused: - if (data.eventSource === EditorEventSource.UserInteraction) { - this.closeAccountMenu() - } - break - case ViewControllerManagerEvent.BeganBackupDownload: - statusService.setMessage('Saving local backup…') - break - case ViewControllerManagerEvent.EndedBackupDownload: { - const successMessage = 'Successfully saved backup.' - const errorMessage = 'Unable to save local backup.' - statusService.setMessage(data.success ? successMessage : errorMessage) - - const twoSeconds = 2000 - setTimeout(() => { - if (statusService.message === successMessage || statusService.message === errorMessage) { - statusService.setMessage('') - } - }, twoSeconds) - break - } - } - } - override async onAppKeyChange() { super.onAppKeyChange().catch(console.error) this.reloadPasscodeStatus().catch(console.error) diff --git a/app/assets/javascripts/Components/Navigation/Navigation.tsx b/app/assets/javascripts/Components/Navigation/Navigation.tsx index 1ca094af9..e01a43df6 100644 --- a/app/assets/javascripts/Components/Navigation/Navigation.tsx +++ b/app/assets/javascripts/Components/Navigation/Navigation.tsx @@ -33,7 +33,7 @@ const Navigation: FunctionComponent = ({ application }) => { (width, _lastLeft, _isMaxWidth, isCollapsed) => { application.setPreference(PrefKey.TagsPanelWidth, width).catch(console.error) viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth() - viewControllerManager.panelDidResize(PANEL_NAME_NAVIGATION, isCollapsed) + application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, isCollapsed) }, [application, viewControllerManager], ) diff --git a/app/assets/javascripts/Components/NoteView/NoteView.tsx b/app/assets/javascripts/Components/NoteView/NoteView.tsx index 5e1d95299..6e04192e9 100644 --- a/app/assets/javascripts/Components/NoteView/NoteView.tsx +++ b/app/assets/javascripts/Components/NoteView/NoteView.tsx @@ -35,6 +35,7 @@ import { } from './TransactionFunctions' import { reloadFont } from './FontFunctions' import { NoteViewProps } from './NoteViewProps' +import { WebAppEvent } from '@/Application/WebAppEvent' const MINIMUM_STATUS_DURATION = 400 const TEXTAREA_DEBOUNCE = 100 @@ -588,7 +589,7 @@ class NoteView extends PureComponent { onContentFocus = () => { if (this.lastEditorFocusEventSource) { - this.application.getViewControllerManager().editorDidFocus(this.lastEditorFocusEventSource) + this.application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: this.lastEditorFocusEventSource }) } this.lastEditorFocusEventSource = undefined } diff --git a/app/assets/javascripts/Components/Preferences/Panes/General/Defaults.tsx b/app/assets/javascripts/Components/Preferences/Panes/General/Defaults.tsx index 2801073d9..4f74ca10d 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/General/Defaults.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/General/Defaults.tsx @@ -57,7 +57,7 @@ const Defaults: FunctionComponent = ({ application }) => { const toggleSpellcheck = () => { setSpellcheck(!spellcheck) - application.getViewControllerManager().toggleGlobalSpellcheck().catch(console.error) + application.toggleGlobalSpellcheck().catch(console.error) } useEffect(() => { diff --git a/app/assets/javascripts/Components/Tags/RootTagDropZone.tsx b/app/assets/javascripts/Components/Tags/RootTagDropZone.tsx index 84987a8b4..13e2e5513 100644 --- a/app/assets/javascripts/Components/Tags/RootTagDropZone.tsx +++ b/app/assets/javascripts/Components/Tags/RootTagDropZone.tsx @@ -1,14 +1,14 @@ import Icon from '@/Components/Icon/Icon' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { FeaturesController } from '@/Controllers/FeaturesController' -import { TagsController } from '@/Controllers/Navigation/TagsController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'react' import { useDrop } from 'react-dnd' import { DropItem, DropProps, ItemTypes } from './DragNDrop' type Props = { - tagsState: TagsController + tagsState: NavigationController featuresState: FeaturesController } diff --git a/app/assets/javascripts/Components/Tags/SmartViewsListItem.tsx b/app/assets/javascripts/Components/Tags/SmartViewsListItem.tsx index ab8a8ca2c..e895770df 100644 --- a/app/assets/javascripts/Components/Tags/SmartViewsListItem.tsx +++ b/app/assets/javascripts/Components/Tags/SmartViewsListItem.tsx @@ -1,6 +1,6 @@ import Icon from '@/Components/Icon/Icon' import { FeaturesController } from '@/Controllers/FeaturesController' -import { TagsController } from '@/Controllers/Navigation/TagsController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' import '@reach/tooltip/styles.css' import { SmartView, SystemViewId, IconType, isSystemView } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' @@ -16,7 +16,7 @@ import { type Props = { view: SmartView - tagsState: TagsController + tagsState: NavigationController features: FeaturesController } diff --git a/app/assets/javascripts/Components/Tags/TagsListItem.tsx b/app/assets/javascripts/Components/Tags/TagsListItem.tsx index 7c524c511..bdacac921 100644 --- a/app/assets/javascripts/Components/Tags/TagsListItem.tsx +++ b/app/assets/javascripts/Components/Tags/TagsListItem.tsx @@ -3,7 +3,7 @@ import { TAG_FOLDERS_FEATURE_NAME } from '@/Constants/Constants' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { KeyboardKey } from '@/Services/IOService' import { FeaturesController } from '@/Controllers/FeaturesController' -import { TagsController } from '@/Controllers/Navigation/TagsController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' import '@reach/tooltip/styles.css' import { SNTag } from '@standardnotes/snjs' import { computed } from 'mobx' @@ -23,7 +23,7 @@ import { DropItem, DropProps, ItemTypes } from './DragNDrop' type Props = { tag: SNTag - tagsState: TagsController + tagsState: NavigationController features: FeaturesController level: number onContextMenu: (tag: SNTag, posX: number, posY: number) => void diff --git a/app/assets/javascripts/Components/Tags/TagsSectionAddButton.tsx b/app/assets/javascripts/Components/Tags/TagsSectionAddButton.tsx index ccac02cc5..405e0b903 100644 --- a/app/assets/javascripts/Components/Tags/TagsSectionAddButton.tsx +++ b/app/assets/javascripts/Components/Tags/TagsSectionAddButton.tsx @@ -1,11 +1,11 @@ import IconButton from '@/Components/Button/IconButton' import { FeaturesController } from '@/Controllers/FeaturesController' -import { TagsController } from '@/Controllers/Navigation/TagsController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'react' type Props = { - tags: TagsController + tags: NavigationController features: FeaturesController } diff --git a/app/assets/javascripts/Controllers/Abstract/AbstractViewController.ts b/app/assets/javascripts/Controllers/Abstract/AbstractViewController.ts index e72335491..9dcc477a5 100644 --- a/app/assets/javascripts/Controllers/Abstract/AbstractViewController.ts +++ b/app/assets/javascripts/Controllers/Abstract/AbstractViewController.ts @@ -1,14 +1,27 @@ -import { DeinitSource } from '@standardnotes/snjs' +import { CrossControllerEvent } from '../CrossControllerEvent' +import { InternalEventBus, InternalEventPublishStrategy } from '@standardnotes/snjs' import { WebApplication } from '../../Application/Application' +import { Disposer } from '@/Types/Disposer' export abstract class AbstractViewController { dealloced = false + protected disposers: Disposer[] = [] - constructor(public application: WebApplication, public viewControllerManager?: AbstractViewController) {} + constructor(public application: WebApplication, protected eventBus: InternalEventBus) {} - deinit(_source: DeinitSource): void { + protected async publishEventSync(name: CrossControllerEvent): Promise { + await this.eventBus.publishSync({ type: name, payload: undefined }, InternalEventPublishStrategy.SEQUENCE) + } + + deinit(): void { this.dealloced = true ;(this.application as unknown) = undefined - ;(this.viewControllerManager as unknown) = undefined + ;(this.eventBus as unknown) = undefined + + for (const disposer of this.disposers) { + disposer() + } + + ;(this.disposers as unknown) = undefined } } diff --git a/app/assets/javascripts/Controllers/Abstract/IsControllerDealloced.ts b/app/assets/javascripts/Controllers/Abstract/IsControllerDealloced.ts index 5c4cbd58f..92f2c069f 100644 --- a/app/assets/javascripts/Controllers/Abstract/IsControllerDealloced.ts +++ b/app/assets/javascripts/Controllers/Abstract/IsControllerDealloced.ts @@ -1,5 +1,3 @@ -import { AbstractViewController } from './AbstractViewController' - -export function isControllerDealloced(state: AbstractViewController): boolean { - return state.dealloced == undefined || state.dealloced === true +export function isControllerDealloced(controller: { dealloced: boolean }): boolean { + return controller.dealloced == undefined || controller.dealloced === true } diff --git a/app/assets/javascripts/Controllers/AccountMenu/AccountMenuController.ts b/app/assets/javascripts/Controllers/AccountMenu/AccountMenuController.ts index f4bedc2dc..9e2128d21 100644 --- a/app/assets/javascripts/Controllers/AccountMenu/AccountMenuController.ts +++ b/app/assets/javascripts/Controllers/AccountMenu/AccountMenuController.ts @@ -1,6 +1,6 @@ import { destroyAllObjectProperties, isDev } from '@/Utils' import { action, computed, makeObservable, observable, runInAction } from 'mobx' -import { ApplicationEvent, ContentType, DeinitSource, SNNote, SNTag } from '@standardnotes/snjs' +import { ApplicationEvent, ContentType, InternalEventBus, SNNote, SNTag } from '@standardnotes/snjs' import { WebApplication } from '@/Application/Application' import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane' import { AbstractViewController } from '../Abstract/AbstractViewController' @@ -21,15 +21,16 @@ export class AccountMenuController extends AbstractViewController { shouldAnimateCloseMenu = false currentPane = AccountMenuPane.GeneralMenu - override deinit(source: DeinitSource) { - super.deinit(source) + override deinit() { + super.deinit() ;(this.notesAndTags as unknown) = undefined destroyAllObjectProperties(this) } - constructor(application: WebApplication, private appEventListeners: (() => void)[]) { - super(application) + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) + makeObservable(this, { show: observable, signingOut: observable, @@ -60,12 +61,7 @@ export class AccountMenuController extends AbstractViewController { notesAndTagsCount: computed, }) - this.addAppLaunchedEventObserver() - this.streamNotesAndTags() - } - - addAppLaunchedEventObserver = (): void => { - this.appEventListeners.push( + this.disposers.push( this.application.addEventObserver(async () => { runInAction(() => { if (isDev && window.devAccountServer) { @@ -77,10 +73,8 @@ export class AccountMenuController extends AbstractViewController { }) }, ApplicationEvent.Launched), ) - } - streamNotesAndTags = (): void => { - this.appEventListeners.push( + this.disposers.push( this.application.streamItems([ContentType.Note, ContentType.Tag], () => { runInAction(() => { this.notesAndTags = this.application.items.getItems([ContentType.Note, ContentType.Tag]) diff --git a/app/assets/javascripts/Controllers/CrossControllerEvent.ts b/app/assets/javascripts/Controllers/CrossControllerEvent.ts new file mode 100644 index 000000000..1c89dd2f9 --- /dev/null +++ b/app/assets/javascripts/Controllers/CrossControllerEvent.ts @@ -0,0 +1,4 @@ +export enum CrossControllerEvent { + TagChanged = 'TagChanged', + ActiveEditorChanged = 'ActiveEditorChanged', +} diff --git a/app/assets/javascripts/Controllers/FeaturesController.ts b/app/assets/javascripts/Controllers/FeaturesController.ts index c1a7720f7..157cfab55 100644 --- a/app/assets/javascripts/Controllers/FeaturesController.ts +++ b/app/assets/javascripts/Controllers/FeaturesController.ts @@ -1,6 +1,6 @@ import { WebApplication } from '@/Application/Application' import { destroyAllObjectProperties } from '@/Utils' -import { ApplicationEvent, DeinitSource, FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs' +import { ApplicationEvent, FeatureIdentifier, FeatureStatus, InternalEventBus } from '@standardnotes/snjs' import { action, makeObservable, observable, runInAction, when } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' @@ -10,8 +10,8 @@ export class FeaturesController extends AbstractViewController { hasFiles: boolean premiumAlertFeatureName: string | undefined - override deinit(source: DeinitSource) { - super.deinit(source) + override deinit() { + super.deinit() ;(this.showPremiumAlert as unknown) = undefined ;(this.closePremiumAlert as unknown) = undefined ;(this.hasFolders as unknown) = undefined @@ -22,8 +22,8 @@ export class FeaturesController extends AbstractViewController { destroyAllObjectProperties(this) } - constructor(application: WebApplication, appObservers: (() => void)[]) { - super(application) + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) this.hasFolders = this.isEntitledToFolders() this.hasSmartViews = this.isEntitledToSmartViews() @@ -43,7 +43,7 @@ export class FeaturesController extends AbstractViewController { this.showPremiumAlert = this.showPremiumAlert.bind(this) this.closePremiumAlert = this.closePremiumAlert.bind(this) - appObservers.push( + this.disposers.push( application.addEventObserver(async (event) => { switch (event) { case ApplicationEvent.FeaturesUpdated: diff --git a/app/assets/javascripts/Controllers/FilesController.ts b/app/assets/javascripts/Controllers/FilesController.ts index aeb7c11d0..d09abd878 100644 --- a/app/assets/javascripts/Controllers/FilesController.ts +++ b/app/assets/javascripts/Controllers/FilesController.ts @@ -1,3 +1,4 @@ +import { FilePreviewModalController } from './FilePreviewModalController' import { PopoverFileItemAction, PopoverFileItemActionType, @@ -13,12 +14,13 @@ import { ClassicFileSaver, parseFileName, } from '@standardnotes/filepicker' -import { ChallengeReason, ClientDisplayableError, ContentType, FileItem } from '@standardnotes/snjs' +import { ChallengeReason, ClientDisplayableError, ContentType, FileItem, InternalEventBus } from '@standardnotes/snjs' import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/stylekit' import { action, computed, makeObservable, observable, reaction } from 'mobx' import { WebApplication } from '../Application/Application' import { AbstractViewController } from './Abstract/AbstractViewController' -import { ViewControllerManager } from '../Services/ViewControllerManager/ViewControllerManager' +import { NotesController } from './NotesController' +import { SelectedItemsController } from './SelectedItemsController' const UnprotectedFileActions = [PopoverFileItemActionType.ToggleFileProtection] const NonMutatingFileActions = [PopoverFileItemActionType.DownloadFile, PopoverFileItemActionType.PreviewFile] @@ -31,12 +33,21 @@ export class FilesController extends AbstractViewController { showFileContextMenu = false fileContextMenuLocation: FileContextMenuLocation = { x: 0, y: 0 } + override deinit(): void { + super.deinit() + ;(this.notesController as unknown) = undefined + ;(this.selectionController as unknown) = undefined + ;(this.filePreviewModalController as unknown) = undefined + } + constructor( application: WebApplication, - override viewControllerManager: ViewControllerManager, - appObservers: (() => void)[], + private notesController: NotesController, + private selectionController: SelectedItemsController, + private filePreviewModalController: FilePreviewModalController, + eventBus: InternalEventBus, ) { - super(application, viewControllerManager) + super(application, eventBus) makeObservable(this, { allFiles: observable, @@ -52,13 +63,16 @@ export class FilesController extends AbstractViewController { setFileContextMenuLocation: action, }) - appObservers.push( + this.disposers.push( application.streamItems(ContentType.File, () => { this.reloadAllFiles() this.reloadAttachedFiles() }), + ) + + this.disposers.push( reaction( - () => viewControllerManager.notesController.selectedNotes, + () => notesController.selectedNotes, () => { this.reloadAttachedFiles() }, @@ -67,7 +81,7 @@ export class FilesController extends AbstractViewController { } get selectedFiles(): FileItem[] { - return this.viewControllerManager.selectionController.getSelectedItems(ContentType.File) + return this.selectionController.getSelectedItems(ContentType.File) } setShowFileContextMenu = (enabled: boolean) => { @@ -83,7 +97,7 @@ export class FilesController extends AbstractViewController { } reloadAttachedFiles = () => { - const note = this.viewControllerManager.notesController.firstSelectedNote + const note = this.notesController.firstSelectedNote if (note) { this.attachedFiles = this.application.items.getFilesForNote(note) } @@ -109,7 +123,7 @@ export class FilesController extends AbstractViewController { } attachFileToNote = async (file: FileItem) => { - const note = this.viewControllerManager.notesController.firstSelectedNote + const note = this.notesController.firstSelectedNote if (!note) { addToast({ type: ToastType.Error, @@ -122,7 +136,7 @@ export class FilesController extends AbstractViewController { } detachFileFromNote = async (file: FileItem) => { - const note = this.viewControllerManager.notesController.firstSelectedNote + const note = this.notesController.firstSelectedNote if (!note) { addToast({ type: ToastType.Error, @@ -197,7 +211,7 @@ export class FilesController extends AbstractViewController { await this.renameFile(file, action.payload.name) break case PopoverFileItemActionType.PreviewFile: - this.viewControllerManager.filePreviewModalController.activate( + this.filePreviewModalController.activate( file, currentTab === PopoverTabs.AllFiles ? this.allFiles : this.attachedFiles, ) diff --git a/app/assets/javascripts/Controllers/ItemList/ItemListController.ts b/app/assets/javascripts/Controllers/ItemList/ItemListController.ts index d6b468191..b909dae27 100644 --- a/app/assets/javascripts/Controllers/ItemList/ItemListController.ts +++ b/app/assets/javascripts/Controllers/ItemList/ItemListController.ts @@ -4,7 +4,6 @@ import { ApplicationEvent, CollectionSort, ContentType, - DeinitSource, findInArray, NoteViewController, PrefKey, @@ -13,13 +12,21 @@ import { SNTag, SystemViewId, DisplayOptions, + InternalEventBus, + InternalEventHandlerInterface, + InternalEventInterface, } from '@standardnotes/snjs' import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx' import { WebApplication } from '../../Application/Application' import { AbstractViewController } from '../Abstract/AbstractViewController' -import { ViewControllerManager } from '../../Services/ViewControllerManager/ViewControllerManager' -import { ViewControllerManagerEvent } from '../../Services/ViewControllerManager/ViewControllerManagerEvent' import { WebDisplayOptions } from './WebDisplayOptions' +import { NavigationController } from '../Navigation/NavigationController' +import { CrossControllerEvent } from '../CrossControllerEvent' +import { SearchOptionsController } from '../SearchOptionsController' +import { SelectedItemsController } from '../SelectedItemsController' +import { NotesController } from '../NotesController' +import { NoteTagsController } from '../NoteTagsController' +import { WebAppEvent } from '@/Application/WebAppEvent' const MinNoteCellHeight = 51.0 const DefaultListNumNotes = 20 @@ -27,7 +34,7 @@ const ElementIdSearchBar = 'search-bar' const ElementIdScrollContainer = 'notes-scrollable' const SupportsFileSelectionState = false -export class ItemListController extends AbstractViewController { +export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface { completedFullSync = false noteFilterText = '' notes: SNNote[] = [] @@ -55,11 +62,16 @@ export class ItemListController extends AbstractViewController { } private reloadItemsPromise?: Promise - override deinit(source: DeinitSource) { - super.deinit(source) + override deinit() { + super.deinit() ;(this.noteFilterText as unknown) = undefined ;(this.notes as unknown) = undefined ;(this.renderedItems as unknown) = undefined + ;(this.navigationController as unknown) = undefined + ;(this.searchOptionsController as unknown) = undefined + ;(this.selectionController as unknown) = undefined + ;(this.notesController as unknown) = undefined + ;(this.noteTagsController as unknown) = undefined ;(window.onresize as unknown) = undefined destroyAllObjectProperties(this) @@ -67,18 +79,27 @@ export class ItemListController extends AbstractViewController { constructor( application: WebApplication, - override viewControllerManager: ViewControllerManager, - appObservers: (() => void)[], + private navigationController: NavigationController, + private searchOptionsController: SearchOptionsController, + private selectionController: SelectedItemsController, + private notesController: NotesController, + private noteTagsController: NoteTagsController, + eventBus: InternalEventBus, ) { - super(application, viewControllerManager) + super(application, eventBus) + + eventBus.addEventHandler(this, CrossControllerEvent.TagChanged) + eventBus.addEventHandler(this, CrossControllerEvent.ActiveEditorChanged) this.resetPagination() - appObservers.push( + this.disposers.push( application.streamItems(ContentType.Note, () => { void this.reloadItems() }), + ) + this.disposers.push( application.streamItems([ContentType.Tag], async ({ changed, inserted }) => { const tags = [...changed, ...inserted] @@ -87,28 +108,34 @@ export class ItemListController extends AbstractViewController { void this.reloadItems() - if ( - viewControllerManager.navigationController.selected && - findInArray(tags, 'uuid', viewControllerManager.navigationController.selected.uuid) - ) { + if (this.navigationController.selected && findInArray(tags, 'uuid', this.navigationController.selected.uuid)) { /** Tag title could have changed */ this.reloadPanelTitle() } }), + ) + + this.disposers.push( application.addEventObserver(async () => { void this.reloadPreferences() }, ApplicationEvent.PreferencesChanged), + ) + + this.disposers.push( application.addEventObserver(async () => { this.application.noteControllerGroup.closeAllNoteControllers() void this.selectFirstItem() this.setCompletedFullSync(false) }, ApplicationEvent.SignedIn), + ) + + this.disposers.push( application.addEventObserver(async () => { void this.reloadItems().then(() => { if ( this.notes.length === 0 && - viewControllerManager.navigationController.selected instanceof SmartView && - viewControllerManager.navigationController.selected.uuid === SystemViewId.AllNotes && + this.navigationController.selected instanceof SmartView && + this.navigationController.selected.uuid === SystemViewId.AllNotes && this.noteFilterText === '' && !this.getActiveNoteController() ) { @@ -117,28 +144,28 @@ export class ItemListController extends AbstractViewController { }) this.setCompletedFullSync(true) }, ApplicationEvent.CompletedFullSync), + ) + this.disposers.push( + application.addWebEventObserver((webEvent) => { + if (webEvent === WebAppEvent.EditorFocused) { + this.setShowDisplayOptionsMenu(false) + } + }), + ) + + this.disposers.push( reaction( () => [ - viewControllerManager.searchOptionsController.includeProtectedContents, - viewControllerManager.searchOptionsController.includeArchived, - viewControllerManager.searchOptionsController.includeTrashed, + this.searchOptionsController.includeProtectedContents, + this.searchOptionsController.includeArchived, + this.searchOptionsController.includeTrashed, ], () => { this.reloadNotesDisplayOptions() void this.reloadItems() }, ), - - viewControllerManager.addObserver(async (eventName) => { - if (eventName === ViewControllerManagerEvent.TagChanged) { - this.handleTagChange() - } else if (eventName === ViewControllerManagerEvent.ActiveEditorChanged) { - this.handleEditorChange().catch(console.error) - } else if (eventName === ViewControllerManagerEvent.EditorFocused) { - this.setShowDisplayOptionsMenu(false) - } - }), ) makeObservable(this, { @@ -170,6 +197,14 @@ export class ItemListController extends AbstractViewController { } } + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === CrossControllerEvent.TagChanged) { + this.handleTagChange() + } else if (event.type === CrossControllerEvent.ActiveEditorChanged) { + this.handleEditorChange().catch(console.error) + } + } + public getActiveNoteController(): NoteViewController | undefined { return this.application.noteControllerGroup.activeNoteViewController } @@ -200,8 +235,8 @@ export class ItemListController extends AbstractViewController { if (this.isFiltering) { const resultCount = this.notes.length title = `${resultCount} search results` - } else if (this.viewControllerManager.navigationController.selected) { - title = `${this.viewControllerManager.navigationController.selected.title}` + } else if (this.navigationController.selected) { + title = `${this.navigationController.selected.title}` } this.panelTitle = title @@ -218,7 +253,7 @@ export class ItemListController extends AbstractViewController { } private async performReloadItems() { - const tag = this.viewControllerManager.navigationController.selected + const tag = this.navigationController.selected if (!tag) { return } @@ -241,17 +276,16 @@ export class ItemListController extends AbstractViewController { } private async recomputeSelectionAfterItemsReload() { - const viewControllerManager = this.viewControllerManager const activeController = this.getActiveNoteController() const activeNote = activeController?.note const isSearching = this.noteFilterText.length > 0 - const hasMultipleItemsSelected = viewControllerManager.selectionController.selectedItemsCount >= 2 + const hasMultipleItemsSelected = this.selectionController.selectedItemsCount >= 2 if (hasMultipleItemsSelected) { return } - const selectedItem = Object.values(viewControllerManager.selectionController.selectedItems)[0] + const selectedItem = Object.values(this.selectionController.selectedItems)[0] const isSelectedItemFile = this.items.includes(selectedItem) && selectedItem && selectedItem.content_type === ContentType.File @@ -280,25 +314,25 @@ export class ItemListController extends AbstractViewController { } const showTrashedNotes = - (viewControllerManager.navigationController.selected instanceof SmartView && - viewControllerManager.navigationController.selected?.uuid === SystemViewId.TrashedNotes) || - viewControllerManager?.searchOptionsController.includeTrashed + (this.navigationController.selected instanceof SmartView && + this.navigationController.selected?.uuid === SystemViewId.TrashedNotes) || + this.searchOptionsController.includeTrashed const showArchivedNotes = - (viewControllerManager.navigationController.selected instanceof SmartView && - viewControllerManager.navigationController.selected.uuid === SystemViewId.ArchivedNotes) || - viewControllerManager.searchOptionsController.includeArchived || + (this.navigationController.selected instanceof SmartView && + this.navigationController.selected.uuid === SystemViewId.ArchivedNotes) || + this.searchOptionsController.includeArchived || this.application.getPreference(PrefKey.NotesShowArchived, false) if ((activeNote.trashed && !showTrashedNotes) || (activeNote.archived && !showArchivedNotes)) { await this.selectNextItemOrCreateNewNote() - } else if (!this.viewControllerManager.selectionController.selectedItems[activeNote.uuid]) { - await this.viewControllerManager.selectionController.selectItem(activeNote.uuid).catch(console.error) + } else if (!this.selectionController.selectedItems[activeNote.uuid]) { + await this.selectionController.selectItem(activeNote.uuid).catch(console.error) } } reloadNotesDisplayOptions = () => { - const tag = this.viewControllerManager.navigationController.selected + const tag = this.navigationController.selected const searchText = this.noteFilterText.toLowerCase() const isSearching = searchText.length @@ -306,8 +340,8 @@ export class ItemListController extends AbstractViewController { let includeTrashed: boolean if (isSearching) { - includeArchived = this.viewControllerManager.searchOptionsController.includeArchived - includeTrashed = this.viewControllerManager.searchOptionsController.includeTrashed + includeArchived = this.searchOptionsController.includeArchived + includeTrashed = this.searchOptionsController.includeTrashed } else { includeArchived = this.displayOptions.includeArchived ?? false includeTrashed = this.displayOptions.includeTrashed ?? false @@ -324,7 +358,7 @@ export class ItemListController extends AbstractViewController { includeProtected: this.displayOptions.includeProtected, searchQuery: { query: searchText, - includeProtectedNoteText: this.viewControllerManager.searchOptionsController.includeProtectedContents, + includeProtectedNoteText: this.searchOptionsController.includeProtectedContents, }, } @@ -387,13 +421,10 @@ export class ItemListController extends AbstractViewController { } createNewNote = async () => { - this.viewControllerManager.notesController.unselectNotes() + this.notesController.unselectNotes() - if ( - this.viewControllerManager.navigationController.isInSmartView() && - !this.viewControllerManager.navigationController.isInHomeView() - ) { - await this.viewControllerManager.navigationController.selectHomeNavigationView() + if (this.navigationController.isInSmartView() && !this.navigationController.isInHomeView()) { + await this.navigationController.selectHomeNavigationView() } let title = `Note ${this.notes.length + 1}` @@ -401,16 +432,13 @@ export class ItemListController extends AbstractViewController { title = this.noteFilterText } - await this.viewControllerManager.notesController.createNewNoteController(title) + await this.notesController.createNewNoteController(title) - this.viewControllerManager.noteTagsController.reloadTagsForCurrentNote() + this.noteTagsController.reloadTagsForCurrentNote() } createPlaceholderNote = () => { - if ( - this.viewControllerManager.navigationController.isInSmartView() && - !this.viewControllerManager.navigationController.isInHomeView() - ) { + if (this.navigationController.isInSmartView() && !this.navigationController.isInHomeView()) { return } @@ -487,7 +515,7 @@ export class ItemListController extends AbstractViewController { }, { userTriggered = false, scrollIntoView = true }, ): Promise => { - await this.viewControllerManager.selectionController.selectItem(item.uuid, userTriggered) + await this.selectionController.selectItem(item.uuid, userTriggered) if (scrollIntoView) { const itemElement = document.getElementById(item.uuid) @@ -514,7 +542,7 @@ export class ItemListController extends AbstractViewController { const displayableItems = this.items const currentIndex = displayableItems.findIndex((candidate) => { - return candidate.uuid === this.viewControllerManager.selectionController.lastSelectedItem?.uuid + return candidate.uuid === this.selectionController.lastSelectedItem?.uuid }) let nextIndex = currentIndex + 1 @@ -554,11 +582,11 @@ export class ItemListController extends AbstractViewController { selectPreviousItem = () => { const displayableItems = this.items - if (!this.viewControllerManager.selectionController.lastSelectedItem) { + if (!this.selectionController.lastSelectedItem) { return } - const currentIndex = displayableItems.indexOf(this.viewControllerManager.selectionController.lastSelectedItem) + const currentIndex = displayableItems.indexOf(this.selectionController.lastSelectedItem) let previousIndex = currentIndex - 1 diff --git a/app/assets/javascripts/Controllers/Navigation/TagsController.ts b/app/assets/javascripts/Controllers/Navigation/NavigationController.ts similarity index 94% rename from app/assets/javascripts/Controllers/Navigation/TagsController.ts rename to app/assets/javascripts/Controllers/Navigation/NavigationController.ts index 6e4d254b2..e56740dbc 100644 --- a/app/assets/javascripts/Controllers/Navigation/TagsController.ts +++ b/app/assets/javascripts/Controllers/Navigation/NavigationController.ts @@ -11,20 +11,20 @@ import { UuidString, isSystemView, FindItem, - DeinitSource, SystemViewId, + InternalEventBus, + InternalEventPublishStrategy, } from '@standardnotes/snjs' import { action, computed, makeAutoObservable, makeObservable, observable, runInAction } from 'mobx' import { WebApplication } from '../../Application/Application' import { FeaturesController } from '../FeaturesController' import { AbstractViewController } from '../Abstract/AbstractViewController' import { destroyAllObjectProperties } from '@/Utils' -import { ViewControllerManager } from '../../Services/ViewControllerManager/ViewControllerManager' -import { ViewControllerManagerEvent } from '../../Services/ViewControllerManager/ViewControllerManagerEvent' import { isValidFutureSiblings, rootTags, tagSiblings } from './Utils' import { AnyTag } from './AnyTagType' +import { CrossControllerEvent } from '../CrossControllerEvent' -export class TagsController extends AbstractViewController { +export class NavigationController extends AbstractViewController { tags: SNTag[] = [] smartViews: SmartView[] = [] allNotesCount_ = 0 @@ -43,13 +43,8 @@ export class TagsController extends AbstractViewController { private readonly tagsCountsState: TagsCountsState - constructor( - application: WebApplication, - override viewControllerManager: ViewControllerManager, - appEventListeners: (() => void)[], - private features: FeaturesController, - ) { - super(application) + constructor(application: WebApplication, private featuresController: FeaturesController, eventBus: InternalEventBus) { + super(application, eventBus) this.tagsCountsState = new TagsCountsState(this.application) @@ -99,7 +94,7 @@ export class TagsController extends AbstractViewController { setContextMenuMaxHeight: action, }) - appEventListeners.push( + this.disposers.push( this.application.streamItems([ContentType.Tag, ContentType.SmartView], ({ changed, removed }) => { runInAction(() => { this.tags = this.application.items.getDisplayableTags() @@ -131,7 +126,7 @@ export class TagsController extends AbstractViewController { }), ) - appEventListeners.push( + this.disposers.push( this.application.items.addNoteCountChangeObserver((tagUuid) => { if (!tagUuid) { this.setAllNotesCount(this.application.items.allCountableNotesCount()) @@ -145,15 +140,16 @@ export class TagsController extends AbstractViewController { ) } - override deinit(source: DeinitSource) { - super.deinit(source) - ;(this.features as unknown) = undefined + override deinit() { + super.deinit() + ;(this.featuresController 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 + ;(this.featuresController as unknown) = undefined destroyAllObjectProperties(this) } @@ -369,10 +365,13 @@ export class TagsController extends AbstractViewController { return } - await this.viewControllerManager.notifyEvent(ViewControllerManagerEvent.TagChanged, { - tag, - previousTag: this.previouslySelected_, - }) + await this.eventBus.publishSync( + { + type: CrossControllerEvent.TagChanged, + payload: { tag, previousTag: this.previouslySelected_ }, + }, + InternalEventPublishStrategy.SEQUENCE, + ) } public async selectHomeNavigationView(): Promise { @@ -473,8 +472,8 @@ export class TagsController extends AbstractViewController { const isSmartViewTitle = this.application.items.isSmartViewTitle(newTitle) if (isSmartViewTitle) { - if (!this.features.hasSmartViews) { - await this.features.showPremiumAlert(SMART_TAGS_FEATURE_NAME) + if (!this.featuresController.hasSmartViews) { + await this.featuresController.showPremiumAlert(SMART_TAGS_FEATURE_NAME) return } } diff --git a/app/assets/javascripts/Controllers/NoAccountWarningController.ts b/app/assets/javascripts/Controllers/NoAccountWarningController.ts index ad73be5bf..03559f580 100644 --- a/app/assets/javascripts/Controllers/NoAccountWarningController.ts +++ b/app/assets/javascripts/Controllers/NoAccountWarningController.ts @@ -1,5 +1,5 @@ import { storage, StorageKey } from '@/Services/LocalStorage' -import { ApplicationEvent } from '@standardnotes/snjs' +import { ApplicationEvent, InternalEventBus } from '@standardnotes/snjs' import { runInAction, makeObservable, observable, action } from 'mobx' import { WebApplication } from '../Application/Application' import { AbstractViewController } from './Abstract/AbstractViewController' @@ -7,17 +7,20 @@ import { AbstractViewController } from './Abstract/AbstractViewController' export class NoAccountWarningController extends AbstractViewController { show: boolean - constructor(application: WebApplication, appObservers: (() => void)[]) { - super(application) + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) this.show = application.hasAccount() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true - appObservers.push( + this.disposers.push( application.addEventObserver(async () => { runInAction(() => { this.show = false }) }, ApplicationEvent.SignedIn), + ) + + this.disposers.push( application.addEventObserver(async () => { if (application.hasAccount()) { runInAction(() => { diff --git a/app/assets/javascripts/Controllers/NoteTagsController.ts b/app/assets/javascripts/Controllers/NoteTagsController.ts index 2cca0809a..8e7e9f236 100644 --- a/app/assets/javascripts/Controllers/NoteTagsController.ts +++ b/app/assets/javascripts/Controllers/NoteTagsController.ts @@ -1,10 +1,18 @@ import { ElementIds } from '@/Constants/ElementIDs' import { destroyAllObjectProperties } from '@/Utils' -import { ApplicationEvent, ContentType, DeinitSource, PrefKey, SNNote, SNTag, UuidString } from '@standardnotes/snjs' +import { + ApplicationEvent, + ContentType, + InternalEventBus, + PrefKey, + SNNote, + SNTag, + UuidString, +} from '@standardnotes/snjs' import { action, computed, makeObservable, observable } from 'mobx' import { WebApplication } from '../Application/Application' import { AbstractViewController } from './Abstract/AbstractViewController' -import { ViewControllerManager } from '../Services/ViewControllerManager/ViewControllerManager' +import { ItemListController } from './ItemList/ItemListController' export class NoteTagsController extends AbstractViewController { autocompleteInputFocused = false @@ -16,21 +24,19 @@ export class NoteTagsController extends AbstractViewController { tags: SNTag[] = [] tagsContainerMaxWidth: number | 'auto' = 0 addNoteToParentFolders: boolean + private itemListController!: ItemListController - override deinit(source: DeinitSource) { - super.deinit(source) + override deinit() { + super.deinit() ;(this.tags as unknown) = undefined ;(this.autocompleteTagResults as unknown) = undefined + ;(this.itemListController as unknown) = undefined destroyAllObjectProperties(this) } - constructor( - application: WebApplication, - override viewControllerManager: ViewControllerManager, - appEventListeners: (() => void)[], - ) { - super(application, viewControllerManager) + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) makeObservable(this, { autocompleteInputFocused: observable, @@ -55,13 +61,17 @@ export class NoteTagsController extends AbstractViewController { }) this.addNoteToParentFolders = application.getPreference(PrefKey.NoteAddToParentFolders, true) + } - appEventListeners.push( - application.streamItems(ContentType.Tag, () => { + public setServicestPostConstruction(itemListController: ItemListController) { + this.itemListController = itemListController + + this.disposers.push( + this.application.streamItems(ContentType.Tag, () => { this.reloadTagsForCurrentNote() }), - application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { - this.addNoteToParentFolders = application.getPreference(PrefKey.NoteAddToParentFolders, true) + this.application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { + this.addNoteToParentFolders = this.application.getPreference(PrefKey.NoteAddToParentFolders, true) }), ) } @@ -151,7 +161,7 @@ export class NoteTagsController extends AbstractViewController { searchActiveNoteAutocompleteTags(): void { const newResults = this.application.items.searchTags( this.autocompleteSearchQuery, - this.viewControllerManager.contentListController.activeControllerNote, + this.itemListController.activeControllerNote, ) this.setAutocompleteTagResults(newResults) } @@ -161,7 +171,7 @@ export class NoteTagsController extends AbstractViewController { } reloadTagsForCurrentNote(): void { - const activeNote = this.viewControllerManager.contentListController.activeControllerNote + const activeNote = this.itemListController.activeControllerNote if (activeNote) { const tags = this.application.items.getSortedTagsForNote(activeNote) @@ -177,7 +187,7 @@ export class NoteTagsController extends AbstractViewController { } async addTagToActiveNote(tag: SNTag): Promise { - const activeNote = this.viewControllerManager.contentListController.activeControllerNote + const activeNote = this.itemListController.activeControllerNote if (activeNote) { await this.application.items.addTagToNote(activeNote, tag, this.addNoteToParentFolders) @@ -187,7 +197,7 @@ export class NoteTagsController extends AbstractViewController { } async removeTagFromActiveNote(tag: SNTag): Promise { - const activeNote = this.viewControllerManager.contentListController.activeControllerNote + const activeNote = this.itemListController.activeControllerNote if (activeNote) { await this.application.mutator.changeItem(tag, (mutator) => { diff --git a/app/assets/javascripts/Controllers/NotesController.ts b/app/assets/javascripts/Controllers/NotesController.ts index 363c2ea8d..f4af058a2 100644 --- a/app/assets/javascripts/Controllers/NotesController.ts +++ b/app/assets/javascripts/Controllers/NotesController.ts @@ -2,11 +2,15 @@ import { destroyAllObjectProperties } from '@/Utils' import { confirmDialog } from '@/Services/AlertService' import { StringEmptyTrash, Strings, StringUtils } from '@/Constants/Strings' import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants' -import { SNNote, NoteMutator, ContentType, SNTag, DeinitSource, TagMutator } from '@standardnotes/snjs' +import { SNNote, NoteMutator, ContentType, SNTag, TagMutator, InternalEventBus } from '@standardnotes/snjs' import { makeObservable, observable, action, computed, runInAction } from 'mobx' import { WebApplication } from '../Application/Application' -import { ViewControllerManager } from '../Services/ViewControllerManager/ViewControllerManager' import { AbstractViewController } from './Abstract/AbstractViewController' +import { SelectedItemsController } from './SelectedItemsController' +import { ItemListController } from './ItemList/ItemListController' +import { NoteTagsController } from './NoteTagsController' +import { NavigationController } from './Navigation/NavigationController' +import { CrossControllerEvent } from './CrossControllerEvent' export class NotesController extends AbstractViewController { lastSelectedNote: SNNote | undefined @@ -19,22 +23,27 @@ export class NotesController extends AbstractViewController { contextMenuMaxHeight: number | 'auto' = 'auto' showProtectedWarning = false showRevisionHistoryModal = false + private itemListController!: ItemListController - override deinit(source: DeinitSource) { - super.deinit(source) + override deinit() { + super.deinit() ;(this.lastSelectedNote as unknown) = undefined - ;(this.onActiveEditorChanged as unknown) = undefined + ;(this.selectionController as unknown) = undefined + ;(this.noteTagsController as unknown) = undefined + ;(this.navigationController as unknown) = undefined + ;(this.itemListController as unknown) = undefined destroyAllObjectProperties(this) } constructor( application: WebApplication, - public override viewControllerManager: ViewControllerManager, - private onActiveEditorChanged: () => Promise, - appEventListeners: (() => void)[], + private selectionController: SelectedItemsController, + private noteTagsController: NoteTagsController, + private navigationController: NavigationController, + eventBus: InternalEventBus, ) { - super(application, viewControllerManager) + super(application, eventBus) makeObservable(this, { contextMenuOpen: observable, @@ -55,17 +64,21 @@ export class NotesController extends AbstractViewController { setShowRevisionHistoryModal: action, unselectNotes: action, }) + } - appEventListeners.push( - application.streamItems(ContentType.Note, ({ changed, inserted, removed }) => { + public setServicestPostConstruction(itemListController: ItemListController) { + this.itemListController = itemListController + + this.disposers.push( + this.application.streamItems(ContentType.Note, ({ changed, inserted, removed }) => { runInAction(() => { for (const removedNote of removed) { - this.viewControllerManager.selectionController.deselectItem(removedNote) + this.selectionController.deselectItem(removedNote) } for (const note of [...changed, ...inserted]) { - if (this.viewControllerManager.selectionController.isItemSelected(note)) { - this.viewControllerManager.selectionController.updateReferenceOfSelectedItem(note) + if (this.selectionController.isItemSelected(note)) { + this.selectionController.updateReferenceOfSelectedItem(note) } } }) @@ -80,7 +93,7 @@ export class NotesController extends AbstractViewController { for (const selectedId of selectedUuids) { if (!activeNoteUuids.includes(selectedId)) { - this.viewControllerManager.selectionController.deselectItem({ uuid: selectedId }) + this.selectionController.deselectItem({ uuid: selectedId }) } } }), @@ -88,7 +101,7 @@ export class NotesController extends AbstractViewController { } public get selectedNotes(): SNNote[] { - return this.viewControllerManager.selectionController.getSelectedItems(ContentType.Note) + return this.selectionController.getSelectedItems(ContentType.Note) } get firstSelectedNote(): SNNote | undefined { @@ -108,7 +121,7 @@ export class NotesController extends AbstractViewController { } async openNote(noteUuid: string): Promise { - if (this.viewControllerManager.contentListController.activeControllerNote?.uuid === noteUuid) { + if (this.itemListController.activeControllerNote?.uuid === noteUuid) { return } @@ -120,13 +133,13 @@ export class NotesController extends AbstractViewController { await this.application.noteControllerGroup.createNoteController(noteUuid) - this.viewControllerManager.noteTagsController.reloadTagsForCurrentNote() + this.noteTagsController.reloadTagsForCurrentNote() - await this.onActiveEditorChanged() + await this.publishEventSync(CrossControllerEvent.ActiveEditorChanged) } async createNewNoteController(title?: string) { - const selectedTag = this.viewControllerManager.navigationController.selected + const selectedTag = this.navigationController.selected const activeRegularTagUuid = selectedTag && selectedTag instanceof SNTag ? selectedTag.uuid : undefined @@ -262,7 +275,7 @@ export class NotesController extends AbstractViewController { if (permanently) { for (const note of this.getSelectedNotesList()) { await this.application.mutator.deleteItem(note) - this.viewControllerManager.selectionController.deselectItem(note) + this.selectionController.deselectItem(note) } } else { await this.changeSelectedNotes((mutator) => { @@ -294,7 +307,7 @@ export class NotesController extends AbstractViewController { }) runInAction(() => { - this.viewControllerManager.selectionController.setSelectedItems({}) + this.selectionController.setSelectedItems({}) this.contextMenuOpen = false }) } @@ -311,11 +324,11 @@ export class NotesController extends AbstractViewController { } unselectNotes(): void { - this.viewControllerManager.selectionController.setSelectedItems({}) + this.selectionController.setSelectedItems({}) } getSpellcheckStateForNote(note: SNNote) { - return note.spellcheck != undefined ? note.spellcheck : this.viewControllerManager.isGlobalSpellcheckEnabled() + return note.spellcheck != undefined ? note.spellcheck : this.application.isGlobalSpellcheckEnabled() } async toggleGlobalSpellcheckForNote(note: SNNote) { @@ -358,7 +371,7 @@ export class NotesController extends AbstractViewController { isTagInSelectedNotes(tag: SNTag): boolean { const selectedNotes = this.getSelectedNotesList() return selectedNotes.every((note) => - this.viewControllerManager.getItemTags(note).find((noteTag) => noteTag.uuid === tag.uuid), + this.application.getItemTags(note).find((noteTag) => noteTag.uuid === tag.uuid), ) } diff --git a/app/assets/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts b/app/assets/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts index 7ea79a5f7..87b058304 100644 --- a/app/assets/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts +++ b/app/assets/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts @@ -1,4 +1,5 @@ import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowFunctions' +import { InternalEventBus } from '@standardnotes/snjs' import { action, makeObservable, observable } from 'mobx' import { WebApplication } from '../../Application/Application' import { AbstractViewController } from '../Abstract/AbstractViewController' @@ -8,8 +9,8 @@ export class PurchaseFlowController extends AbstractViewController { isOpen = false currentPane = PurchaseFlowPane.CreateAccount - constructor(application: WebApplication) { - super(application) + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) makeObservable(this, { isOpen: observable, diff --git a/app/assets/javascripts/Controllers/SearchOptionsController.ts b/app/assets/javascripts/Controllers/SearchOptionsController.ts index 661cd521e..3f7d7914e 100644 --- a/app/assets/javascripts/Controllers/SearchOptionsController.ts +++ b/app/assets/javascripts/Controllers/SearchOptionsController.ts @@ -1,4 +1,4 @@ -import { ApplicationEvent } from '@standardnotes/snjs' +import { ApplicationEvent, InternalEventBus } from '@standardnotes/snjs' import { makeObservable, observable, action, runInAction } from 'mobx' import { WebApplication } from '../Application/Application' import { AbstractViewController } from './Abstract/AbstractViewController' @@ -8,8 +8,8 @@ export class SearchOptionsController extends AbstractViewController { includeArchived = false includeTrashed = false - constructor(application: WebApplication, appObservers: (() => void)[]) { - super(application) + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) makeObservable(this, { includeProtectedContents: observable, @@ -22,7 +22,7 @@ export class SearchOptionsController extends AbstractViewController { refreshIncludeProtectedContents: action, }) - appObservers.push( + this.disposers.push( this.application.addEventObserver(async () => { this.refreshIncludeProtectedContents() }, ApplicationEvent.UnprotectedSessionBegan), diff --git a/app/assets/javascripts/Controllers/SelectedItemsController.ts b/app/assets/javascripts/Controllers/SelectedItemsController.ts index a46c56afb..599b671fd 100644 --- a/app/assets/javascripts/Controllers/SelectedItemsController.ts +++ b/app/assets/javascripts/Controllers/SelectedItemsController.ts @@ -1,22 +1,35 @@ import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem' -import { ChallengeReason, ContentType, KeyboardModifier, FileItem, SNNote, UuidString } from '@standardnotes/snjs' +import { + ChallengeReason, + ContentType, + KeyboardModifier, + FileItem, + SNNote, + UuidString, + InternalEventBus, +} from '@standardnotes/snjs' import { action, computed, makeObservable, observable, runInAction } from 'mobx' import { WebApplication } from '../Application/Application' import { AbstractViewController } from './Abstract/AbstractViewController' -import { ViewControllerManager } from '../Services/ViewControllerManager/ViewControllerManager' +import { ItemListController } from './ItemList/ItemListController' +import { NotesController } from './NotesController' type SelectedItems = Record export class SelectedItemsController extends AbstractViewController { lastSelectedItem: ListableContentItem | undefined selectedItems: SelectedItems = {} + private itemListController!: ItemListController + private notesController!: NotesController - constructor( - application: WebApplication, - override viewControllerManager: ViewControllerManager, - appObservers: (() => void)[], - ) { - super(application) + override deinit(): void { + super.deinit() + ;(this.itemListController as unknown) = undefined + ;(this.notesController as unknown) = undefined + } + + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) makeObservable(this, { selectedItems: observable, @@ -26,9 +39,14 @@ export class SelectedItemsController extends AbstractViewController { selectItem: action, setSelectedItems: action, }) + } - appObservers.push( - application.streamItems( + public setServicestPostConstruction(itemListController: ItemListController, notesController: NotesController) { + this.itemListController = itemListController + this.notesController = notesController + + this.disposers.push( + this.application.streamItems( [ContentType.Note, ContentType.File], ({ changed, inserted, removed }) => { runInAction(() => { @@ -82,7 +100,7 @@ export class SelectedItemsController extends AbstractViewController { } private selectItemsRange = async (selectedItem: ListableContentItem): Promise => { - const items = this.viewControllerManager.contentListController.renderedItems + const items = this.itemListController.renderedItems const lastSelectedItemIndex = items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid) const selectedItemIndex = items.findIndex((item) => item.uuid == selectedItem.uuid) @@ -171,7 +189,7 @@ export class SelectedItemsController extends AbstractViewController { if (this.selectedItemsCount === 1) { const item = Object.values(this.selectedItems)[0] if (item.content_type === ContentType.Note) { - await this.viewControllerManager.notesController.openNote(item.uuid) + await this.notesController.openNote(item.uuid) } } diff --git a/app/assets/javascripts/Controllers/Subscription/SubscriptionController.ts b/app/assets/javascripts/Controllers/Subscription/SubscriptionController.ts index 82d1caa6a..c10768d85 100644 --- a/app/assets/javascripts/Controllers/Subscription/SubscriptionController.ts +++ b/app/assets/javascripts/Controllers/Subscription/SubscriptionController.ts @@ -3,7 +3,7 @@ import { ApplicationEvent, ClientDisplayableError, convertTimestampToMilliseconds, - DeinitSource, + InternalEventBus, } from '@standardnotes/snjs' import { action, computed, makeObservable, observable } from 'mobx' import { WebApplication } from '../../Application/Application' @@ -15,16 +15,16 @@ export class SubscriptionController extends AbstractViewController { userSubscription: Subscription | undefined = undefined availableSubscriptions: AvailableSubscriptions | undefined = undefined - override deinit(source: DeinitSource) { - super.deinit(source) + override deinit() { + super.deinit() ;(this.userSubscription as unknown) = undefined ;(this.availableSubscriptions as unknown) = undefined destroyAllObjectProperties(this) } - constructor(application: WebApplication, appObservers: (() => void)[]) { - super(application) + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) makeObservable(this, { userSubscription: observable, @@ -39,15 +39,21 @@ export class SubscriptionController extends AbstractViewController { setAvailableSubscriptions: action, }) - appObservers.push( + this.disposers.push( application.addEventObserver(async () => { if (application.hasAccount()) { this.getSubscriptionInfo().catch(console.error) } }, ApplicationEvent.Launched), + ) + + this.disposers.push( application.addEventObserver(async () => { this.getSubscriptionInfo().catch(console.error) }, ApplicationEvent.SignedIn), + ) + + this.disposers.push( application.addEventObserver(async () => { this.getSubscriptionInfo().catch(console.error) }, ApplicationEvent.UserRolesChanged), diff --git a/app/assets/javascripts/Services/DesktopManager.ts b/app/assets/javascripts/Services/DesktopManager.ts index 20a991513..4126aad15 100644 --- a/app/assets/javascripts/Services/DesktopManager.ts +++ b/app/assets/javascripts/Services/DesktopManager.ts @@ -13,7 +13,8 @@ import { DesktopClientRequiresWebMethods, DesktopDeviceInterface, } from '@standardnotes/snjs' -import { WebAppEvent, WebApplication } from '@/Application/Application' +import { WebApplication } from '@/Application/Application' +import { WebAppEvent } from '@/Application/WebAppEvent' export class DesktopManager extends ApplicationService @@ -106,11 +107,11 @@ export class DesktopManager } windowGainedFocus(): void { - this.webApplication.notifyWebEvent(WebAppEvent.DesktopWindowGainedFocus) + this.webApplication.notifyWebEvent(WebAppEvent.WindowDidFocus) } windowLostFocus(): void { - this.webApplication.notifyWebEvent(WebAppEvent.DesktopWindowLostFocus) + this.webApplication.notifyWebEvent(WebAppEvent.WindowDidBlur) } async onComponentInstallationComplete(componentData: DecryptedTransferPayload, error: unknown) { @@ -155,10 +156,10 @@ export class DesktopManager } didBeginBackup() { - this.webApplication.getViewControllerManager().beganBackupDownload() + this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload) } didFinishBackup(success: boolean) { - this.webApplication.getViewControllerManager().endedBackupDownload(success) + this.webApplication.notifyWebEvent(WebAppEvent.EndedBackupDownload, { success }) } } diff --git a/app/assets/javascripts/Services/ViewControllerManager.ts b/app/assets/javascripts/Services/ViewControllerManager.ts new file mode 100644 index 000000000..06a7d00fe --- /dev/null +++ b/app/assets/javascripts/Services/ViewControllerManager.ts @@ -0,0 +1,204 @@ +import { storage, StorageKey } from '@/Services/LocalStorage' +import { WebApplication } from '@/Application/Application' +import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' +import { destroyAllObjectProperties } from '@/Utils' +import { ApplicationEvent, DeinitSource, WebOrDesktopDeviceInterface, InternalEventBus } from '@standardnotes/snjs' +import { action, makeObservable, observable } from 'mobx' +import { ActionsMenuController } from '../Controllers/ActionsMenuController' +import { FeaturesController } from '../Controllers/FeaturesController' +import { FilesController } from '../Controllers/FilesController' +import { NotesController } from '../Controllers/NotesController' +import { ItemListController } from '../Controllers/ItemList/ItemListController' +import { NoteTagsController } from '../Controllers/NoteTagsController' +import { NoAccountWarningController } from '../Controllers/NoAccountWarningController' +import { PreferencesController } from '../Controllers/PreferencesController' +import { PurchaseFlowController } from '../Controllers/PurchaseFlow/PurchaseFlowController' +import { QuickSettingsController } from '../Controllers/QuickSettingsController' +import { SearchOptionsController } from '../Controllers/SearchOptionsController' +import { SubscriptionController } from '../Controllers/Subscription/SubscriptionController' +import { SyncStatusController } from '../Controllers/SyncStatusController' +import { NavigationController } from '../Controllers/Navigation/NavigationController' +import { FilePreviewModalController } from '../Controllers/FilePreviewModalController' +import { SelectedItemsController } from '../Controllers/SelectedItemsController' + +export class ViewControllerManager { + readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures + + private unsubAppEventObserver!: () => void + showBetaWarning: boolean + public dealloced = false + + readonly accountMenuController: AccountMenuController + readonly actionsMenuController = new ActionsMenuController() + readonly featuresController: FeaturesController + readonly filePreviewModalController = new FilePreviewModalController() + readonly filesController: FilesController + readonly noAccountWarningController: NoAccountWarningController + readonly notesController: NotesController + readonly itemListController: ItemListController + readonly noteTagsController: NoteTagsController + readonly preferencesController = new PreferencesController() + readonly purchaseFlowController: PurchaseFlowController + readonly quickSettingsMenuController = new QuickSettingsController() + readonly searchOptionsController: SearchOptionsController + readonly subscriptionController: SubscriptionController + readonly syncStatusController = new SyncStatusController() + readonly navigationController: NavigationController + readonly selectionController: SelectedItemsController + + public isSessionsModalVisible = false + + private appEventObserverRemovers: (() => void)[] = [] + private eventBus: InternalEventBus + + constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) { + this.eventBus = new InternalEventBus() + + this.selectionController = new SelectedItemsController(application, this.eventBus) + + this.noteTagsController = new NoteTagsController(application, this.eventBus) + + this.featuresController = new FeaturesController(application, this.eventBus) + + this.navigationController = new NavigationController(application, this.featuresController, this.eventBus) + + this.notesController = new NotesController( + application, + this.selectionController, + this.noteTagsController, + this.navigationController, + this.eventBus, + ) + + this.searchOptionsController = new SearchOptionsController(application, this.eventBus) + + this.itemListController = new ItemListController( + application, + this.navigationController, + this.searchOptionsController, + this.selectionController, + this.notesController, + this.noteTagsController, + this.eventBus, + ) + + this.notesController.setServicestPostConstruction(this.itemListController) + this.noteTagsController.setServicestPostConstruction(this.itemListController) + this.selectionController.setServicestPostConstruction(this.itemListController, this.notesController) + + this.noAccountWarningController = new NoAccountWarningController(application, this.eventBus) + + this.accountMenuController = new AccountMenuController(application, this.eventBus) + + this.subscriptionController = new SubscriptionController(application, this.eventBus) + + this.purchaseFlowController = new PurchaseFlowController(application, this.eventBus) + + this.filesController = new FilesController( + application, + this.notesController, + this.selectionController, + this.filePreviewModalController, + this.eventBus, + ) + + this.addAppEventObserver() + + if (this.device.appVersion.includes('-beta')) { + this.showBetaWarning = storage.get(StorageKey.ShowBetaWarning) ?? true + } else { + this.showBetaWarning = false + } + + makeObservable(this, { + showBetaWarning: observable, + isSessionsModalVisible: observable, + preferencesController: observable, + + openSessionsModal: action, + closeSessionsModal: action, + }) + } + + deinit(source: DeinitSource): void { + this.dealloced = true + ;(this.application as unknown) = undefined + + if (source === DeinitSource.SignOut) { + storage.remove(StorageKey.ShowBetaWarning) + this.noAccountWarningController.reset() + } + + this.unsubAppEventObserver?.() + ;(this.unsubAppEventObserver as unknown) = undefined + + this.appEventObserverRemovers.forEach((remover) => remover()) + this.appEventObserverRemovers.length = 0 + ;(this.device as unknown) = undefined + ;(this.filePreviewModalController as unknown) = undefined + ;(this.preferencesController as unknown) = undefined + ;(this.quickSettingsMenuController as unknown) = undefined + ;(this.syncStatusController as unknown) = undefined + + this.actionsMenuController.reset() + ;(this.actionsMenuController as unknown) = undefined + + this.featuresController.deinit() + ;(this.featuresController as unknown) = undefined + + this.accountMenuController.deinit() + ;(this.accountMenuController as unknown) = undefined + + this.filesController.deinit() + ;(this.filesController as unknown) = undefined + + this.noAccountWarningController.deinit() + ;(this.noAccountWarningController as unknown) = undefined + + this.notesController.deinit() + ;(this.notesController as unknown) = undefined + + this.itemListController.deinit() + ;(this.itemListController as unknown) = undefined + + this.noteTagsController.deinit() + ;(this.noteTagsController as unknown) = undefined + + this.purchaseFlowController.deinit() + ;(this.purchaseFlowController as unknown) = undefined + + this.searchOptionsController.deinit() + ;(this.searchOptionsController as unknown) = undefined + + this.subscriptionController.deinit() + ;(this.subscriptionController as unknown) = undefined + + this.navigationController.deinit() + ;(this.navigationController as unknown) = undefined + + destroyAllObjectProperties(this) + } + + openSessionsModal(): void { + this.isSessionsModalVisible = true + } + + closeSessionsModal(): void { + this.isSessionsModalVisible = false + } + + addAppEventObserver() { + this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => { + switch (eventName) { + case ApplicationEvent.Launched: + if (window.location.search.includes('purchase=true')) { + this.purchaseFlowController.openPurchaseFlow() + } + break + case ApplicationEvent.SyncStatusChanged: + this.syncStatusController.update(this.application.sync.getSyncStatus()) + break + } + }) + } +} diff --git a/app/assets/javascripts/Services/ViewControllerManager/ViewControllerManager.ts b/app/assets/javascripts/Services/ViewControllerManager/ViewControllerManager.ts deleted file mode 100644 index 2621abaae..000000000 --- a/app/assets/javascripts/Services/ViewControllerManager/ViewControllerManager.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { storage, StorageKey } from '@/Services/LocalStorage' -import { WebApplication, WebAppEvent } from '@/Application/Application' -import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' -import { destroyAllObjectProperties, isDesktopApplication } from '@/Utils' -import { - ApplicationEvent, - ContentType, - DeinitSource, - PrefKey, - SNTag, - removeFromArray, - WebOrDesktopDeviceInterface, -} from '@standardnotes/snjs' -import { action, makeObservable, observable } from 'mobx' -import { ActionsMenuController } from '../../Controllers/ActionsMenuController' -import { FeaturesController } from '../../Controllers/FeaturesController' -import { FilesController } from '../../Controllers/FilesController' -import { NotesController } from '../../Controllers/NotesController' -import { ItemListController } from '../../Controllers/ItemList/ItemListController' -import { NoteTagsController } from '../../Controllers/NoteTagsController' -import { NoAccountWarningController } from '../../Controllers/NoAccountWarningController' -import { PreferencesController } from '../../Controllers/PreferencesController' -import { PurchaseFlowController } from '../../Controllers/PurchaseFlow/PurchaseFlowController' -import { QuickSettingsController } from '../../Controllers/QuickSettingsController' -import { SearchOptionsController } from '../../Controllers/SearchOptionsController' -import { SubscriptionController } from '../../Controllers/Subscription/SubscriptionController' -import { SyncStatusController } from '../../Controllers/SyncStatusController' -import { TagsController } from '../../Controllers/Navigation/TagsController' -import { FilePreviewModalController } from '../../Controllers/FilePreviewModalController' -import { SelectedItemsController } from '../../Controllers/SelectedItemsController' -import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem' -import { ViewControllerManagerEvent } from './ViewControllerManagerEvent' -import { EditorEventSource } from '../../Types/EditorEventSource' -import { PanelResizedData } from '../../Types/PanelResizedData' - -type ObserverCallback = (event: ViewControllerManagerEvent, data?: unknown) => Promise - -export class ViewControllerManager { - readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures - - observers: ObserverCallback[] = [] - locked = true - unsubAppEventObserver!: () => void - webAppEventDisposer?: () => void - onVisibilityChange: () => void - showBetaWarning: boolean - dealloced = false - - readonly accountMenuController: AccountMenuController - readonly actionsMenuController = new ActionsMenuController() - readonly featuresController: FeaturesController - readonly filePreviewModalController = new FilePreviewModalController() - readonly filesController: FilesController - readonly noAccountWarningController: NoAccountWarningController - readonly notesController: NotesController - readonly contentListController: ItemListController - readonly noteTagsController: NoteTagsController - readonly preferencesController = new PreferencesController() - readonly purchaseFlowController: PurchaseFlowController - readonly quickSettingsMenuController = new QuickSettingsController() - readonly searchOptionsController: SearchOptionsController - readonly subscriptionController: SubscriptionController - readonly syncStatusController = new SyncStatusController() - readonly navigationController: TagsController - readonly selectionController: SelectedItemsController - - isSessionsModalVisible = false - - private appEventObserverRemovers: (() => void)[] = [] - - constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) { - this.selectionController = new SelectedItemsController(application, this, this.appEventObserverRemovers) - this.notesController = new NotesController( - application, - this, - async () => { - await this.notifyEvent(ViewControllerManagerEvent.ActiveEditorChanged) - }, - this.appEventObserverRemovers, - ) - this.featuresController = new FeaturesController(application, this.appEventObserverRemovers) - this.navigationController = new TagsController( - application, - this, - this.appEventObserverRemovers, - this.featuresController, - ) - this.searchOptionsController = new SearchOptionsController(application, this.appEventObserverRemovers) - this.contentListController = new ItemListController(application, this, this.appEventObserverRemovers) - this.noteTagsController = new NoteTagsController(application, this, this.appEventObserverRemovers) - this.noAccountWarningController = new NoAccountWarningController(application, this.appEventObserverRemovers) - this.accountMenuController = new AccountMenuController(application, this.appEventObserverRemovers) - this.subscriptionController = new SubscriptionController(application, this.appEventObserverRemovers) - this.purchaseFlowController = new PurchaseFlowController(application) - this.filesController = new FilesController(application, this, this.appEventObserverRemovers) - this.addAppEventObserver() - this.onVisibilityChange = () => { - const visible = document.visibilityState === 'visible' - const event = visible ? ViewControllerManagerEvent.WindowDidFocus : ViewControllerManagerEvent.WindowDidBlur - this.notifyEvent(event).catch(console.error) - } - this.registerVisibilityObservers() - - if (this.device.appVersion.includes('-beta')) { - this.showBetaWarning = storage.get(StorageKey.ShowBetaWarning) ?? true - } else { - this.showBetaWarning = false - } - - makeObservable(this, { - showBetaWarning: observable, - isSessionsModalVisible: observable, - preferencesController: observable, - - enableBetaWarning: action, - disableBetaWarning: action, - openSessionsModal: action, - closeSessionsModal: action, - }) - } - - deinit(source: DeinitSource): void { - this.dealloced = true - ;(this.application as unknown) = undefined - - if (source === DeinitSource.SignOut) { - storage.remove(StorageKey.ShowBetaWarning) - this.noAccountWarningController.reset() - } - - this.unsubAppEventObserver?.() - ;(this.unsubAppEventObserver as unknown) = undefined - this.observers.length = 0 - - this.appEventObserverRemovers.forEach((remover) => remover()) - this.appEventObserverRemovers.length = 0 - ;(this.device as unknown) = undefined - - this.webAppEventDisposer?.() - this.webAppEventDisposer = undefined - ;(this.filePreviewModalController as unknown) = undefined - ;(this.preferencesController as unknown) = undefined - ;(this.quickSettingsMenuController as unknown) = undefined - ;(this.syncStatusController as unknown) = undefined - - this.actionsMenuController.reset() - ;(this.actionsMenuController as unknown) = undefined - - this.featuresController.deinit(source) - ;(this.featuresController as unknown) = undefined - - this.accountMenuController.deinit(source) - ;(this.accountMenuController as unknown) = undefined - - this.filesController.deinit(source) - ;(this.filesController as unknown) = undefined - - this.noAccountWarningController.deinit(source) - ;(this.noAccountWarningController as unknown) = undefined - - this.notesController.deinit(source) - ;(this.notesController as unknown) = undefined - - this.contentListController.deinit(source) - ;(this.contentListController as unknown) = undefined - - this.noteTagsController.deinit(source) - ;(this.noteTagsController as unknown) = undefined - - this.purchaseFlowController.deinit(source) - ;(this.purchaseFlowController as unknown) = undefined - - this.searchOptionsController.deinit(source) - ;(this.searchOptionsController as unknown) = undefined - - this.subscriptionController.deinit(source) - ;(this.subscriptionController as unknown) = undefined - - this.navigationController.deinit(source) - ;(this.navigationController as unknown) = undefined - - document.removeEventListener('visibilitychange', this.onVisibilityChange) - ;(this.onVisibilityChange as unknown) = undefined - - destroyAllObjectProperties(this) - } - - openSessionsModal(): void { - this.isSessionsModalVisible = true - } - - closeSessionsModal(): void { - this.isSessionsModalVisible = false - } - - disableBetaWarning() { - this.showBetaWarning = false - storage.set(StorageKey.ShowBetaWarning, false) - } - - enableBetaWarning() { - this.showBetaWarning = true - storage.set(StorageKey.ShowBetaWarning, true) - } - - public get version(): string { - return this.device.appVersion - } - - isGlobalSpellcheckEnabled(): boolean { - return this.application.getPreference(PrefKey.EditorSpellcheck, true) - } - - async toggleGlobalSpellcheck() { - const currentValue = this.isGlobalSpellcheckEnabled() - return this.application.setPreference(PrefKey.EditorSpellcheck, !currentValue) - } - - addAppEventObserver() { - this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => { - switch (eventName) { - case ApplicationEvent.Started: - this.locked = true - break - case ApplicationEvent.Launched: - this.locked = false - if (window.location.search.includes('purchase=true')) { - this.purchaseFlowController.openPurchaseFlow() - } - break - case ApplicationEvent.SyncStatusChanged: - this.syncStatusController.update(this.application.sync.getSyncStatus()) - break - } - }) - } - - isLocked() { - return this.locked - } - - registerVisibilityObservers() { - if (isDesktopApplication()) { - this.webAppEventDisposer = this.application.addWebEventObserver((event) => { - if (event === WebAppEvent.DesktopWindowGainedFocus) { - this.notifyEvent(ViewControllerManagerEvent.WindowDidFocus).catch(console.error) - } else if (event === WebAppEvent.DesktopWindowLostFocus) { - this.notifyEvent(ViewControllerManagerEvent.WindowDidBlur).catch(console.error) - } - }) - } else { - /* Tab visibility listener, web only */ - document.addEventListener('visibilitychange', this.onVisibilityChange) - } - } - - addObserver(callback: ObserverCallback): () => void { - this.observers.push(callback) - - const thislessObservers = this.observers - return () => { - removeFromArray(thislessObservers, callback) - } - } - - async notifyEvent(eventName: ViewControllerManagerEvent, data?: unknown) { - for (const callback of this.observers) { - await callback(eventName, data) - } - } - - /** Returns the tags that are referncing this note */ - public getItemTags(item: ListableContentItem) { - return this.application.items.itemsReferencingItem(item).filter((ref) => { - return ref.content_type === ContentType.Tag - }) as SNTag[] - } - - panelDidResize(name: string, collapsed: boolean) { - const data: PanelResizedData = { - panel: name, - collapsed: collapsed, - } - this.notifyEvent(ViewControllerManagerEvent.PanelResized, data).catch(console.error) - } - - editorDidFocus(eventSource: EditorEventSource) { - this.notifyEvent(ViewControllerManagerEvent.EditorFocused, { eventSource: eventSource }).catch(console.error) - } - - beganBackupDownload() { - this.notifyEvent(ViewControllerManagerEvent.BeganBackupDownload).catch(console.error) - } - - endedBackupDownload(success: boolean) { - this.notifyEvent(ViewControllerManagerEvent.EndedBackupDownload, { success: success }).catch(console.error) - } -} diff --git a/app/assets/javascripts/Services/ViewControllerManager/ViewControllerManagerEvent.ts b/app/assets/javascripts/Services/ViewControllerManager/ViewControllerManagerEvent.ts deleted file mode 100644 index 540b76f3b..000000000 --- a/app/assets/javascripts/Services/ViewControllerManager/ViewControllerManagerEvent.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum ViewControllerManagerEvent { - TagChanged, - ActiveEditorChanged, - PanelResized, - EditorFocused, - BeganBackupDownload, - EndedBackupDownload, - WindowDidFocus, - WindowDidBlur, -} diff --git a/app/assets/javascripts/Services/ViewControllerManager/index.ts b/app/assets/javascripts/Services/ViewControllerManager/index.ts deleted file mode 100644 index d3517e9a1..000000000 --- a/app/assets/javascripts/Services/ViewControllerManager/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ViewControllerManager' -export * from './ViewControllerManagerEvent' diff --git a/app/assets/javascripts/Types/Disposer.ts b/app/assets/javascripts/Types/Disposer.ts new file mode 100644 index 000000000..7bb1de228 --- /dev/null +++ b/app/assets/javascripts/Types/Disposer.ts @@ -0,0 +1 @@ +export type Disposer = () => void diff --git a/package.json b/package.json index 35cd85db2..0b48aa458 100644 --- a/package.json +++ b/package.json @@ -73,10 +73,10 @@ "@standardnotes/components": "1.8.2", "@standardnotes/filepicker": "1.16.2", "@standardnotes/icons": "^1.1.8", - "@standardnotes/services": "^1.13.3", "@standardnotes/sncrypto-web": "1.10.1", "@standardnotes/snjs": "^2.114.5", "@standardnotes/stylekit": "5.29.3", + "@standardnotes/services": "^1.13.6", "@zip.js/zip.js": "^2.4.10", "mobx": "^6.5.0", "mobx-react-lite": "^3.3.0", diff --git a/yarn.lock b/yarn.lock index 5274ec156..3b1fc927e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2366,6 +2366,14 @@ "@standardnotes/common" "^1.22.0" jsonwebtoken "^8.5.1" +"@standardnotes/auth@^3.19.2": + version "3.19.2" + resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.19.2.tgz#a2758cdde588190eebd30e567fe7756c6823adcd" + integrity sha512-m7MvSN2BHHh8+FZ3tUe6IpoPGzu/I1lmQF8snlYfuBmSxVdwVLTYhIbFAjfi4PST/Rx3FXnaoMnfJSR0k+OmWw== + dependencies: + "@standardnotes/common" "^1.22.0" + jsonwebtoken "^8.5.1" + "@standardnotes/common@^1.22.0": version "1.22.0" resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.22.0.tgz#397604fb4b92901bac276940a2647509b70a7ad2" @@ -2420,6 +2428,14 @@ "@standardnotes/auth" "^3.19.1" "@standardnotes/common" "^1.22.0" +"@standardnotes/features@^1.44.6": + version "1.44.6" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.44.6.tgz#1a7872a7e79026a553d3670f8610b2b79c1f5fa4" + integrity sha512-iP0oR4bb16Rx0kSspl0R8rSKY38hF59ExaoMIREf0MGH8WLjwDJyILafGfhxv8NjMeRtpIIXKbK+TokM6A5ZXg== + dependencies: + "@standardnotes/auth" "^3.19.2" + "@standardnotes/common" "^1.22.0" + "@standardnotes/filepicker@1.16.2": version "1.16.2" resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.16.2.tgz#d6fda94b5578f30e6b4f792c874e3eb11ce58453" @@ -2473,6 +2489,15 @@ "@standardnotes/responses" "^1.6.28" "@standardnotes/utils" "^1.6.10" +"@standardnotes/models@^1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@standardnotes/models/-/models-1.11.1.tgz#d622b7cffd7ee4faebcd564d7ae880b372ec7be8" + integrity sha512-XKXoV8Pi5iuzrjUGWs4grvxn3m2BQyt49Br+euToOkgvZvW5HIiaCGLwAtvP5S3d3ecgD5EvbLzGJIFEs5F4rw== + dependencies: + "@standardnotes/features" "^1.44.6" + "@standardnotes/responses" "^1.6.29" + "@standardnotes/utils" "^1.6.10" + "@standardnotes/responses@^1.6.27": version "1.6.27" resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.27.tgz#3a440090e5cee09f2980df5cc57a60f76adff735" @@ -2491,6 +2516,15 @@ "@standardnotes/common" "^1.22.0" "@standardnotes/features" "^1.44.5" +"@standardnotes/responses@^1.6.29": + version "1.6.29" + resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.29.tgz#fac5875bb84e382d7b1acf14c0af77876fe5c41d" + integrity sha512-BWrkR6gIWD+dC9/a+ii/pDzWtIlAsgZWICTwKZ34jBgjPSO1svewcHzXwBXILKZd6JvrUf6pglmSt3I9fmsHaQ== + dependencies: + "@standardnotes/auth" "^3.19.2" + "@standardnotes/common" "^1.22.0" + "@standardnotes/features" "^1.44.6" + "@standardnotes/services@^1.13.3": version "1.13.3" resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.13.3.tgz#b857a6ed42b15d40c2e9d08b41739f5fb6b0d3e0" @@ -2513,6 +2547,17 @@ "@standardnotes/responses" "^1.6.28" "@standardnotes/utils" "^1.6.10" +"@standardnotes/services@^1.13.6": + version "1.13.6" + resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.13.6.tgz#9ab4d0c3ed0ad3693ba54ac360f4fdaad08f7410" + integrity sha512-Vt/hptzJK4D6qUPp/cdhOLlRT57uf3CDjDUwyfnELXDyU8mFTVvS/M1deD1PecNWodsfLH7aDWeMWTKApP6LKg== + dependencies: + "@standardnotes/auth" "^3.19.2" + "@standardnotes/common" "^1.22.0" + "@standardnotes/models" "^1.11.1" + "@standardnotes/responses" "^1.6.29" + "@standardnotes/utils" "^1.6.10" + "@standardnotes/settings@^1.14.3": version "1.14.3" resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.14.3.tgz#021085e8c383a9893a2c49daa74cc0754ccd67b5"