Remove dummy concept in favor of editor group and editors

This commit is contained in:
Mo Bitar
2020-04-14 15:01:32 -05:00
parent ef66170ba4
commit 9cf99896a5
81 changed files with 8136 additions and 7179 deletions

View File

@@ -0,0 +1,258 @@
import { EditorGroup } from '@/ui_models/editor_group';
import { InputModalScope } from '@/directives/views/inputModal';
import { PasswordWizardType, PasswordWizardScope } from '@/types';
import {
Environment,
SNApplication,
SNAlertService,
platformFromString,
Challenge,
ChallengeOrchestrator,
ProtectedAction
} from 'snjs';
import angular from 'angular';
import { getPlatformString } from '@/utils';
import { AlertService } from '@/services/alertService';
import { WebDeviceInterface } from '@/web_device_interface';
import {
AppState,
DesktopManager,
LockManager,
ArchiveManager,
NativeExtManager,
StatusManager,
ThemeManager,
PreferencesManager,
KeyboardManager
} from '@/services';
type WebServices = {
appState: AppState
desktopService: DesktopManager
lockService: LockManager
archiveService: ArchiveManager
nativeExtService: NativeExtManager
statusService: StatusManager
themeService: ThemeManager
prefsService: PreferencesManager
keyboardService: KeyboardManager
}
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
/* @ngInject */
constructor(
$compile: ng.ICompileService,
$timeout: ng.ITimeoutService,
scope: ng.IScope,
onDeinit: (app: WebApplication) => void
) {
const namespace = '';
const deviceInterface = new WebDeviceInterface(namespace, $timeout);
super(
Environment.Web,
platformFromString(getPlatformString()),
deviceInterface,
namespace,
undefined,
[
{
swap: SNAlertService,
with: AlertService
}
]
);
this.$compile = $compile;
this.scope = scope;
this.onDeinit = onDeinit;
deviceInterface.setApplication(this);
this.editorGroup = new EditorGroup(this);
}
/** @override */
deinit() {
for (const key of Object.keys(this.webServices)) {
const service = (this.webServices as any)[key];
if (service.deinit) {
service.deinit();
}
service.application = undefined;
}
this.webServices = {} as WebServices;
this.onDeinit!(this);
this.onDeinit = undefined;
this.$compile = undefined;
this.editorGroup.deinit();
(this.scope! as any).application = undefined;
this.scope!.$destroy();
this.scope = undefined;
super.deinit();
}
setWebServices(services: WebServices) {
this.webServices = services;
}
/** @access public */
getAppState() {
return this.webServices.appState;
}
/** @access public */
getDesktopService() {
return this.webServices.desktopService;
}
/** @access public */
getLockService() {
return this.webServices.lockService;
}
/** @access public */
getArchiveService() {
return this.webServices.archiveService;
}
/** @access public */
getNativeExtService() {
return this.webServices.nativeExtService;
}
/** @access public */
getStatusService() {
return this.webServices.statusService;
}
/** @access public */
getThemeService() {
return this.webServices.themeService;
}
/** @access public */
getPrefsService() {
return this.webServices.prefsService;
}
/** @access public */
getKeyboardService() {
return this.webServices.keyboardService;
}
async checkForSecurityUpdate() {
return this.protocolUpgradeAvailable();
}
presentPasswordWizard(type: PasswordWizardType) {
const scope = this.scope!.$new(true) as PasswordWizardScope;
scope.type = type;
scope.application = this;
const el = this.$compile!(
"<password-wizard application='application' type='type'></password-wizard>"
)(scope as any);
angular.element(document.body).append(el);
}
promptForChallenge(challenge: Challenge, orchestrator: ChallengeOrchestrator) {
const scope: any = this.scope!.$new(true);
scope.challenge = challenge;
scope.orchestrator = orchestrator;
scope.application = this;
const el = this.$compile!(
"<challenge-modal " +
"class='sk-modal' application='application' challenge='challenge' orchestrator='orchestrator'>" +
"</challenge-modal>"
)(scope);
angular.element(document.body).append(el);
}
async performProtocolUpgrade() {
const errors = await this.upgradeProtocolVersion();
if (!errors || errors.length === 0) {
this.alertService!.alert(
"Success! Your encryption version has been upgraded." +
" You'll be asked to enter your credentials again on other devices you're signed into."
);
} else {
this.alertService!.alert(
"Unable to upgrade encryption version. Please try again."
);
}
}
async presentPrivilegesModal(
action: ProtectedAction,
onSuccess?: any,
onCancel?: any
) {
if (this.authenticationInProgress()) {
onCancel && onCancel();
return;
}
const customSuccess = async () => {
onSuccess && await onSuccess();
this.currentAuthenticationElement = undefined;
};
const customCancel = async () => {
onCancel && await onCancel();
this.currentAuthenticationElement = undefined;
};
const scope: any = this.scope!.$new(true);
scope.action = action;
scope.onSuccess = customSuccess;
scope.onCancel = customCancel;
scope.application = this;
const el = this.$compile!(`
<privileges-auth-modal application='application' action='action' on-success='onSuccess'
on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>
`)(scope);
angular.element(document.body).append(el);
this.currentAuthenticationElement = el;
}
presentPrivilegesManagementModal() {
const scope: any = this.scope!.$new(true);
scope.application = this;
const el = this.$compile!("<privileges-management-modal application='application' class='sk-modal'></privileges-management-modal>")(scope);
angular.element(document.body).append(el);
}
authenticationInProgress() {
return this.currentAuthenticationElement != null;
}
presentPasswordModal(callback: () => void) {
const scope = this.scope!.$new(true) as InputModalScope;
scope.type = "password";
scope.title = "Decryption Assistance";
scope.message = `Unable to decrypt this item with your current keys.
Please enter your account password at the time of this revision.`;
scope.callback = callback;
const el = this.$compile!(
`<input-modal type='type' message='message'
title='title' callback='callback()'></input-modal>`
)(scope as any);
angular.element(document.body).append(el);
}
presentRevisionPreviewModal(uuid: string, content: any) {
const scope: any = this.scope!.$new(true);
scope.uuid = uuid;
scope.content = content;
scope.application = this;
const el = this.$compile!(
`<revision-preview-modal application='application' uuid='uuid' content='content'
class='sk-modal'></revision-preview-modal>`
)(scope);
angular.element(document.body).append(el);
}
}

