From 2b6abeebfcb9ab99ef853020f79c5a4b6b6200f9 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 15 Sep 2020 10:55:32 -0500 Subject: [PATCH] feat: snjs app groups (#468) * feat: snjs app groups * fix: update snjs version to point to wip commit * wip: account switcher * feat: rename lock manager to auto lock service * fix: more relevant sign out copy * chore(deps): update snjs * fix: use setTimeout instead of setImmediate * feat: make account switcher expiremental feature * chore(deps): upgrade snjs --- app/assets/javascripts/app.ts | 2 + app/assets/javascripts/database.ts | 23 ++- .../directives/functional/selectOnFocus.ts | 4 +- .../directives/views/accountMenu.ts | 9 +- .../directives/views/editorMenu.ts | 2 +- .../directives/views/historyMenu.ts | 2 +- .../views/privilegesManagementModal.ts | 4 +- .../directives/views/revisionPreviewModal.ts | 2 +- .../{lockManager.ts => autolock_service.ts} | 25 ++- app/assets/javascripts/services/index.ts | 2 +- .../javascripts/services/nativeExtManager.ts | 4 +- app/assets/javascripts/strings.ts | 2 +- app/assets/javascripts/types.ts | 4 + .../javascripts/ui_models/application.ts | 43 +++-- .../ui_models/application_group.ts | 85 +++------ .../javascripts/ui_models/component_group.ts | 6 +- .../views/abstract/pure_view_ctrl.ts | 10 +- .../account_switcher/account-switcher.pug | 32 ++++ .../account_switcher/account_switcher.ts | 105 +++++++++++ .../views/application/application-view.pug | 13 +- .../views/application/application_view.ts | 2 +- .../application-group-view.pug | 1 + .../application_group_view.ts | 9 +- .../javascripts/views/footer/footer-view.pug | 46 +++-- .../javascripts/views/footer/footer_view.ts | 88 +++++---- .../javascripts/views/notes/notes_view.ts | 2 +- .../javascripts/web_device_interface.ts | 67 ++++--- app/assets/stylesheets/_footer.scss | 18 +- app/assets/stylesheets/_ionicons.scss | 26 ++- app/assets/stylesheets/_modals.scss | 27 ++- .../templates/directives/account-menu.pug | 170 +++++++++--------- .../app/assets/javascripts/database.d.ts | 7 +- .../javascripts/services/alertService.d.ts | 2 +- .../services/autolock_service.d.ts | 27 +++ .../assets/javascripts/services/index.d.ts | 2 +- .../services/nativeExtManager.d.ts | 2 +- .../app/assets/javascripts/strings.d.ts | 3 +- dist/@types/app/assets/javascripts/types.d.ts | 3 + .../javascripts/ui_models/application.d.ts | 14 +- .../ui_models/application_group.d.ts | 24 +-- .../ui_models/component_group.d.ts | 2 +- .../account_switcher/account_switcher.d.ts | 4 + .../javascripts/views/notes/note_utils.d.ts | 4 +- .../javascripts/web_device_interface.d.ts | 25 ++- package-lock.json | 6 +- package.json | 5 +- 46 files changed, 590 insertions(+), 375 deletions(-) rename app/assets/javascripts/services/{lockManager.ts => autolock_service.ts} (91%) create mode 100644 app/assets/javascripts/views/account_switcher/account-switcher.pug create mode 100644 app/assets/javascripts/views/account_switcher/account_switcher.ts create mode 100644 dist/@types/app/assets/javascripts/services/autolock_service.d.ts create mode 100644 dist/@types/app/assets/javascripts/views/account_switcher/account_switcher.d.ts diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 33f46181e..f48eac800 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -7,6 +7,7 @@ import angular from 'angular'; import { configRoutes } from './routes'; import { ApplicationGroup } from './ui_models/application_group'; +import { AccountSwitcher } from './views/account_switcher/account_switcher'; import { ApplicationGroupView, @@ -104,6 +105,7 @@ function startApplication( angular .module('app') .directive('accountMenu', () => new AccountMenu()) + .directive('accountSwitcher', () => new AccountSwitcher()) .directive('actionsMenu', () => new ActionsMenu()) .directive('challengeModal', () => new ChallengeModal()) .directive('componentModal', () => new ComponentModal()) diff --git a/app/assets/javascripts/database.ts b/app/assets/javascripts/database.ts index 4c0980bdc..c29449110 100644 --- a/app/assets/javascripts/database.ts +++ b/app/assets/javascripts/database.ts @@ -1,6 +1,5 @@ -import { SNAlertService } from "@node_modules/snjs/dist/@types"; +import { SNAlertService } from "snjs/dist/@types"; -const DB_NAME = 'standardnotes'; const STORE_NAME = 'items'; const READ_WRITE = 'readwrite'; @@ -17,18 +16,18 @@ const DB_DELETION_BLOCKED = const QUOTE_EXCEEDED_ERROR = 'QuotaExceededError'; export class Database { - private locked = true - private alertService?: SNAlertService private db?: IDBDatabase - public deinit() { - this.alertService = undefined; - this.db = undefined; + constructor( + public databaseName: string, + private alertService: SNAlertService) { + } - public setAlertService(alertService: SNAlertService) { - this.alertService = alertService; + public deinit() { + (this.alertService as any) = undefined; + this.db = undefined; } /** @@ -41,7 +40,7 @@ export class Database { /** * Opens the database natively, or returns the existing database object if already opened. * @param onNewDatabase - Callback to invoke when a database has been created - * as part of the open process. This can happen on new application sessions, or if the + * as part of the open process. This can happen on new application sessions, or if the * browser deleted the database without the user being aware. */ public async openDatabase(onNewDatabase?: () => void): Promise { @@ -51,7 +50,7 @@ export class Database { if (this.db) { return this.db; } - const request = window.indexedDB.open(DB_NAME, 1); + const request = window.indexedDB.open(this.databaseName, 1); return new Promise((resolve, reject) => { request.onerror = (event) => { const target = event!.target! as any; @@ -181,7 +180,7 @@ export class Database { } public async clearAllPayloads(): Promise { - const deleteRequest = window.indexedDB.deleteDatabase(DB_NAME); + const deleteRequest = window.indexedDB.deleteDatabase(this.databaseName); return new Promise((resolve, reject) => { deleteRequest.onerror = () => { reject(Error('Error deleting database.')); diff --git a/app/assets/javascripts/directives/functional/selectOnFocus.ts b/app/assets/javascripts/directives/functional/selectOnFocus.ts index 755c1331a..a35b17c0b 100644 --- a/app/assets/javascripts/directives/functional/selectOnFocus.ts +++ b/app/assets/javascripts/directives/functional/selectOnFocus.ts @@ -7,9 +7,9 @@ export function selectOnFocus($window: ng.IWindowService) { if (!$window.getSelection()!.toString()) { const input = element[0] as HTMLInputElement; /** Allow text to populate */ - setImmediate(() => { + setTimeout(() => { input.setSelectionRange(0, input.value.length); - }) + }, 0); } }); } diff --git a/app/assets/javascripts/directives/views/accountMenu.ts b/app/assets/javascripts/directives/views/accountMenu.ts index 955c3b4cd..5c0fc6ec0 100644 --- a/app/assets/javascripts/directives/views/accountMenu.ts +++ b/app/assets/javascripts/directives/views/accountMenu.ts @@ -70,7 +70,8 @@ type AccountMenuState = { class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> { public appVersion: string - private syncStatus?: SyncOpStatus + /** @template */ + syncStatus?: SyncOpStatus private closeFunction?: () => void /* @ngInject */ @@ -86,7 +87,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> { getInitialState() { return { appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion), - passcodeAutoLockOptions: this.application!.getLockService().getAutoLockIntervalOptions(), + passcodeAutoLockOptions: this.application!.getAutolockService().getAutoLockIntervalOptions(), user: this.application!.getUser(), formData: { mergeLocal: true, @@ -463,7 +464,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> { } async reloadAutoLockInterval() { - const interval = await this.application!.getLockService().getAutoLockInterval(); + const interval = await this.application!.getAutolockService().getAutoLockInterval(); this.setState({ selectedAutoLockInterval: interval }); @@ -471,7 +472,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> { async selectAutoLockInterval(interval: number) { const run = async () => { - await this.application!.getLockService().setAutoLockInterval(interval); + await this.application!.getAutolockService().setAutoLockInterval(interval); this.reloadAutoLockInterval(); }; const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege( diff --git a/app/assets/javascripts/directives/views/editorMenu.ts b/app/assets/javascripts/directives/views/editorMenu.ts index e9b27536d..863b826e3 100644 --- a/app/assets/javascripts/directives/views/editorMenu.ts +++ b/app/assets/javascripts/directives/views/editorMenu.ts @@ -4,7 +4,7 @@ import { SNComponent, SNItem, ComponentArea } from 'snjs'; import { isDesktopApplication } from '@/utils'; import template from '%/directives/editor-menu.pug'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; -import { ComponentMutator } from '@node_modules/snjs/dist/@types/models'; +import { ComponentMutator } from 'snjs/dist/@types/models'; interface EditorMenuScope { callback: (component: SNComponent) => void diff --git a/app/assets/javascripts/directives/views/historyMenu.ts b/app/assets/javascripts/directives/views/historyMenu.ts index 0790188e5..70e355443 100644 --- a/app/assets/javascripts/directives/views/historyMenu.ts +++ b/app/assets/javascripts/directives/views/historyMenu.ts @@ -1,7 +1,7 @@ import { WebDirective } from '../../types'; import { WebApplication } from '@/ui_models/application'; import template from '%/directives/history-menu.pug'; -import { SNItem, ItemHistoryEntry } from '@node_modules/snjs/dist/@types'; +import { SNItem, ItemHistoryEntry } from 'snjs/dist/@types'; import { PureViewCtrl } from '@/views'; import { ItemSessionHistory } from 'snjs/dist/@types/services/history/session/item_session_history'; import { RemoteHistoryList, RemoteHistoryListEntry } from 'snjs/dist/@types/services/history/history_manager'; diff --git a/app/assets/javascripts/directives/views/privilegesManagementModal.ts b/app/assets/javascripts/directives/views/privilegesManagementModal.ts index 626869d5c..837d4e3b3 100644 --- a/app/assets/javascripts/directives/views/privilegesManagementModal.ts +++ b/app/assets/javascripts/directives/views/privilegesManagementModal.ts @@ -3,7 +3,7 @@ import { WebApplication } from '@/ui_models/application'; import template from '%/directives/privileges-management-modal.pug'; import { PrivilegeCredential, ProtectedAction, SNPrivileges, PrivilegeSessionLength } from 'snjs'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; -import { PrivilegeMutator } from '@node_modules/snjs/dist/@types/models'; +import { PrivilegeMutator } from 'snjs/dist/@types/models'; type DisplayInfo = { label: string @@ -32,7 +32,7 @@ class PrivilegesManagementModalCtrl extends PureViewCtrl { super($timeout); this.$element = $element; } - + async onAppLaunch() { super.onAppLaunch(); this.hasPasscode = this.application.hasPasscode(); diff --git a/app/assets/javascripts/directives/views/revisionPreviewModal.ts b/app/assets/javascripts/directives/views/revisionPreviewModal.ts index 4ae5e0357..5bda26156 100644 --- a/app/assets/javascripts/directives/views/revisionPreviewModal.ts +++ b/app/assets/javascripts/directives/views/revisionPreviewModal.ts @@ -8,7 +8,7 @@ import { ComponentArea } from 'snjs'; import template from '%/directives/revision-preview-modal.pug'; -import { PayloadContent } from '@node_modules/snjs/dist/@types/protocol/payloads/generator'; +import { PayloadContent } from 'snjs/dist/@types/protocol/payloads/generator'; import { confirmDialog } from '@/services/alertService'; interface RevisionPreviewScope { diff --git a/app/assets/javascripts/services/lockManager.ts b/app/assets/javascripts/services/autolock_service.ts similarity index 91% rename from app/assets/javascripts/services/lockManager.ts rename to app/assets/javascripts/services/autolock_service.ts index 19208f563..cea78b53e 100644 --- a/app/assets/javascripts/services/lockManager.ts +++ b/app/assets/javascripts/services/autolock_service.ts @@ -1,3 +1,4 @@ +import { ApplicationGroup } from './../ui_models/application_group'; import { WebApplication } from '@/ui_models/application'; import { isDesktopApplication } from '@/utils'; import { AppStateEvent } from '@/ui_models/app_state'; @@ -12,7 +13,7 @@ const LOCK_INTERVAL_ONE_HOUR = 3600 * MILLISECONDS_PER_SECOND; const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey"; -export class LockManager { +export class AutolockService { private application: WebApplication private unsubState: any @@ -21,11 +22,13 @@ export class LockManager { private lockAfterDate?: Date private lockTimeout?: any - constructor(application: WebApplication) { + constructor( + application: WebApplication + ) { this.application = application; - setImmediate(() => { + setTimeout(() => { this.observeVisibility(); - }); + }, 0); } observeVisibility() { @@ -50,6 +53,10 @@ export class LockManager { } } + private lockApplication() { + this.application.lock(); + } + async setAutoLockInterval(interval: number) { return this.application!.setValue( STORAGE_KEY_AUTOLOCK_INTERVAL, @@ -118,7 +125,7 @@ export class LockManager { this.lockAfterDate && new Date() > this.lockAfterDate ) { - this.application.lock(); + this.lockApplication(); } this.cancelAutoLockTimer(); } else { @@ -132,9 +139,9 @@ export class LockManager { return; } /** - * Use a timeout if possible, but if the computer is put to sleep, timeouts won't - * work. Need to set a date as backup. this.lockAfterDate does not need to be - * persisted, as living in memory is sufficient. If memory is cleared, then the + * Use a timeout if possible, but if the computer is put to sleep, timeouts won't + * work. Need to set a date as backup. this.lockAfterDate does not need to be + * persisted, as living in memory is sufficient. If memory is cleared, then the * application will lock anyway. */ const addToNow = (seconds: number) => { @@ -145,7 +152,7 @@ export class LockManager { this.lockAfterDate = addToNow(interval / MILLISECONDS_PER_SECOND); this.lockTimeout = setTimeout(() => { this.cancelAutoLockTimer(); - this.application.lock(); + this.lockApplication(); this.lockAfterDate = undefined; }, interval); } diff --git a/app/assets/javascripts/services/index.ts b/app/assets/javascripts/services/index.ts index 3de0713bf..b63b520e8 100644 --- a/app/assets/javascripts/services/index.ts +++ b/app/assets/javascripts/services/index.ts @@ -2,7 +2,7 @@ export { AlertService } from './alertService'; export { ArchiveManager } from './archiveManager'; export { DesktopManager } from './desktopManager'; export { KeyboardManager } from './keyboardManager'; -export { LockManager } from './lockManager'; +export { AutolockService } from './autolock_service'; export { NativeExtManager } from './nativeExtManager'; export { PreferencesManager } from './preferencesManager'; export { StatusManager } from './statusManager'; diff --git a/app/assets/javascripts/services/nativeExtManager.ts b/app/assets/javascripts/services/nativeExtManager.ts index 86156c40d..8cec7f356 100644 --- a/app/assets/javascripts/services/nativeExtManager.ts +++ b/app/assets/javascripts/services/nativeExtManager.ts @@ -10,8 +10,8 @@ import { Copy, dictToArray } from 'snjs'; -import { PayloadContent } from '@node_modules/snjs/dist/@types/protocol/payloads/generator'; -import { ComponentPermission } from '@node_modules/snjs/dist/@types/models/app/component'; +import { PayloadContent } from 'snjs/dist/@types/protocol/payloads/generator'; +import { ComponentPermission } from 'snjs/dist/@types/models/app/component'; /** A class for handling installation of system extensions */ export class NativeExtManager extends ApplicationService { diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts index 02b5dbea8..8351b22fd 100644 --- a/app/assets/javascripts/strings.ts +++ b/app/assets/javascripts/strings.ts @@ -46,7 +46,7 @@ export const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a p export function StringImportError(errorCount: number) { return `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`; } -export const STRING_ENTER_ACCOUNT_PASSCODE = 'Enter your application passcode to decrypt your data and unlock the application'; +export const STRING_ENTER_ACCOUNT_PASSCODE = 'Enter your application passcode to unlock the application'; export const STRING_ENTER_ACCOUNT_PASSWORD = 'Enter your account password'; export const STRING_ENTER_PASSCODE_FOR_MIGRATION = 'Your application passcode is required to perform an upgrade of your local data storage structure.'; export const STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER = 'Enter your application passcode before signing in or registering'; diff --git a/app/assets/javascripts/types.ts b/app/assets/javascripts/types.ts index df218f7c1..9042d0ee6 100644 --- a/app/assets/javascripts/types.ts +++ b/app/assets/javascripts/types.ts @@ -27,6 +27,10 @@ export interface PermissionsModalScope extends Partial { callback: (approved: boolean) => void } +export interface AccountSwitcherScope extends Partial { + application: any +} + export type PanelPuppet = { onReady?: () => void ready?: boolean diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts index c5fb3dab6..b6ecce1c9 100644 --- a/app/assets/javascripts/ui_models/application.ts +++ b/app/assets/javascripts/ui_models/application.ts @@ -1,3 +1,4 @@ +import { AccountSwitcherScope } from './../types'; import { ComponentGroup } from './component_group'; import { EditorGroup } from '@/ui_models/editor_group'; import { InputModalScope } from '@/directives/views/inputModal'; @@ -16,7 +17,7 @@ import { AlertService } from '@/services/alertService'; import { WebDeviceInterface } from '@/web_device_interface'; import { DesktopManager, - LockManager, + AutolockService, ArchiveManager, NativeExtManager, StatusManager, @@ -27,11 +28,12 @@ import { import { AppState } from '@/ui_models/app_state'; import { SNWebCrypto } from 'sncrypto/dist/sncrypto-web'; import { Bridge } from '@/services/bridge'; +import { DeinitSource } from 'snjs/dist/@types/types'; type WebServices = { appState: AppState desktopService: DesktopManager - lockService: LockManager + autolockService: AutolockService archiveService: ArchiveManager nativeExtService: NativeExtManager statusService: StatusManager @@ -44,7 +46,6 @@ export class WebApplication extends SNApplication { private $compile?: ng.ICompileService private scope?: ng.IScope - private onDeinit?: (app: WebApplication) => void private webServices!: WebServices private currentAuthenticationElement?: JQLite public editorGroup: EditorGroup @@ -52,38 +53,33 @@ export class WebApplication extends SNApplication { /* @ngInject */ constructor( + deviceInterface: WebDeviceInterface, + identifier: string, $compile: ng.ICompileService, - $timeout: ng.ITimeoutService, scope: ng.IScope, - onDeinit: (app: WebApplication) => void, defaultSyncServerHost: string, bridge: Bridge, ) { - const deviceInterface = new WebDeviceInterface( - $timeout, - bridge - ); super( bridge.environment, platformFromString(getPlatformString()), deviceInterface, new SNWebCrypto(), new AlertService(), - undefined, + identifier, undefined, undefined, defaultSyncServerHost ); this.$compile = $compile; this.scope = scope; - this.onDeinit = onDeinit; deviceInterface.setApplication(this); this.editorGroup = new EditorGroup(this); this.componentGroup = new ComponentGroup(this); } /** @override */ - deinit() { + deinit(source: DeinitSource) { for (const key of Object.keys(this.webServices)) { const service = (this.webServices as any)[key]; if (service.deinit) { @@ -92,8 +88,6 @@ export class WebApplication extends SNApplication { service.application = undefined; } this.webServices = {} as WebServices; - this.onDeinit!(this); - this.onDeinit = undefined; this.$compile = undefined; this.editorGroup.deinit(); this.componentGroup.deinit(); @@ -102,9 +96,9 @@ export class WebApplication extends SNApplication { this.scope = undefined; /** Allow our Angular directives to be destroyed and any pending digest cycles * to complete before destroying the global application instance and all its services */ - setImmediate(() => { - super.deinit(); - }) + setTimeout(() => { + super.deinit(source); + }, 0) } setWebServices(services: WebServices) { @@ -119,8 +113,8 @@ export class WebApplication extends SNApplication { return this.webServices.desktopService; } - public getLockService() { - return this.webServices.lockService; + public getAutolockService() { + return this.webServices.autolockService; } public getArchiveService() { @@ -257,4 +251,15 @@ export class WebApplication extends SNApplication { )(scope); angular.element(document.body).append(el); } + + public openAccountSwitcher() { + const scope = this.scope!.$new(true) as Partial; + scope.application = this; + const el = this.$compile!( + "" + )(scope as any); + angular.element(document.body).append(el); + } + } diff --git a/app/assets/javascripts/ui_models/application_group.ts b/app/assets/javascripts/ui_models/application_group.ts index 19a033cda..1db1a977b 100644 --- a/app/assets/javascripts/ui_models/application_group.ts +++ b/app/assets/javascripts/ui_models/application_group.ts @@ -1,10 +1,11 @@ +import { WebDeviceInterface } from '@/web_device_interface'; import { WebApplication } from './application'; -import { removeFromArray } from 'snjs'; +import { ApplicationDescriptor, SNApplicationGroup, DeviceInterface } from 'snjs'; import { ArchiveManager, DesktopManager, KeyboardManager, - LockManager, + AutolockService, NativeExtManager, PreferencesManager, StatusManager, @@ -13,16 +14,11 @@ import { import { AppState } from '@/ui_models/app_state'; import { Bridge } from '@/services/bridge'; -type AppManagerChangeCallback = () => void - -export class ApplicationGroup { +export class ApplicationGroup extends SNApplicationGroup { $compile: ng.ICompileService $rootScope: ng.IRootScopeService $timeout: ng.ITimeoutService - applications: WebApplication[] = [] - changeObservers: AppManagerChangeCallback[] = [] - activeApplication?: WebApplication /* @ngInject */ constructor( @@ -32,46 +28,35 @@ export class ApplicationGroup { private defaultSyncServerHost: string, private bridge: Bridge, ) { + super(new WebDeviceInterface( + $timeout, + bridge + )); this.$compile = $compile; this.$timeout = $timeout; this.$rootScope = $rootScope; - this.onApplicationDeinit = this.onApplicationDeinit.bind(this); - this.createDefaultApplication(); + } + + async initialize(callback?: any) { + await super.initialize({ + applicationCreator: this.createApplication + }); /** FIXME(baptiste): rely on a less fragile method to detect Electron */ if ((window as any).isElectron) { Object.defineProperty(window, 'desktopManager', { - get: () => this.activeApplication?.getDesktopService() + get: () => (this.primaryApplication as WebApplication).getDesktopService() }); } } - private createDefaultApplication() { - this.activeApplication = this.createNewApplication(); - this.applications.push(this.activeApplication!); - this.notifyObserversOfAppChange(); - } - - /** @callback */ - onApplicationDeinit(application: WebApplication) { - removeFromArray(this.applications, application); - if (this.activeApplication === application) { - this.activeApplication = undefined; - } - if (this.applications.length === 0) { - this.createDefaultApplication(); - } else { - this.notifyObserversOfAppChange(); - } - } - - private createNewApplication() { + private createApplication = (descriptor: ApplicationDescriptor, deviceInterface: DeviceInterface) => { const scope = this.$rootScope.$new(true); const application = new WebApplication( + deviceInterface as WebDeviceInterface, + descriptor.identifier, this.$compile, - this.$timeout, scope, - this.onApplicationDeinit, this.defaultSyncServerHost, this.bridge, ); @@ -90,7 +75,7 @@ export class ApplicationGroup { this.bridge, ); const keyboardService = new KeyboardManager(); - const lockService = new LockManager( + const autolockService = new AutolockService( application ); const nativeExtService = new NativeExtManager( @@ -108,7 +93,7 @@ export class ApplicationGroup { archiveService, desktopService, keyboardService, - lockService, + autolockService, nativeExtService, prefsService, statusService, @@ -116,34 +101,4 @@ export class ApplicationGroup { }); return application; } - - get application() { - return this.activeApplication; - } - - public getApplications() { - return this.applications.slice(); - } - - /** - * Notifies observer when the active application has changed. - * Any application which is no longer active is destroyed, and - * must be removed from the interface. - */ - public addApplicationChangeObserver(callback: AppManagerChangeCallback) { - this.changeObservers.push(callback); - if (this.application) { - callback(); - } - - return () => { - removeFromArray(this.changeObservers, callback); - } - } - - private notifyObserversOfAppChange() { - for (const observer of this.changeObservers) { - observer(); - } - } } diff --git a/app/assets/javascripts/ui_models/component_group.ts b/app/assets/javascripts/ui_models/component_group.ts index f8c8dae7d..7aa3b4c89 100644 --- a/app/assets/javascripts/ui_models/component_group.ts +++ b/app/assets/javascripts/ui_models/component_group.ts @@ -1,6 +1,6 @@ import { SNComponent, ComponentArea, removeFromArray, addIfUnique } from 'snjs'; import { WebApplication } from './application'; -import { UuidString } from '@node_modules/snjs/dist/@types/types'; +import { UuidString } from 'snjs/dist/@types/types'; /** Areas that only allow a single component to be active */ const SingleComponentAreas = [ @@ -48,8 +48,8 @@ export class ComponentGroup { } removeFromArray(this.activeComponents, component.uuid); /** If this function is called as part of global application deinit (locking), - * componentManager can be destroyed. In this case, it's harmless to not take any - * action since the componentManager will be destroyed, and the component will + * componentManager can be destroyed. In this case, it's harmless to not take any + * action since the componentManager will be destroyed, and the component will * essentially be deregistered. */ if(this.componentManager) { await this.componentManager.deactivateComponent(component.uuid); diff --git a/app/assets/javascripts/views/abstract/pure_view_ctrl.ts b/app/assets/javascripts/views/abstract/pure_view_ctrl.ts index 5b9bb8a78..e73224cf4 100644 --- a/app/assets/javascripts/views/abstract/pure_view_ctrl.ts +++ b/app/assets/javascripts/views/abstract/pure_view_ctrl.ts @@ -63,9 +63,15 @@ export class PureViewCtrl

{ if (!this.$timeout) { return; } - this.state = Object.freeze(Object.assign({}, this.state, state)); return new Promise((resolve) => { - this.stateTimeout = this.$timeout(resolve); + this.stateTimeout = this.$timeout(() => { + /** + * State changes must be *inside* the timeout block for them to be affected in the UI + * Otherwise UI controllers will need to use $timeout everywhere + */ + this.state = Object.freeze(Object.assign({}, this.state, state)); + resolve(); + }); }); } diff --git a/app/assets/javascripts/views/account_switcher/account-switcher.pug b/app/assets/javascripts/views/account_switcher/account-switcher.pug new file mode 100644 index 000000000..8596611a7 --- /dev/null +++ b/app/assets/javascripts/views/account_switcher/account-switcher.pug @@ -0,0 +1,32 @@ +.sk-modal-background(ng-click="ctrl.dismiss()") +#account-switcher.sk-modal-content + .sn-component + .sk-menu-panel#menu-panel + .sk-menu-panel-header + .sk-menu-panel-column + .sk-menu-panel-header-title Account Switcher + .sk-menu-panel-column + a.sk-label.info(ng-click='ctrl.addNewApplication()') Add Account + .sk-menu-panel-row( + ng-repeat='descriptor in ctrl.state.descriptors track by descriptor.identifier' + ng-click='ctrl.selectDescriptor(descriptor)' + ) + .sk-menu-panel-column.stretch + .left + .sk-menu-panel-column(ng-if='descriptor.identifier == ctrl.activeApplication.identifier') + .sk-circle.small.success + .sk-menu-panel-column.stretch + input.sk-label.clickable( + ng-model='descriptor.label' + ng-disabled='descriptor != ctrl.state.editingDescriptor' + ng-keyup='$event.keyCode == 13 && ctrl.submitRename($event)', + ng-attr-id='input-{{descriptor.identifier}}' + spellcheck="false" + ) + .sk-sublabel(ng-if='descriptor.identifier == ctrl.activeApplication.identifier') + | Current Application + .sk-menu-panel-column(ng-if='descriptor.identifier == ctrl.activeApplication.identifier') + .sk-button.success( + ng-click='ctrl.renameDescriptor($event, descriptor)' + ) + .sk-label Rename \ No newline at end of file diff --git a/app/assets/javascripts/views/account_switcher/account_switcher.ts b/app/assets/javascripts/views/account_switcher/account_switcher.ts new file mode 100644 index 000000000..2f2a64e4b --- /dev/null +++ b/app/assets/javascripts/views/account_switcher/account_switcher.ts @@ -0,0 +1,105 @@ +import { ApplicationGroup } from '@/ui_models/application_group'; +import { WebApplication } from '@/ui_models/application'; +import template from './account-switcher.pug'; +import { + ApplicationDescriptor, +} from 'snjs'; +import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; +import { WebDirective } from '@/types'; + +class AccountSwitcherCtrl extends PureViewCtrl<{}, { + descriptors: ApplicationDescriptor[]; + editingDescriptor?: ApplicationDescriptor +}> { + private $element: JQLite + application!: WebApplication + private removeAppGroupObserver: any; + /** @template */ + activeApplication!: WebApplication + + /* @ngInject */ + constructor( + $element: JQLite, + $timeout: ng.ITimeoutService, + private mainApplicationGroup: ApplicationGroup + ) { + super($timeout); + this.$element = $element; + this.removeAppGroupObserver = mainApplicationGroup.addApplicationChangeObserver(() => { + this.activeApplication = mainApplicationGroup.primaryApplication as WebApplication; + this.reloadApplications(); + }); + } + + $onInit() { + super.$onInit(); + } + + reloadApplications() { + this.setState({ + descriptors: this.mainApplicationGroup.getDescriptors() + }) + } + + /** @template */ + addNewApplication() { + this.dismiss(); + this.mainApplicationGroup.addNewApplication(); + } + + /** @template */ + selectDescriptor(descriptor: ApplicationDescriptor) { + this.dismiss(); + this.mainApplicationGroup.loadApplicationForDescriptor(descriptor); + } + + inputForDescriptor(descriptor: ApplicationDescriptor) { + return document.getElementById(`input-${descriptor.identifier}`); + } + + /** @template */ + renameDescriptor($event: Event, descriptor: ApplicationDescriptor) { + $event.stopPropagation(); + this.setState({ editingDescriptor: descriptor }).then(() => { + const input = this.inputForDescriptor(descriptor); + input?.focus(); + }) + } + + /** @template */ + submitRename() { + this.mainApplicationGroup.renameDescriptor( + this.state.editingDescriptor!, + this.state.editingDescriptor!.label + ) + this.setState({ editingDescriptor: undefined }); + } + + deinit() { + (this.application as any) = undefined; + super.deinit(); + this.removeAppGroupObserver(); + this.removeAppGroupObserver = undefined; + } + + dismiss() { + const elem = this.$element; + const scope = elem.scope(); + scope.$destroy(); + elem.remove(); + } +} + +export class AccountSwitcher extends WebDirective { + constructor() { + super(); + this.restrict = 'E'; + this.template = template; + this.controller = AccountSwitcherCtrl; + this.controllerAs = 'ctrl'; + this.bindToController = true; + this.scope = { + application: '=' + }; + } +} diff --git a/app/assets/javascripts/views/application/application-view.pug b/app/assets/javascripts/views/application/application-view.pug index fde4ff7d3..9da7ffde2 100644 --- a/app/assets/javascripts/views/application/application-view.pug +++ b/app/assets/javascripts/views/application/application-view.pug @@ -2,7 +2,7 @@ ng-class='self.platformString' ) #app.app( - ng-class='self.state.appClass', + ng-class='self.state.appClass', ng-if='!self.state.needsUnlock && self.state.ready' ) tags-view(application='self.application') @@ -14,4 +14,13 @@ footer-view( ng-if='!self.state.needsUnlock && self.state.ready' application='self.application' - ) \ No newline at end of file + ) + + svg(data-ionicons="5.1.2", style="display: none") + symbol#people-circle-outline.ionicon(viewbox="0 0 512 512") + path(d="M256 464c-114.69 0-208-93.31-208-208S141.31 48 256 48s208 93.31 208 208-93.31 208-208 208zm0-384c-97 0-176 79-176 176s79 176 176 176 176-78.95 176-176S353.05 80 256 80z") + path(d="M323.67 292c-17.4 0-34.21-7.72-47.34-21.73a83.76 83.76 0 01-22-51.32c-1.47-20.7 4.88-39.75 17.88-53.62S303.38 144 323.67 144c20.14 0 38.37 7.62 51.33 21.46s19.47 33 18 53.51a84 84 0 01-22 51.3C357.86 284.28 341.06 292 323.67 292zm55.81-74zM163.82 295.36c-29.76 0-55.93-27.51-58.33-61.33-1.23-17.32 4.15-33.33 15.17-45.08s26.22-18 43.15-18 32.12 6.44 43.07 18.14 16.5 27.82 15.25 45c-2.44 33.77-28.6 61.27-58.31 61.27zM420.37 355.28c-1.59-4.7-5.46-9.71-13.22-14.46-23.46-14.33-52.32-21.91-83.48-21.91-30.57 0-60.23 7.9-83.53 22.25-26.25 16.17-43.89 39.75-51 68.18-1.68 6.69-4.13 19.14-1.51 26.11a192.18 192.18 0 00232.75-80.17zM163.63 401.37c7.07-28.21 22.12-51.73 45.47-70.75a8 8 0 00-2.59-13.77c-12-3.83-25.7-5.88-42.69-5.88-23.82 0-49.11 6.45-68.14 18.17-5.4 3.33-10.7 4.61-14.78 5.75a192.84 192.84 0 0077.78 86.64l1.79-.14a102.82 102.82 0 013.16-20.02z") + + symbol#layers-sharp.ionicon(viewbox="0 0 512 512") + path(d="M480 150L256 48 32 150l224 104 224-104zM255.71 392.95l-144.81-66.2L32 362l224 102 224-102-78.69-35.3-145.6 66.25z") + path(d="M480 256l-75.53-33.53L256.1 290.6l-148.77-68.17L32 256l224 102 224-102z") diff --git a/app/assets/javascripts/views/application/application_view.ts b/app/assets/javascripts/views/application/application_view.ts index d690087fd..020fe30a2 100644 --- a/app/assets/javascripts/views/application/application_view.ts +++ b/app/assets/javascripts/views/application/application_view.ts @@ -14,7 +14,7 @@ import { STRING_DEFAULT_FILE_ERROR } from '@/strings'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; -import { PermissionDialog } from '@node_modules/snjs/dist/@types/services/component_manager'; +import { PermissionDialog } from 'snjs/dist/@types/services/component_manager'; import { alertDialog } from '@/services/alertService'; class ApplicationViewCtrl extends PureViewCtrl { diff --git a/app/assets/javascripts/views/application_group/application-group-view.pug b/app/assets/javascripts/views/application_group/application-group-view.pug index fa901e39d..8b3f23482 100644 --- a/app/assets/javascripts/views/application_group/application-group-view.pug +++ b/app/assets/javascripts/views/application_group/application-group-view.pug @@ -1,4 +1,5 @@ application-view( ng-repeat='application in self.applications', + ng-if='application == self.activeApplication' application='application' ) \ No newline at end of file diff --git a/app/assets/javascripts/views/application_group/application_group_view.ts b/app/assets/javascripts/views/application_group/application_group_view.ts index bfe016bb5..5dcfe33cf 100644 --- a/app/assets/javascripts/views/application_group/application_group_view.ts +++ b/app/assets/javascripts/views/application_group/application_group_view.ts @@ -7,7 +7,8 @@ class ApplicationGroupViewCtrl { private $timeout: ng.ITimeoutService private applicationGroup: ApplicationGroup - public applications: WebApplication[] = [] + applications!: WebApplication[] + activeApplication!: WebApplication /* @ngInject */ constructor( @@ -19,11 +20,13 @@ class ApplicationGroupViewCtrl { this.applicationGroup.addApplicationChangeObserver(() => { this.reload(); }); + this.applicationGroup.initialize(); } reload() { this.$timeout(() => { - this.applications = this.applicationGroup.getApplications(); + this.activeApplication = this.applicationGroup.primaryApplication as WebApplication; + this.applications = this.applicationGroup.getApplications() as WebApplication[]; }); } } @@ -33,7 +36,7 @@ export class ApplicationGroupView extends WebDirective { super(); this.template = template; this.controller = ApplicationGroupViewCtrl; - this.replace = true; + this.replace = false; this.controllerAs = 'self'; this.bindToController = true; } diff --git a/app/assets/javascripts/views/footer/footer-view.pug b/app/assets/javascripts/views/footer/footer-view.pug index 437484672..7e069a88c 100644 --- a/app/assets/javascripts/views/footer/footer-view.pug +++ b/app/assets/javascripts/views/footer/footer-view.pug @@ -3,7 +3,7 @@ .left .sk-app-bar-item( click-outside='ctrl.clickOutsideAccountMenu()', - is-open='ctrl.showAccountMenu', + is-open='ctrl.showAccountMenu', ng-click='ctrl.accountMenuPressed()' ) .sk-app-bar-item-column @@ -13,15 +13,15 @@ .sk-app-bar-item-column .sk-label.title(ng-class='{red: ctrl.hasError}') Account account-menu( - close-function='ctrl.closeAccountMenu()', - ng-click='$event.stopPropagation()', + close-function='ctrl.closeAccountMenu()', + ng-click='$event.stopPropagation()', ng-if='ctrl.showAccountMenu', application='ctrl.application' ) .sk-app-bar-item a.no-decoration.sk-label.title( - href='https://standardnotes.org/help', - rel='noopener', + href='https://standardnotes.org/help', + rel='noopener', target='_blank' ) | Help @@ -30,8 +30,8 @@ .sk-app-bar-item-column(ng-click='ctrl.selectRoom(room)') .sk-label {{room.name}} component-modal( - component-uuid='room.uuid', - ng-if='ctrl.roomShowState[room.uuid]', + component-uuid='room.uuid', + ng-if='ctrl.roomShowState[room.uuid]', on-dismiss='ctrl.onRoomDismiss(room)', application='ctrl.application' ) @@ -41,12 +41,12 @@ span.neutral.sk-label {{ctrl.arbitraryStatusMessage}} .right .sk-app-bar-item( - ng-click='ctrl.openSecurityUpdate()', + ng-click='ctrl.openSecurityUpdate()', ng-if='ctrl.state.dataUpgradeAvailable' ) span.success.sk-label Encryption upgrade available. .sk-app-bar-item( - ng-click='ctrl.clickedNewUpdateAnnouncement()', + ng-click='ctrl.clickedNewUpdateAnnouncement()', ng-if='ctrl.newUpdateAvailable == true' ) span.info.sk-label New update available. @@ -56,13 +56,13 @@ .sk-label.subtle(ng-if='!ctrl.offline') | Last refreshed {{ctrl.lastSyncDate}} .sk-app-bar-item( - ng-click='ctrl.toggleSyncResolutionMenu()', + ng-click='ctrl.toggleSyncResolutionMenu()', ng-if='(ctrl.state.outOfSync && !ctrl.isRefreshing) || ctrl.showSyncResolution' ) .sk-label.warning(ng-if='ctrl.state.outOfSync') Potentially Out of Sync sync-resolution-menu( - close-function='ctrl.toggleSyncResolutionMenu()', - ng-click='$event.stopPropagation();', + close-function='ctrl.toggleSyncResolutionMenu()', + ng-click='$event.stopPropagation();', ng-if='ctrl.showSyncResolution', application='ctrl.application' ) @@ -70,27 +70,35 @@ .sk-spinner.small .sk-app-bar-item(ng-if='ctrl.offline') .sk-label Offline - .sk-app-bar-item(ng-click='ctrl.refreshData()', ng-if='!ctrl.offline') + .sk-app-bar-item(ng-click='ctrl.refreshData()' ng-if='!ctrl.offline') .sk-label Refresh .sk-app-bar-item.border(ng-if='ctrl.state.dockShortcuts.length > 0') .sk-app-bar-item.dock-shortcut(ng-repeat='shortcut in ctrl.state.dockShortcuts') .sk-app-bar-item-column( - ng-class="{'underline': shortcut.component.active}", + ng-class="{'underline': shortcut.component.active}", ng-click='ctrl.selectShortcut(shortcut)' ) - .div(ng-if="shortcut.icon.type == 'circle'", title='{{shortcut.name}}') + .div(ng-if="shortcut.icon.type == 'circle'" title='{{shortcut.name}}') .sk-circle.small( ng-style="{'background-color': shortcut.icon.background_color, 'border-color': shortcut.icon.border_color}" ) - .div(ng-if="shortcut.icon.type == 'svg'", title='{{shortcut.name}}') + .div(ng-if="shortcut.icon.type == 'svg'" title='{{shortcut.name}}') .svg-item( - elem-ready='ctrl.initSvgForShortcut(shortcut)', + elem-ready='ctrl.initSvgForShortcut(shortcut)', ng-attr-id='dock-svg-{{shortcut.component.uuid}}' ) + .sk-app-bar-item.border(ng-if='ctrl.state.hasAccountSwitcher') + .sk-app-bar-item( + ng-if='ctrl.state.hasAccountSwitcher' + ng-click='ctrl.openAccountSwitcher()', + ) + #account-switcher-icon(ng-class='{"alone": !ctrl.state.hasPasscode}') + svg.info.ionicon + use(href="#layers-sharp") .sk-app-bar-item.border(ng-if='ctrl.state.hasPasscode') #lock-item.sk-app-bar-item( - ng-click='ctrl.lockApp()', - ng-if='ctrl.state.hasPasscode', + ng-click='ctrl.lockApp()', + ng-if='ctrl.state.hasPasscode', title='Locks application and wipes unencrypted data from memory.' ) .sk-label diff --git a/app/assets/javascripts/views/footer/footer_view.ts b/app/assets/javascripts/views/footer/footer_view.ts index 7185e2707..6f711d7b5 100644 --- a/app/assets/javascripts/views/footer/footer_view.ts +++ b/app/assets/javascripts/views/footer/footer_view.ts @@ -1,3 +1,4 @@ +import { ApplicationGroup } from '@/ui_models/application_group'; import { FooterStatus, WebDirective } from '@/types'; import { dateToLocalizedString, preventRefreshing } from '@/utils'; import { @@ -11,7 +12,7 @@ import { ComponentAction, topLevelCompare, CollectionSort, - ComponentMutator, + ComponentMutator } from 'snjs'; import template from './footer-view.pug'; import { AppStateEvent, EventSource } from '@/ui_models/app_state'; @@ -22,6 +23,14 @@ import { } from '@/strings'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; +/** + * Disable before production release. + * Anyone who used the beta will still have access to + * the account switcher in production via local storage flag + */ +const ACCOUNT_SWITCHER_ENABLED = true; +const ACCOUNT_SWITCHER_FEATURE_KEY = 'account_switcher'; + type DockShortcut = { name: string, component: SNComponent, @@ -37,8 +46,8 @@ class FooterViewCtrl extends PureViewCtrl<{}, { hasPasscode: boolean; dataUpgradeAvailable: boolean; dockShortcuts: DockShortcut[]; + hasAccountSwitcher: boolean }> { - private $rootScope: ng.IRootScopeService private rooms: SNComponent[] = [] private themesWithIcons: SNTheme[] = [] @@ -66,6 +75,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { constructor( $rootScope: ng.IRootScopeService, $timeout: ng.ITimeoutService, + private mainApplicationGroup: ApplicationGroup ) { super($timeout); this.$rootScope = $rootScope; @@ -92,11 +102,22 @@ class FooterViewCtrl extends PureViewCtrl<{}, { $onInit() { super.$onInit(); - this.application!.getStatusService().addStatusObserver((string: string) => { + this.application.getStatusService().addStatusObserver((string: string) => { this.$timeout(() => { this.arbitraryStatusMessage = string; }); }); + this.loadAccountSwitcherState(); + } + + loadAccountSwitcherState() { + const stringValue = localStorage.getItem(ACCOUNT_SWITCHER_FEATURE_KEY); + if (!stringValue && ACCOUNT_SWITCHER_ENABLED) { + /** Enable permanently for this user so they don't lose the feature after its disabled */ + localStorage.setItem(ACCOUNT_SWITCHER_FEATURE_KEY, JSON.stringify(true)); + } + const hasAccountSwitcher = stringValue ? JSON.parse(stringValue) : ACCOUNT_SWITCHER_ENABLED; + this.setState({ hasAccountSwitcher }); } getInitialState() { @@ -105,17 +126,24 @@ class FooterViewCtrl extends PureViewCtrl<{}, { dataUpgradeAvailable: false, hasPasscode: false, dockShortcuts: [], + descriptors: this.mainApplicationGroup.getDescriptors(), + hasAccountSwitcher: false }; } reloadUpgradeStatus() { - this.application!.checkForSecurityUpdate().then((available) => { + this.application.checkForSecurityUpdate().then((available) => { this.setState({ dataUpgradeAvailable: available }); }); } + /** @template */ + openAccountSwitcher() { + this.application.openAccountSwitcher(); + } + async onAppLaunch() { super.onAppLaunch(); this.reloadPasscodeStatus(); @@ -128,11 +156,11 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } reloadUser() { - this.user = this.application!.getUser(); + this.user = this.application.getUser(); } async reloadPasscodeStatus() { - const hasPasscode = this.application!.hasPasscode(); + const hasPasscode = this.application.hasPasscode(); this.setState({ hasPasscode: hasPasscode }); @@ -157,23 +185,23 @@ class FooterViewCtrl extends PureViewCtrl<{}, { this.closeAccountMenu(); } } else if (eventName === AppStateEvent.BeganBackupDownload) { - this.backupStatus = this.application!.getStatusService().addStatusFromString( + this.backupStatus = this.application.getStatusService().addStatusFromString( "Saving local backup..." ); } else if (eventName === AppStateEvent.EndedBackupDownload) { if (data.success) { - this.backupStatus = this.application!.getStatusService().replaceStatusWithString( + this.backupStatus = this.application.getStatusService().replaceStatusWithString( this.backupStatus!, "Successfully saved backup." ); } else { - this.backupStatus = this.application!.getStatusService().replaceStatusWithString( + this.backupStatus = this.application.getStatusService().replaceStatusWithString( this.backupStatus!, "Unable to save local backup." ); } this.$timeout(() => { - this.backupStatus = this.application!.getStatusService().removeStatus(this.backupStatus!); + this.backupStatus = this.application.getStatusService().removeStatus(this.backupStatus!); }, 2000); } } @@ -199,7 +227,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } else if (eventName === ApplicationEvent.CompletedFullSync) { if (!this.didCheckForOffline) { this.didCheckForOffline = true; - if (this.offline && this.application!.getNoteCount() === 0) { + if (this.offline && this.application.getNoteCount() === 0) { this.showAccountMenu = true; } } @@ -230,10 +258,10 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } ) - this.observerRemovers.push(this.application!.streamItems( + this.observerRemovers.push(this.application.streamItems( ContentType.Component, async () => { - const components = this.application!.getItems(ContentType.Component) as SNComponent[]; + const components = this.application.getItems(ContentType.Component) as SNComponent[]; this.rooms = components.filter((candidate) => { return candidate.area === ComponentArea.Rooms && !candidate.deleted; }); @@ -244,10 +272,10 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } )); - this.observerRemovers.push(this.application!.streamItems( + this.observerRemovers.push(this.application.streamItems( ContentType.Theme, async () => { - const themes = this.application!.getDisplayableItems(ContentType.Theme) as SNTheme[]; + const themes = this.application.getDisplayableItems(ContentType.Theme) as SNTheme[]; this.themesWithIcons = themes; this.reloadDockShortcuts(); } @@ -255,14 +283,14 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } registerComponentHandler() { - this.unregisterComponent = this.application!.componentManager!.registerHandler({ + this.unregisterComponent = this.application.componentManager!.registerHandler({ identifier: 'room-bar', areas: [ComponentArea.Rooms, ComponentArea.Modal], actionHandler: (component, action, data) => { if (action === ComponentAction.SetSize) { /** Do comparison to avoid repetitive calls by arbitrary component */ if (!topLevelCompare(component.getLastSize(), data)) { - this.application!.changeItem(component.uuid, (mutator) => { + this.application.changeItem(component.uuid, (mutator) => { mutator.setLastSize(data); }) } @@ -288,7 +316,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { * then closing it after a short delay. */ const extWindow = this.rooms.find((room) => { - return room.package_info.identifier === this.application! + return room.package_info.identifier === this.application .getNativeExtService().extManagerId; }); if (!extWindow) { @@ -305,17 +333,17 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } updateOfflineStatus() { - this.offline = this.application!.noAccount(); + this.offline = this.application.noAccount(); } async openSecurityUpdate() { preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => { - await this.application!.performProtocolUpgrade(); + await this.application.performProtocolUpgrade(); }); } findErrors() { - this.hasError = this.application!.getSyncStatus().hasError(); + this.hasError = this.application.getSyncStatus().hasError(); } accountMenuPressed() { @@ -332,12 +360,12 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } lockApp() { - this.application!.lock(); + this.application.lock(); } refreshData() { this.isRefreshing = true; - this.application!.sync({ + this.application.sync({ queueStrategy: SyncQueueStrategy.ForceSpawnNew, checkIntegrity: true }).then((response) => { @@ -345,7 +373,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { this.isRefreshing = false; }, 200); if (response && response.error) { - this.application!.alertService!.alert( + this.application.alertService!.alert( STRING_GENERIC_SYNC_ERROR ); } else { @@ -355,7 +383,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } syncUpdated() { - this.lastSyncDate = dateToLocalizedString(this.application!.getLastSyncDate()!); + this.lastSyncDate = dateToLocalizedString(this.application.getLastSyncDate()!); } onNewUpdateAvailable() { @@ -364,7 +392,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { clickedNewUpdateAnnouncement() { this.newUpdateAvailable = false; - this.application!.alertService!.alert( + this.application.alertService!.alert( STRING_NEW_UPDATE_READY ); } @@ -409,7 +437,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } selectShortcut(shortcut: DockShortcut) { - this.application!.toggleComponent(shortcut.component); + this.application.toggleComponent(shortcut.component); } onRoomDismiss(room: SNComponent) { @@ -430,12 +458,12 @@ class FooterViewCtrl extends PureViewCtrl<{}, { }; if (!this.roomShowState[room.uuid]) { - const requiresPrivilege = await this.application!.privilegesService! + const requiresPrivilege = await this.application.privilegesService! .actionRequiresPrivilege( ProtectedAction.ManageExtensions ); if (requiresPrivilege) { - this.application!.presentPrivilegesModal( + this.application.presentPrivilegesModal( ProtectedAction.ManageExtensions, run ); @@ -448,7 +476,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, { } clickOutsideAccountMenu() { - if (this.application && this.application!.authenticationInProgress()) { + if (this.application && this.application.authenticationInProgress()) { return; } this.showAccountMenu = false; diff --git a/app/assets/javascripts/views/notes/notes_view.ts b/app/assets/javascripts/views/notes/notes_view.ts index 754677ce7..5e007e925 100644 --- a/app/assets/javascripts/views/notes/notes_view.ts +++ b/app/assets/javascripts/views/notes/notes_view.ts @@ -20,7 +20,7 @@ import { NoteSortKey, notePassesFilter } from './note_utils'; -import { UuidString } from '@node_modules/snjs/dist/@types/types'; +import { UuidString } from 'snjs/dist/@types/types'; type NotesState = { panelTitle: string diff --git a/app/assets/javascripts/web_device_interface.ts b/app/assets/javascripts/web_device_interface.ts index 395bf7b41..8ffc67b73 100644 --- a/app/assets/javascripts/web_device_interface.ts +++ b/app/assets/javascripts/web_device_interface.ts @@ -1,10 +1,10 @@ -import { DeviceInterface, getGlobalScope, SNApplication } from 'snjs'; +import { DeviceInterface, getGlobalScope, SNApplication, ApplicationIdentifier } from 'snjs'; import { Database } from '@/database'; import { Bridge } from './services/bridge'; export class WebDeviceInterface extends DeviceInterface { - private database: Database + private databases: Database[] = [] constructor( timeout: any, @@ -14,16 +14,23 @@ export class WebDeviceInterface extends DeviceInterface { timeout || setTimeout.bind(getGlobalScope()), setInterval.bind(getGlobalScope()) ); - this.database = new Database(); } setApplication(application: SNApplication) { - this.database.setAlertService(application.alertService!); + const database = new Database(application.identifier, application.alertService!); + this.databases.push(database); + } + + private databaseForIdentifier(identifier: ApplicationIdentifier) { + return this.databases.find(database => database.databaseName === identifier)!; } deinit() { super.deinit(); - this.database.deinit(); + for(const database of this.databases) { + database.deinit(); + } + this.databases = []; } async getRawStorageValue(key: string) { @@ -53,10 +60,10 @@ export class WebDeviceInterface extends DeviceInterface { localStorage.clear(); } - async openDatabase() { - this.database.unlock(); + async openDatabase(identifier: ApplicationIdentifier) { + this.databaseForIdentifier(identifier).unlock(); return new Promise((resolve, reject) => { - this.database.openDatabase(() => { + this.databaseForIdentifier(identifier).openDatabase(() => { resolve({ isNewDatabase: true }); }).then(() => { resolve({ isNewDatabase: false }); @@ -66,63 +73,51 @@ export class WebDeviceInterface extends DeviceInterface { }) as Promise<{ isNewDatabase?: boolean } | undefined>; } - private getDatabaseKeyPrefix() { - if (this.namespace) { - return `${this.namespace}-item-`; - } else { - return `item-`; - } + async getAllRawDatabasePayloads(identifier: ApplicationIdentifier) { + return this.databaseForIdentifier(identifier).getAllPayloads(); } - private keyForPayloadId(id: string) { - return `${this.getDatabaseKeyPrefix()}${id}`; + async saveRawDatabasePayload(payload: any, identifier: ApplicationIdentifier) { + return this.databaseForIdentifier(identifier).savePayload(payload); } - async getAllRawDatabasePayloads() { - return this.database.getAllPayloads(); + async saveRawDatabasePayloads(payloads: any[], identifier: ApplicationIdentifier) { + return this.databaseForIdentifier(identifier).savePayloads(payloads); } - async saveRawDatabasePayload(payload: any) { - return this.database.savePayload(payload); + async removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier) { + return this.databaseForIdentifier(identifier).deletePayload(id); } - async saveRawDatabasePayloads(payloads: any[]) { - return this.database.savePayloads(payloads); + async removeAllRawDatabasePayloads(identifier: ApplicationIdentifier) { + return this.databaseForIdentifier(identifier).clearAllPayloads(); } - async removeRawDatabasePayloadWithId(id: string) { - return this.database.deletePayload(id); - } - - async removeAllRawDatabasePayloads() { - return this.database.clearAllPayloads(); - } - - async getNamespacedKeychainValue() { + async getNamespacedKeychainValue(identifier: ApplicationIdentifier) { const keychain = await this.getRawKeychainValue(); if (!keychain) { return; } - return keychain[this.namespace!.identifier]; + return keychain[identifier]; } - async setNamespacedKeychainValue(value: any) { + async setNamespacedKeychainValue(value: any, identifier: ApplicationIdentifier) { let keychain = await this.getRawKeychainValue(); if (!keychain) { keychain = {}; } this.bridge.setKeychainValue({ ...keychain, - [this.namespace!.identifier]: value + [identifier]: value }); } - async clearNamespacedKeychainValue() { + async clearNamespacedKeychainValue(identifier: ApplicationIdentifier) { const keychain = await this.getRawKeychainValue(); if (!keychain) { return; } - delete keychain[this.namespace!.identifier]; + delete keychain[identifier]; this.bridge.setKeychainValue(keychain); } diff --git a/app/assets/stylesheets/_footer.scss b/app/assets/stylesheets/_footer.scss index 7375efede..94bc22fdd 100644 --- a/app/assets/stylesheets/_footer.scss +++ b/app/assets/stylesheets/_footer.scss @@ -31,18 +31,28 @@ border-bottom: 2px solid var(--sn-stylekit-info-color); } - svg { + .ionicon { width: 12px; height: 12px; - fill: var(--sn-stylekit-foreground-color); + fill: var(--sn-stylekit-secondary-foreground-color); &:hover { - fill: var(--sn-stylekit-info-color); + fill: var(--sn-stylekit-info-color) !important; + color: var(--sn-stylekit-info-color) !important; } } } -#account-panel, #sync-resolution-menu { +#account-switcher-icon { + // When this icon is alone in the footer bar, it is displayed with too little + // padding on the right, possibly due to the fact it is a raw SVG. + &.alone { + margin-right: 4px; + } +} + +#account-panel, +#sync-resolution-menu { width: 400px; } diff --git a/app/assets/stylesheets/_ionicons.scss b/app/assets/stylesheets/_ionicons.scss index 7a3510b6a..e2d032034 100644 --- a/app/assets/stylesheets/_ionicons.scss +++ b/app/assets/stylesheets/_ionicons.scss @@ -12,22 +12,30 @@ Modified icons to fit ionicon’s grid from original. */ @font-face { - font-family: "Ionicons"; - src: url("../fonts/ionicons.eot?v=2.0.0"); - src: url("../fonts/ionicons.eot?v=2.0.1#iefix") format("embedded-opentype"), - url("../fonts/ionicons.ttf?v=2.0.1") format("truetype"), - url("../fonts/ionicons.woff?v=2.0.1") format("woff"), - url("../fonts/ionicons.svg?v=2.0.1#Ionicons") format("svg"); + font-family: 'Ionicons'; + src: url('../fonts/ionicons.eot?v=2.0.0'); + src: url('../fonts/ionicons.eot?v=2.0.1#iefix') format('embedded-opentype'), + url('../fonts/ionicons.ttf?v=2.0.1') format('truetype'), + url('../fonts/ionicons.woff?v=2.0.1') format('woff'), + url('../fonts/ionicons.svg?v=2.0.1#Ionicons') format('svg'); font-weight: normal; font-style: normal; } +.ionicon-fill-none { + fill: none; +} +.ionicon-stroke-width { + stroke-width: 32px; +} + + .ion, .ionicons, .ion-locked:before, .ion-plus:before { display: inline-block; - font-family: "Ionicons"; + font-family: 'Ionicons'; speak: none; font-style: normal; font-weight: normal; @@ -41,11 +49,11 @@ } .ion-locked:before { - content: "\f200"; + content: '\f200'; } .ion-plus:before { - content: "\f218"; + content: '\f218'; } /*# sourceMappingURL=ionicons.css.map */ diff --git a/app/assets/stylesheets/_modals.scss b/app/assets/stylesheets/_modals.scss index be8495c7f..8220d1fdf 100644 --- a/app/assets/stylesheets/_modals.scss +++ b/app/assets/stylesheets/_modals.scss @@ -25,6 +25,22 @@ } } +#account-switcher { + min-width: 400px; + max-width: 580px; + input, input:disabled { + width: 100%; + border: none; + background-color: transparent !important; + color: inherit; + margin-left: -2px; + } + + input.clickable:hover { + cursor: pointer; + } +} + #privileges-modal { min-width: 400px; max-width: 700px; @@ -50,7 +66,8 @@ background-color: var(--sn-stylekit-background-color); color: var(--sn-stylekit-contrast-foreground-color); - th, td { + th, + td { padding: 6px 13px; border: 1px solid var(--sn-stylekit-contrast-border-color); } @@ -65,7 +82,7 @@ font-weight: normal; } - .priv-header { + .priv-header { display: flex; flex-direction: column; justify-content: center; @@ -157,9 +174,9 @@ padding-bottom: 0; min-width: 300px; - -webkit-box-shadow: 0px 2px 35px 0px rgba(0,0,0,0.19); - -moz-box-shadow: 0px 2px 35px 0px rgba(0,0,0,0.19); - box-shadow: 0px 2px 35px 0px rgba(0,0,0,0.19); + -webkit-box-shadow: 0px 2px 35px 0px rgba(0, 0, 0, 0.19); + -moz-box-shadow: 0px 2px 35px 0px rgba(0, 0, 0, 0.19); + box-shadow: 0px 2px 35px 0px rgba(0, 0, 0, 0.19); } } diff --git a/app/assets/templates/directives/account-menu.pug b/app/assets/templates/directives/account-menu.pug index 97f27c81f..0898b91dc 100644 --- a/app/assets/templates/directives/account-menu.pug +++ b/app/assets/templates/directives/account-menu.pug @@ -6,9 +6,9 @@ .sk-panel-content .sk-panel-section.sk-panel-hero( ng-if=` - !self.state.user && - !self.state.formData.showLogin && - !self.state.formData.showRegister && + !self.state.user && + !self.state.formData.showLogin && + !self.state.formData.showRegister && !self.state.formData.mfa` ) .sk-panel-row @@ -20,10 +20,10 @@ .sk-button.info.featured(ng-click='self.state.formData.showRegister = true') .sk-label Register .sk-panel-row.sk-p - | Standard Notes is free on every platform, and comes + | Standard Notes is free on every platform, and comes | standard with sync and encryption. .sk-panel-section(ng-if=` - self.state.formData.showLogin || + self.state.formData.showLogin || self.state.formData.showRegister` ) .sk-panel-section-title @@ -31,31 +31,31 @@ form.sk-panel-form(ng-submit='self.submitAuthForm()') .sk-panel-section input.sk-input.contrast( - name='email', - ng-model='self.state.formData.email', - ng-model-options='{allowInvalid: true}', - placeholder='Email', - required='', - should-focus='true', - sn-autofocus='true', - spellcheck='false', + name='email', + ng-model='self.state.formData.email', + ng-model-options='{allowInvalid: true}', + placeholder='Email', + required='', + should-focus='true', + sn-autofocus='true', + spellcheck='false', type='email' ) input.sk-input.contrast( - name='password', - ng-model='self.state.formData.user_password', - placeholder='Password', - required='', - sn-enter='self.submitAuthForm()', + name='password', + ng-model='self.state.formData.user_password', + placeholder='Password', + required='', + sn-enter='self.submitAuthForm()', type='password' ) input.sk-input.contrast( - name='password_conf', - ng-if='self.state.formData.showRegister', - ng-model='self.state.formData.password_conf', - placeholder='Confirm Password', - required='', - sn-enter='self.submitAuthForm()', + name='password_conf', + ng-if='self.state.formData.showRegister', + ng-model='self.state.formData.password_conf', + placeholder='Confirm Password', + required='', + sn-enter='self.submitAuthForm()', type='password' ) .sk-panel-row @@ -73,11 +73,11 @@ .bordered-row.padded-row label.sk-label Sync Server Domain input.sk-input.mt-5.sk-base( - name='server', - ng-model='self.state.formData.url', + name='server', + ng-model='self.state.formData.url', ng-change='self.onHostInputChange()' - placeholder='Server URL', - required='', + placeholder='Server URL', + required='', type='text' ) label.sk-label.padded-row.sk-panel-row.justify-left( @@ -85,28 +85,28 @@ ) .sk-horizontal-group.tight input.sk-input( - ng-model='self.state.formData.strictSignin', + ng-model='self.state.formData.strictSignin', type='checkbox' ) p.sk-p Use strict sign in span a.info( - href='https://standardnotes.org/help/security', - rel='noopener', + href='https://standardnotes.org/help/security', + rel='noopener', target='_blank' ) (Learn more) .sk-panel-section.form-submit(ng-if='!self.state.formData.authenticating') .sk-button-group.stretch .sk-button.info.featured( - ng-click='self.submitAuthForm()', + ng-click='self.submitAuthForm()', ng-disabled='self.state.formData.authenticating' ) .sk-label {{self.state.formData.showLogin ? "Sign In" : "Register"}} .sk-notification.neutral(ng-if='self.state.formData.showRegister') .sk-notification-title No Password Reset. .sk-notification-text - | Because your notes are encrypted using your password, - | Standard Notes does not have a password reset option. + | Because your notes are encrypted using your password, + | Standard Notes does not have a password reset option. | You cannot forget your password. .sk-panel-section.no-bottom-pad(ng-if='self.state.formData.status') .sk-horizontal-group @@ -116,18 +116,18 @@ label.sk-panel-row.justify-left .sk-horizontal-group.tight input( - ng-false-value='true', - ng-model='self.state.formData.ephemeral', - ng-true-value='false', + ng-false-value='true', + ng-model='self.state.formData.ephemeral', + ng-true-value='false', type='checkbox' ) p.sk-p Stay signed in label.sk-panel-row.justify-left(ng-if='self.notesAndTagsCount() > 0') .sk-horizontal-group.tight input( - ng-bind='true', - ng-change='self.mergeLocalChanged()', - ng-model='self.state.formData.mergeLocal', + ng-bind='true', + ng-change='self.mergeLocalChanged()', + ng-model='self.state.formData.mergeLocal', type='checkbox' ) p.sk-p Merge local data ({{self.notesAndTagsCount()}} notes and tags) @@ -136,12 +136,12 @@ .sk-p.sk-panel-row {{self.state.formData.mfa.message}} .sk-panel-row input.sk-input.contrast( - autofocus='true', - name='mfa', - ng-model='self.state.formData.userMfaCode', - placeholder='Enter Code', - required='', - should-focus='true', + autofocus='true', + name='mfa', + ng-model='self.state.formData.userMfaCode', + placeholder='Enter Code', + required='', + should-focus='true', sn-autofocus='true' ) .sk-button-group.stretch.sk-panel-row.form-submit( @@ -157,19 +157,19 @@ .sk-label {{self.state.formData.status}} div( ng-if=` - !self.state.formData.showLogin && - !self.state.formData.showRegister && + !self.state.formData.showLogin && + !self.state.formData.showRegister && !self.state.formData.mfa` ) .sk-panel-section(ng-if='self.state.user') .sk-notification.danger(ng-if='self.syncStatus.error') .sk-notification-title Sync Unreachable .sk-notification-text - | Hmm...we can't seem to sync your account. + | Hmm...we can't seem to sync your account. | The reason: {{self.syncStatus.error.message}} a.sk-a.info-contrast.sk-bold.sk-panel-row( - href='https://standardnotes.org/help', - rel='noopener', + href='https://standardnotes.org/help', + rel='noopener', target='_blank' ) Need help? .sk-panel-row @@ -177,8 +177,8 @@ .sk-h1.sk-bold.wrap {{self.state.user.email}} .sk-subtitle.subtle.normal {{self.state.server}} .sk-horizontal-group( - delay='1000', - delay-hide='true', + delay='1000', + delay-hide='true', show='self.syncStatus.syncOpInProgress || self.syncStatus.needsMoreSync' ) .sk-spinner.small.info @@ -193,7 +193,7 @@ ) | Change Password a.sk-a.info.sk-panel-row.condensed( - ng-click="self.openPrivilegesModal('')", + ng-click="self.openPrivilegesModal('')", ng-show='self.state.user' ) | Manage Privileges @@ -212,28 +212,28 @@ ng-click='self.addPasscodeClicked(); $event.stopPropagation();' ) .sk-label Add Passcode - p.sk-p - | Add a passcode to lock the application and + p.sk-p + | Add a passcode to lock the application and | encrypt on-device key storage. div(ng-if='!self.state.canAddPasscode') p.sk-p - | Adding a passcode is not supported in temporary sessions. Please sign + | Adding a passcode is not supported in temporary sessions. Please sign | out, then sign back in with the "Stay signed in" option checked. form.sk-panel-form( - ng-if='self.state.formData.showPasscodeForm', + ng-if='self.state.formData.showPasscodeForm', ng-submit='self.submitPasscodeForm()' ) .sk-panel-row input.sk-input.contrast( - ng-model='self.state.formData.passcode', - placeholder='Passcode', - should-focus='true', - sn-autofocus='true', + ng-model='self.state.formData.passcode', + placeholder='Passcode', + should-focus='true', + sn-autofocus='true', type='password' ) input.sk-input.contrast( - ng-model='self.state.formData.confirmPasscode', - placeholder='Confirm Passcode', + ng-model='self.state.formData.confirmPasscode', + placeholder='Confirm Passcode', type='password' ) .sk-button-group.stretch.sk-panel-row.form-submit @@ -254,15 +254,15 @@ a.sk-a.info( ng-class=`{ 'boxed' : option.value == self.state.selectedAutoLockInterval - }`, - ng-click='self.selectAutoLockInterval(option.value)', + }`, + ng-click='self.selectAutoLockInterval(option.value)', ng-repeat='option in self.state.passcodeAutoLockOptions' ) | {{option.label}} .sk-p The autolock timer begins when the window or tab loses focus. .sk-panel-row a.sk-a.info.sk-panel-row.condensed( - ng-click="self.openPrivilegesModal('')", + ng-click="self.openPrivilegesModal('')", ng-show='!self.state.user' ) Manage Privileges a.sk-a.info.sk-panel-row.condensed( @@ -280,17 +280,17 @@ .sk-input-group label.sk-horizontal-group.tight input( - ng-change='self.state.mutable.backupEncrypted = true', - ng-model='self.state.mutable.backupEncrypted', - ng-value='true', + ng-change='self.state.mutable.backupEncrypted = true', + ng-model='self.state.mutable.backupEncrypted', + ng-value='true', type='radio' ) p.sk-p Encrypted label.sk-horizontal-group.tight input( - ng-change='self.state.mutable.backupEncrypted = false', - ng-model='self.state.mutable.backupEncrypted', - ng-value='false', + ng-change='self.state.mutable.backupEncrypted = false', + ng-model='self.state.mutable.backupEncrypted', + ng-value='false', type='radio' ) p.sk-p Decrypted @@ -300,32 +300,32 @@ .sk-label Download Backup label.sk-button.info input( - file-change='->', - handler='self.importFileSelected(files)', - style='display: none;', + file-change='->', + handler='self.importFileSelected(files)', + style='display: none;', type='file' ) .sk-label Import Backup span(ng-if='self.isDesktopApplication()') - | Backups are automatically created on desktop and can be managed + | Backups are automatically created on desktop and can be managed | via the "Backups" top-level menu. #import-password-request(ng-if='self.state.importData.requestPassword') form.sk-panel-form.stretch(ng-submit='self.submitImportPassword()') p Enter the account password associated with the import file. input.sk-input.contrast.mt-5( - autofocus='true', - ng-model='self.state.importData.password', - placeholder='Enter File Account Password', + autofocus='true', + ng-model='self.state.importData.password', + placeholder='Enter File Account Password', type='password' ) .sk-button-group.stretch.sk-panel-row.form-submit button.sk-button.info(type='submit') .sk-label Decrypt & Import p - | Importing from backup will not overwrite existing data, + | Importing from backup will not overwrite existing data, | but instead create a duplicate of any differing data. p - | If you'd like to import only a selection of items instead of + | If you'd like to import only a selection of items instead of | the whole file, please use the Batch Manager extension. .sk-panel-row .sk-spinner.small.info(ng-if='self.state.importData.loading') @@ -338,9 +338,9 @@ ) | Cancel a.sk-a.right.danger( - ng-click='self.destroyLocalData()', + ng-click='self.destroyLocalData()', ng-if=` - !self.state.formData.showLogin && + !self.state.formData.showLogin && !self.state.formData.showRegister` ) - | {{ self.state.user ? "Sign out and clear local data" : "Clear all local data" }} + | {{ self.state.user ? "Sign out" : "Clear session data" }} diff --git a/dist/@types/app/assets/javascripts/database.d.ts b/dist/@types/app/assets/javascripts/database.d.ts index 7cea92fd2..457736ca3 100644 --- a/dist/@types/app/assets/javascripts/database.d.ts +++ b/dist/@types/app/assets/javascripts/database.d.ts @@ -1,10 +1,11 @@ -import { SNAlertService } from "@node_modules/snjs/dist/@types"; +import { SNAlertService } from "snjs/dist/@types"; export declare class Database { + databaseName: string; + private alertService; private locked; - private alertService?; private db?; + constructor(databaseName: string, alertService: SNAlertService); deinit(): void; - setAlertService(alertService: SNAlertService): void; /** * Relinquishes the lock and allows db operations to proceed */ diff --git a/dist/@types/app/assets/javascripts/services/alertService.d.ts b/dist/@types/app/assets/javascripts/services/alertService.d.ts index 1d3afcdb9..f0597949e 100644 --- a/dist/@types/app/assets/javascripts/services/alertService.d.ts +++ b/dist/@types/app/assets/javascripts/services/alertService.d.ts @@ -18,5 +18,5 @@ export declare class AlertService implements SNAlertService { */ alert(text: string, title?: string, closeButtonText?: string): Promise; confirm(text: string, title?: string, confirmButtonText?: string, confirmButtonType?: ButtonType, cancelButtonText?: string): Promise; - blockingDialog(text: string): () => void; + blockingDialog(text: string, title?: string): () => void; } diff --git a/dist/@types/app/assets/javascripts/services/autolock_service.d.ts b/dist/@types/app/assets/javascripts/services/autolock_service.d.ts new file mode 100644 index 000000000..7a0d92401 --- /dev/null +++ b/dist/@types/app/assets/javascripts/services/autolock_service.d.ts @@ -0,0 +1,27 @@ +import { WebApplication } from '@/ui_models/application'; +export declare class AutolockService { + private application; + private unsubState; + private pollFocusInterval; + private lastFocusState?; + private lockAfterDate?; + private lockTimeout?; + constructor(application: WebApplication); + observeVisibility(): void; + deinit(): void; + private lockApplication; + setAutoLockInterval(interval: number): Promise; + getAutoLockInterval(): Promise; + /** + * Verify document is in focus every so often as visibilitychange event is + * not triggered on a typical window blur event but rather on tab changes. + */ + beginWebFocusPolling(): void; + getAutoLockIntervalOptions(): { + value: number; + label: string; + }[]; + documentVisibilityChanged(visible: boolean): Promise; + beginAutoLockTimer(): Promise; + cancelAutoLockTimer(): void; +} diff --git a/dist/@types/app/assets/javascripts/services/index.d.ts b/dist/@types/app/assets/javascripts/services/index.d.ts index 3de0713bf..b63b520e8 100644 --- a/dist/@types/app/assets/javascripts/services/index.d.ts +++ b/dist/@types/app/assets/javascripts/services/index.d.ts @@ -2,7 +2,7 @@ export { AlertService } from './alertService'; export { ArchiveManager } from './archiveManager'; export { DesktopManager } from './desktopManager'; export { KeyboardManager } from './keyboardManager'; -export { LockManager } from './lockManager'; +export { AutolockService } from './autolock_service'; export { NativeExtManager } from './nativeExtManager'; export { PreferencesManager } from './preferencesManager'; export { StatusManager } from './statusManager'; diff --git a/dist/@types/app/assets/javascripts/services/nativeExtManager.d.ts b/dist/@types/app/assets/javascripts/services/nativeExtManager.d.ts index f0d040bdf..4bca8a757 100644 --- a/dist/@types/app/assets/javascripts/services/nativeExtManager.d.ts +++ b/dist/@types/app/assets/javascripts/services/nativeExtManager.d.ts @@ -1,5 +1,5 @@ import { SNPredicate, ApplicationService } from 'snjs'; -import { PayloadContent } from '@node_modules/snjs/dist/@types/protocol/payloads/generator'; +import { PayloadContent } from 'snjs/dist/@types/protocol/payloads/generator'; /** A class for handling installation of system extensions */ export declare class NativeExtManager extends ApplicationService { extManagerId: string; diff --git a/dist/@types/app/assets/javascripts/strings.d.ts b/dist/@types/app/assets/javascripts/strings.d.ts index f36f46c02..a5ca2ad34 100644 --- a/dist/@types/app/assets/javascripts/strings.d.ts +++ b/dist/@types/app/assets/javascripts/strings.d.ts @@ -32,9 +32,10 @@ export declare const STRING_GENERATING_LOGIN_KEYS = "Generating Login Keys..."; export declare const STRING_GENERATING_REGISTER_KEYS = "Generating Account Keys..."; export declare const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a proper JSON file and try again."; export declare function StringImportError(errorCount: number): string; -export declare const STRING_ENTER_ACCOUNT_PASSCODE = "Enter your application passcode"; +export declare const STRING_ENTER_ACCOUNT_PASSCODE = "Enter your application passcode to unlock the application"; export declare const STRING_ENTER_ACCOUNT_PASSWORD = "Enter your account password"; export declare const STRING_ENTER_PASSCODE_FOR_MIGRATION = "Your application passcode is required to perform an upgrade of your local data storage structure."; +export declare const STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER = "Enter your application passcode before signing in or registering"; export declare const STRING_STORAGE_UPDATE = "Storage Update"; export declare const STRING_AUTHENTICATION_REQUIRED = "Authentication Required"; export declare const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = "This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again."; diff --git a/dist/@types/app/assets/javascripts/types.d.ts b/dist/@types/app/assets/javascripts/types.d.ts index 2e0727d6b..1bfddb0f0 100644 --- a/dist/@types/app/assets/javascripts/types.d.ts +++ b/dist/@types/app/assets/javascripts/types.d.ts @@ -28,6 +28,9 @@ export interface PermissionsModalScope extends Partial { permissionsString: string; callback: (approved: boolean) => void; } +export interface AccountSwitcherScope extends Partial { + application: any; +} export declare type PanelPuppet = { onReady?: () => void; ready?: boolean; diff --git a/dist/@types/app/assets/javascripts/ui_models/application.d.ts b/dist/@types/app/assets/javascripts/ui_models/application.d.ts index 87a609894..12223b172 100644 --- a/dist/@types/app/assets/javascripts/ui_models/application.d.ts +++ b/dist/@types/app/assets/javascripts/ui_models/application.d.ts @@ -3,13 +3,15 @@ import { ComponentGroup } from './component_group'; import { EditorGroup } from '@/ui_models/editor_group'; import { PasswordWizardType } from '@/types'; import { SNApplication, Challenge, ProtectedAction } from 'snjs'; -import { DesktopManager, LockManager, ArchiveManager, NativeExtManager, StatusManager, ThemeManager, PreferencesManager, KeyboardManager } from '@/services'; +import { WebDeviceInterface } from '@/web_device_interface'; +import { DesktopManager, AutolockService, ArchiveManager, NativeExtManager, StatusManager, ThemeManager, PreferencesManager, KeyboardManager } from '@/services'; import { AppState } from '@/ui_models/app_state'; import { Bridge } from '@/services/bridge'; +import { DeinitSource } from 'snjs/dist/@types/types'; declare type WebServices = { appState: AppState; desktopService: DesktopManager; - lockService: LockManager; + autolockService: AutolockService; archiveService: ArchiveManager; nativeExtService: NativeExtManager; statusService: StatusManager; @@ -20,18 +22,17 @@ declare type WebServices = { export declare class WebApplication extends SNApplication { private $compile?; private scope?; - private onDeinit?; private webServices; private currentAuthenticationElement?; editorGroup: EditorGroup; componentGroup: ComponentGroup; - constructor($compile: ng.ICompileService, $timeout: ng.ITimeoutService, scope: ng.IScope, onDeinit: (app: WebApplication) => void, defaultSyncServerHost: string, bridge: Bridge); + constructor(deviceInterface: WebDeviceInterface, identifier: string, $compile: ng.ICompileService, scope: ng.IScope, defaultSyncServerHost: string, bridge: Bridge); /** @override */ - deinit(): void; + deinit(source: DeinitSource): void; setWebServices(services: WebServices): void; getAppState(): AppState; getDesktopService(): DesktopManager; - getLockService(): LockManager; + getAutolockService(): AutolockService; getArchiveService(): ArchiveManager; getNativeExtService(): NativeExtManager; getStatusService(): StatusManager; @@ -47,5 +48,6 @@ export declare class WebApplication extends SNApplication { authenticationInProgress(): boolean; presentPasswordModal(callback: () => void): void; presentRevisionPreviewModal(uuid: string, content: any): void; + openAccountSwitcher(): void; } export {}; diff --git a/dist/@types/app/assets/javascripts/ui_models/application_group.d.ts b/dist/@types/app/assets/javascripts/ui_models/application_group.d.ts index 01558e1bc..250918e0c 100644 --- a/dist/@types/app/assets/javascripts/ui_models/application_group.d.ts +++ b/dist/@types/app/assets/javascripts/ui_models/application_group.d.ts @@ -1,29 +1,13 @@ /// -import { WebApplication } from './application'; +import { SNApplicationGroup } from 'snjs'; import { Bridge } from '@/services/bridge'; -declare type AppManagerChangeCallback = () => void; -export declare class ApplicationGroup { +export declare class ApplicationGroup extends SNApplicationGroup { private defaultSyncServerHost; private bridge; $compile: ng.ICompileService; $rootScope: ng.IRootScopeService; $timeout: ng.ITimeoutService; - applications: WebApplication[]; - changeObservers: AppManagerChangeCallback[]; - activeApplication?: WebApplication; constructor($compile: ng.ICompileService, $rootScope: ng.IRootScopeService, $timeout: ng.ITimeoutService, defaultSyncServerHost: string, bridge: Bridge); - private createDefaultApplication; - /** @callback */ - onApplicationDeinit(application: WebApplication): void; - private createNewApplication; - get application(): WebApplication | undefined; - getApplications(): WebApplication[]; - /** - * Notifies observer when the active application has changed. - * Any application which is no longer active is destroyed, and - * must be removed from the interface. - */ - addApplicationChangeObserver(callback: AppManagerChangeCallback): () => void; - private notifyObserversOfAppChange; + initialize(callback?: any): Promise; + private createApplication; } -export {}; diff --git a/dist/@types/app/assets/javascripts/ui_models/component_group.d.ts b/dist/@types/app/assets/javascripts/ui_models/component_group.d.ts index fde939585..2c2d7a925 100644 --- a/dist/@types/app/assets/javascripts/ui_models/component_group.d.ts +++ b/dist/@types/app/assets/javascripts/ui_models/component_group.d.ts @@ -1,6 +1,6 @@ import { SNComponent, ComponentArea } from 'snjs'; import { WebApplication } from './application'; -import { UuidString } from '@node_modules/snjs/dist/@types/types'; +import { UuidString } from 'snjs/dist/@types/types'; export declare class ComponentGroup { private application; changeObservers: any[]; diff --git a/dist/@types/app/assets/javascripts/views/account_switcher/account_switcher.d.ts b/dist/@types/app/assets/javascripts/views/account_switcher/account_switcher.d.ts new file mode 100644 index 000000000..8974f7f1d --- /dev/null +++ b/dist/@types/app/assets/javascripts/views/account_switcher/account_switcher.d.ts @@ -0,0 +1,4 @@ +import { WebDirective } from '@/types'; +export declare class AccountSwitcher extends WebDirective { + constructor(); +} diff --git a/dist/@types/app/assets/javascripts/views/notes/note_utils.d.ts b/dist/@types/app/assets/javascripts/views/notes/note_utils.d.ts index 2f0abe733..3b23a9c69 100644 --- a/dist/@types/app/assets/javascripts/views/notes/note_utils.d.ts +++ b/dist/@types/app/assets/javascripts/views/notes/note_utils.d.ts @@ -1,4 +1,4 @@ -import { SNNote, SNTag } from 'snjs'; +import { SNNote } from 'snjs'; export declare enum NoteSortKey { CreatedAt = "created_at", UserUpdatedAt = "userModifiedDate", @@ -8,4 +8,4 @@ export declare enum NoteSortKey { /** @legacy Use UserUpdatedAt instead */ ClientUpdatedAt = "client_updated_at" } -export declare function notePassesFilter(note: SNNote, selectedTag: SNTag, showArchived: boolean, hidePinned: boolean, filterText: string): boolean; +export declare function notePassesFilter(note: SNNote, showArchived: boolean, hidePinned: boolean, filterText: string): boolean; diff --git a/dist/@types/app/assets/javascripts/web_device_interface.d.ts b/dist/@types/app/assets/javascripts/web_device_interface.d.ts index 3e2e61b18..4d2c9556e 100644 --- a/dist/@types/app/assets/javascripts/web_device_interface.d.ts +++ b/dist/@types/app/assets/javascripts/web_device_interface.d.ts @@ -1,10 +1,11 @@ -import { DeviceInterface, SNApplication } from 'snjs'; +import { DeviceInterface, SNApplication, ApplicationIdentifier } from 'snjs'; import { Bridge } from './services/bridge'; export declare class WebDeviceInterface extends DeviceInterface { private bridge; - private database; + private databases; constructor(timeout: any, bridge: Bridge); setApplication(application: SNApplication): void; + private databaseForIdentifier; deinit(): void; getRawStorageValue(key: string): Promise; getAllRawStorageKeyValues(): Promise<{ @@ -14,19 +15,17 @@ export declare class WebDeviceInterface extends DeviceInterface { setRawStorageValue(key: string, value: any): Promise; removeRawStorageValue(key: string): Promise; removeAllRawStorageValues(): Promise; - openDatabase(): Promise<{ + openDatabase(identifier: ApplicationIdentifier): Promise<{ isNewDatabase?: boolean | undefined; } | undefined>; - private getDatabaseKeyPrefix; - private keyForPayloadId; - getAllRawDatabasePayloads(): Promise; - saveRawDatabasePayload(payload: any): Promise; - saveRawDatabasePayloads(payloads: any[]): Promise; - removeRawDatabasePayloadWithId(id: string): Promise; - removeAllRawDatabasePayloads(): Promise; - getNamespacedKeychainValue(): Promise; - setNamespacedKeychainValue(value: any): Promise; - clearNamespacedKeychainValue(): Promise; + getAllRawDatabasePayloads(identifier: ApplicationIdentifier): Promise; + saveRawDatabasePayload(payload: any, identifier: ApplicationIdentifier): Promise; + saveRawDatabasePayloads(payloads: any[], identifier: ApplicationIdentifier): Promise; + removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier): Promise; + removeAllRawDatabasePayloads(identifier: ApplicationIdentifier): Promise; + getNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise; + setNamespacedKeychainValue(value: any, identifier: ApplicationIdentifier): Promise; + clearNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise; getRawKeychainValue(): Promise; clearRawKeychainValue(): Promise; openUrl(url: string): void; diff --git a/package-lock.json b/package-lock.json index 84de48359..e339395dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.5.0-beta3", + "version": "3.5.0-beta2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -10956,8 +10956,8 @@ "from": "github:standardnotes/sncrypto#8794c88daa967eaae493cd5fdec7506d52b257ad" }, "snjs": { - "version": "github:standardnotes/snjs#a39193f71cdcf1c02a7f0fa0cb96850f07b1717f", - "from": "github:standardnotes/snjs#a39193f71cdcf1c02a7f0fa0cb96850f07b1717f" + "version": "github:standardnotes/snjs#b029e6f7da367fecc40acab6e378e40ce247505a", + "from": "github:standardnotes/snjs#b029e6f7da367fecc40acab6e378e40ce247505a" }, "sockjs": { "version": "0.3.20", diff --git a/package.json b/package.json index a9b0deaf4..c599bf531 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.5.0-beta3", + "version": "3.5.0-beta2", "license": "AGPL-3.0-or-later", "repository": { "type": "git", @@ -11,7 +11,6 @@ "start": "webpack-dev-server --progress --config webpack.dev.js", "watch": "webpack -w --config webpack.dev.js", "bundle": "webpack --config webpack.prod.js && npm run tsc", - "bundle:desktop": "webpack --config webpack.prod.js --env.platform='desktop'", "build": "bundle install && npm ci && bundle exec rails assets:precompile && npm run bundle", "submodules": "git submodule update --init --force", "lint": "eslint --fix app/assets/javascripts/**/*.js", @@ -68,6 +67,6 @@ }, "dependencies": { "sncrypto": "github:standardnotes/sncrypto#8794c88daa967eaae493cd5fdec7506d52b257ad", - "snjs": "github:standardnotes/snjs#a39193f71cdcf1c02a7f0fa0cb96850f07b1717f" + "snjs": "github:standardnotes/snjs#b029e6f7da367fecc40acab6e378e40ce247505a" } }