View File

@@ -0,0 +1,131 @@
import { WebApplication } from './application';
import { removeFromArray } from 'snjs';
import {
ArchiveManager,
DesktopManager,
KeyboardManager,
LockManager,
NativeExtManager,
PreferencesManager,
StatusManager,
ThemeManager,
AppState
} from '@/services';
type AppManagerChangeCallback = () => void
export class ApplicationGroup {
$compile: ng.ICompileService
$rootScope: ng.IRootScopeService
$timeout: ng.ITimeoutService
applications: WebApplication[] = []
changeObservers: AppManagerChangeCallback[] = []
activeApplication?: WebApplication
/* @ngInject */
constructor(
$compile: ng.ICompileService,
$rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService
) {
this.$compile = $compile;
this.$timeout = $timeout;
this.$rootScope = $rootScope;
this.onApplicationDeinit = this.onApplicationDeinit.bind(this);
this.createDefaultApplication();
}
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();
}
this.notifyObserversOfAppChange();
}
private createNewApplication() {
const scope = this.$rootScope.$new(true);
const application = new WebApplication(
this.$compile,
this.$timeout,
scope,
this.onApplicationDeinit
);
const appState = new AppState(
this.$rootScope,
this.$timeout,
application
);
const archiveService = new ArchiveManager(
application
);
const desktopService = new DesktopManager(
this.$rootScope,
this.$timeout,
application
);
const keyboardService = new KeyboardManager();
const lockService = new LockManager(
application
);
const nativeExtService = new NativeExtManager(
application
);
const prefsService = new PreferencesManager(
application
);
const statusService = new StatusManager();
const themeService = new ThemeManager(
application,
);
application.setWebServices({
appState,
archiveService,
desktopService,
keyboardService,
lockService,
nativeExtService,
prefsService,
statusService,
themeService
});
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();
}
}
private notifyObserversOfAppChange() {
for (const observer of this.changeObservers) {
observer();
}
}
}

View File

@@ -0,0 +1,104 @@
import { SNNote, ContentType, PayloadSource } from 'snjs';
import { WebApplication } from './application';
export class Editor {
public note!: SNNote
private application: WebApplication
private _onNoteChange?: () => void
private _onNoteValueChange?: (note: SNNote, source?: PayloadSource) => void
private removeStreamObserver: () => void
public isTemplateNote = true
constructor(
application: WebApplication,
noteUuid?: string,
noteTitle?: string
) {
this.application = application;
if (noteUuid) {
this.note = application.findItem(noteUuid) as SNNote;
} else {
this.reset(noteTitle);
}
this.removeStreamObserver = this.application.streamItems(
ContentType.Note,
async (items, source) => {
await this.handleNoteStream(items as SNNote[], source);
}
);
}
private async handleNoteStream(notes: SNNote[], source?: PayloadSource) {
/** Update our note object reference whenever it changes */
const matchingNote = notes.find((item) => {
return item.uuid === this.note.uuid;
}) as SNNote;
if (matchingNote) {
this.isTemplateNote = false;
this.note = matchingNote;
this._onNoteValueChange!(matchingNote, source);
}
}
async insertTemplatedNote() {
return this.application.insertItem(this.note);
}
/**
* Reverts the editor to a blank state, removing any existing note from view,
* and creating a placeholder note.
*/
async reset(noteTitle?: string) {
const note = await this.application.createTemplateItem(
ContentType.Note,
{
text: '',
title: noteTitle || '',
references: []
}
);
this.isTemplateNote = true;
this.setNote(note as SNNote);
}
deinit() {
this.removeStreamObserver();
(this.removeStreamObserver as any) = undefined;
this._onNoteChange = undefined;
(this.application as any) = undefined;
this._onNoteChange = undefined;
this._onNoteValueChange = undefined;
}
/**
* Register to be notified when the editor's note changes.
*/
public onNoteChange(onNoteChange: () => void) {
this._onNoteChange = onNoteChange;
if (this.note) {
onNoteChange();
}
}
/**
* Register to be notified when the editor's note's values change
* (and thus a new object reference is created)
*/
public onNoteValueChange(
onNoteValueChange: (note: SNNote, source?: PayloadSource) => void
) {
this._onNoteValueChange = onNoteValueChange;
}
/**
* Sets the editor contents by setting its note.
*/
public setNote(note: SNNote) {
this.note = note;
if (this._onNoteChange) {
this._onNoteChange();
}
}
}

View File

@@ -0,0 +1,69 @@
import { removeFromArray } from 'snjs';
import { Editor } from './editor';
import { WebApplication } from './application';
type EditorGroupChangeCallback = () => void
export class EditorGroup {
public editors: Editor[] = []
private application: WebApplication
changeObservers: EditorGroupChangeCallback[] = []
constructor(application: WebApplication) {
this.application = application;
}
public deinit() {
(this.application as any) = undefined;
for (const editor of this.editors) {
this.deleteEditor(editor);
}
}
createEditor(noteUuid?: string, noteTitle?: string) {
const editor = new Editor(this.application, noteUuid, noteTitle);
this.editors.push(editor);
this.notifyObservers();
}
deleteEditor(editor: Editor) {
editor.deinit();
removeFromArray(this.editors, editor);
}
closeEditor(editor: Editor) {
this.deleteEditor(editor);
this.notifyObservers();
}
closeActiveEditor() {
this.deleteEditor(this.editors[0]);
}
closeAllEditors() {
for(const editor of this.editors) {
this.deleteEditor(editor);
}
}
get activeEditor() {
return this.editors[0];
}
/**
* Notifies observer when the active editor has changed.
*/
public addChangeObserver(callback: EditorGroupChangeCallback) {
this.changeObservers.push(callback);
if (this.activeEditor) {
callback();
}
}
private notifyObservers() {
for (const observer of this.changeObservers) {
observer();
}
}
}