Refactors most controllers and directives into classes for more organized and maintainable code

This commit is contained in:
Mo Bitar
2020-01-30 13:37:16 -06:00
parent badadba8f8
commit 3c8c43ac7e
144 changed files with 87972 additions and 5613 deletions

View File

@@ -0,0 +1,16 @@
/* @ngInject */
export function autofocus($timeout) {
return {
restrict: 'A',
scope: {
shouldFocus: '='
},
link: function($scope, $element) {
$timeout(function() {
if ($scope.shouldFocus) {
$element[0].focus();
}
});
}
};
}

View File

@@ -0,0 +1,29 @@
/* @ngInject */
export function clickOutside($document) {
return {
restrict: 'A',
replace: false,
link: function($scope, $element, attrs) {
var didApplyClickOutside = false;
$element.bind('click', function(e) {
didApplyClickOutside = false;
if (attrs.isOpen) {
e.stopPropagation();
}
});
$document.bind('click', function() {
// Ignore click if on SKAlert
if (event.target.closest(".sk-modal")) {
return;
}
if (!didApplyClickOutside) {
$scope.$apply(attrs.clickOutside);
didApplyClickOutside = true;
}
});
}
};
}

View File

@@ -0,0 +1,44 @@
import angular from 'angular';
/* @ngInject */
export function delayHide($timeout) {
return {
restrict: 'A',
scope: {
show: '=',
delay: '@'
},
link: function(scope, elem, attrs) {
showElement(false);
// This is where all the magic happens!
// Whenever the scope variable updates we simply
// show if it evaluates to 'true' and hide if 'false'
scope.$watch('show', function(newVal) {
newVal ? showSpinner() : hideSpinner();
});
function showSpinner() {
if (scope.hidePromise) {
$timeout.cancel(scope.hidePromise);
scope.hidePromise = null;
}
showElement(true);
}
function hideSpinner() {
scope.hidePromise = $timeout(showElement.bind(this, false), getDelay());
}
function showElement(show) {
show ? elem.css({ display: '' }) : elem.css({ display: 'none' });
}
function getDelay() {
var delay = parseInt(scope.delay);
return angular.isNumber(delay) ? delay : 200;
}
}
};
}

View File

@@ -0,0 +1,14 @@
/* @ngInject */
export function elemReady($parse) {
return {
restrict: 'A',
link: function($scope, elem, attrs) {
elem.ready(function() {
$scope.$apply(function() {
var func = $parse(attrs.elemReady);
func($scope);
});
});
}
};
}

View File

@@ -0,0 +1,16 @@
/* @ngInject */
export function fileChange() {
return {
restrict: 'A',
scope: {
handler: '&'
},
link: function(scope, element) {
element.on('change', function(event) {
scope.$apply(function() {
scope.handler({ files: event.target.files });
});
});
}
};
}

View File

@@ -0,0 +1,9 @@
export { autofocus } from './autofocus';
export { clickOutside } from './click-outside';
export { delayHide } from './delay-hide';
export { elemReady } from './elemReady';
export { fileChange } from './file-change';
export { infiniteScroll } from './infiniteScroll';
export { lowercase } from './lowercase';
export { selectOnClick } from './selectOnClick';
export { snEnter } from './snEnter';

View File

@@ -0,0 +1,17 @@
/* @ngInject */
export function infiniteScroll($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
const offset = parseInt(attrs.threshold) || 0;
const e = elem[0];
elem.on('scroll', function() {
if (
scope.$eval(attrs.canLoad) &&
e.scrollTop + e.offsetHeight >= e.scrollHeight - offset
) {
scope.$apply(attrs.infiniteScroll);
}
});
}
};
}

View File

@@ -0,0 +1,19 @@
/* @ngInject */
export function lowercase() {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var lowercase = function(inputValue) {
if (inputValue === undefined) inputValue = '';
var lowercased = inputValue.toLowerCase();
if (lowercased !== inputValue) {
modelCtrl.$setViewValue(lowercased);
modelCtrl.$render();
}
return lowercased;
};
modelCtrl.$parsers.push(lowercase);
lowercase(scope[attrs.ngModel]);
}
};
}

View File

@@ -0,0 +1,14 @@
/* @ngInject */
export function selectOnClick($window) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('focus', function() {
if (!$window.getSelection().toString()) {
/** Required for mobile Safari */
this.setSelectionRange(0, this.value.length);
}
});
}
};
}

View File

@@ -0,0 +1,14 @@
/* @ngInject */
export function snEnter() {
return function(scope, element, attrs) {
element.bind('keydown keypress', function(event) {
if (event.which === 13) {
scope.$apply(function() {
scope.$eval(attrs.snEnter, { event: event });
});
event.preventDefault();
}
});
};
}

View File

@@ -0,0 +1,563 @@
import { isDesktopApplication, isNullOrUndefined } from '@/utils';
import { PrivilegesManager } from '@/services/privilegesManager';
import template from '%/directives/account-menu.pug';
import { protocolManager } from 'snjs';
import { PureCtrl } from '@Controllers';
import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
STRING_SIGN_OUT_CONFIRMATION,
STRING_ERROR_DECRYPTING_IMPORT,
STRING_E2E_ENABLED,
STRING_LOCAL_ENC_ENABLED,
STRING_ENC_NOT_ENABLED,
STRING_IMPORT_SUCCESS,
STRING_REMOVE_PASSCODE_CONFIRMATION,
STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM,
STRING_NON_MATCHING_PASSCODES,
STRING_NON_MATCHING_PASSWORDS,
STRING_INVALID_IMPORT_FILE,
STRING_GENERATING_LOGIN_KEYS,
STRING_GENERATING_REGISTER_KEYS,
StringImportError
} from '@/strings';
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
class AccountMenuCtrl extends PureCtrl {
/* @ngInject */
constructor(
$scope,
$rootScope,
$timeout,
alertManager,
archiveManager,
appVersion,
authManager,
modelManager,
passcodeManager,
privilegesManager,
storageManager,
syncManager,
) {
super($timeout);
this.$scope = $scope;
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.alertManager = alertManager;
this.archiveManager = archiveManager;
this.authManager = authManager;
this.modelManager = modelManager;
this.passcodeManager = passcodeManager;
this.privilegesManager = privilegesManager;
this.storageManager = storageManager;
this.syncManager = syncManager;
this.state = {
appVersion: 'v' + (window.electronAppVersion || appVersion),
user: this.authManager.user,
canAddPasscode: !this.authManager.isEphemeralSession(),
passcodeAutoLockOptions: this.passcodeManager.getAutoLockIntervalOptions(),
formData: {
mergeLocal: true,
ephemeral: false
},
mutable: {
backupEncrypted: this.encryptedBackupsAvailable()
}
}
this.syncStatus = this.syncManager.syncStatus;
this.syncManager.getServerURL().then((url) => {
this.setState({
server: url,
formData: { ...this.state.formData, url: url }
})
})
this.authManager.checkForSecurityUpdate().then((available) => {
this.setState({
securityUpdateAvailable: available
})
})
this.reloadAutoLockInterval();
}
$onInit() {
this.initProps({
closeFunction: this.closeFunction
})
}
close() {
this.$timeout(() => {
this.props.closeFunction()();
})
}
encryptedBackupsAvailable() {
return !isNullOrUndefined(this.authManager.user) || this.passcodeManager.hasPasscode();
}
submitMfaForm() {
const params = {
[this.state.formData.mfa.payload.mfa_key]: this.state.formData.userMfaCode
};
this.login(params);
}
submitAuthForm() {
if (!this.state.formData.email || !this.state.formData.user_password) {
return;
}
if (this.state.formData.showLogin) {
this.login();
} else {
this.register();
}
}
async login(extraParams) {
/** Prevent a timed sync from occuring while signing in. */
this.syncManager.lockSyncing();
this.state.formData.status = STRING_GENERATING_LOGIN_KEYS;
this.state.formData.authenticating = true;
const response = await this.authManager.login(
this.state.formData.url,
this.state.formData.email,
this.state.formData.user_password,
this.state.formData.ephemeral,
this.state.formData.strictSignin,
extraParams
);
const hasError = !response || response.error;
if (!hasError) {
await this.onAuthSuccess();
this.syncManager.unlockSyncing();
this.syncManager.sync({ performIntegrityCheck: true });
return;
}
this.syncManager.unlockSyncing();
this.state.formData.status = null;
const error = response
? response.error
: { message: "An unknown error occured." }
if (error.tag === 'mfa-required' || error.tag === 'mfa-invalid') {
this.state.formData.showLogin = false;
this.state.formData.mfa = error;
} else {
this.state.formData.showLogin = true;
this.state.formData.mfa = null;
if (error.message) {
this.alertManager.alert({
text: error.message
});
}
}
this.state.formData.authenticating = false;
}
async register() {
const confirmation = this.state.formData.password_conf;
if (confirmation !== this.state.formData.user_password) {
this.alertManager.alert({
text: STRING_NON_MATCHING_PASSWORDS
});
return;
}
this.state.formData.confirmPassword = false;
this.state.formData.status = STRING_GENERATING_REGISTER_KEYS;
this.state.formData.authenticating = true;
const response = await this.authManager.register(
this.state.formData.url,
this.state.formData.email,
this.state.formData.user_password,
this.state.formData.ephemeral
)
if (!response || response.error) {
this.state.formData.status = null;
const error = response
? response.error
: { message: "An unknown error occured." };
this.state.formData.authenticating = false;
this.alertManager.alert({
text: error.message
});
} else {
await this.onAuthSuccess();
this.syncManager.sync();
}
}
mergeLocalChanged() {
if (!this.state.formData.mergeLocal) {
this.alertManager.confirm({
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
destructive: true,
onCancel: () => {
this.state.formData.mergeLocal = true;
}
})
}
}
async onAuthSuccess() {
if (this.state.formData.mergeLocal) {
this.$rootScope.$broadcast('major-data-change');
await this.clearDatabaseAndRewriteAllItems({ alternateUuids: true });
} else {
this.modelManager.removeAllItemsFromMemory();
await this.storageManager.clearAllModels();
}
this.state.formData.authenticating = false;
this.syncManager.refreshErroredItems();
this.close();
}
openPasswordWizard(type) {
this.close();
this.authManager.presentPasswordWizard(type);
}
async openPrivilegesModal() {
this.close();
const run = () => {
this.privilegesManager.presentPrivilegesManagementModal();
}
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManagePrivileges
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManagePrivileges,
() => {
run();
}
);
} else {
run();
}
}
/**
* Allows IndexedDB unencrypted logs to be deleted
* `clearAllModels` will remove data from backing store,
* but not from working memory See:
* https://github.com/standardnotes/desktop/issues/131
*/
async clearDatabaseAndRewriteAllItems({ alternateUuids } = {}) {
await this.storageManager.clearAllModels();
await this.syncManager.markAllItemsDirtyAndSaveOffline(alternateUuids)
}
destroyLocalData() {
this.alertManager.confirm({
text: STRING_SIGN_OUT_CONFIRMATION,
destructive: true,
onConfirm: async () => {
await this.authManager.signout(true);
window.location.reload();
}
})
}
async submitImportPassword() {
await this.performImport(
this.state.importData.data,
this.state.importData.password
);
}
async readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (e) {
try {
const data = JSON.parse(e.target.result);
resolve(data);
} catch (e) {
this.alertManager.alert({
text: STRING_INVALID_IMPORT_FILE
});
}
}
reader.readAsText(file);
})
}
/**
* @template
*/
async importFileSelected(files) {
const run = async () => {
const file = files[0];
const data = await this.readFile(file);
if (!data) {
return;
}
if (data.auth_params) {
await this.setState({
importData: {
...this.state.importData,
requestPassword: true,
data: data
}
})
const element = document.getElementById(
ELEMENT_ID_IMPORT_PASSWORD_INPUT
);
if (element) {
element.scrollIntoView(false);
}
} else {
await this.performImport(data, null);
}
}
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManageBackups
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManageBackups,
run
);
} else {
run();
}
}
async performImport(data, password) {
await this.setState({
importData: {
...this.state.importData,
loading: true
}
})
const errorCount = await this.importJSONData(data, password);
this.setState({
importData: null
})
if (errorCount > 0) {
const message = StringImportError({ errorCount: errorCount })
this.alertManager.alert({
text: message
});
} else {
this.alertManager.alert({
text: STRING_IMPORT_SUCCESS
})
}
}
async importJSONData(data, password) {
let errorCount = 0;
if (data.auth_params) {
const keys = await protocolManager.computeEncryptionKeysForUser(
password,
data.auth_params
);
try {
const throws = false;
await protocolManager.decryptMultipleItems(data.items, keys, throws);
const items = [];
for (const item of data.items) {
item.enc_item_key = null;
item.auth_hash = null;
if (item.errorDecrypting) {
errorCount++;
} else {
items.push(item);
}
}
data.items = items;
} catch (e) {
this.alertManager.alert({
text: STRING_ERROR_DECRYPTING_IMPORT
});
return;
}
}
const items = await this.modelManager.importItems(data.items);
for (const item of items) {
/**
* Don't want to activate any components during import process in
* case of exceptions breaking up the import proccess
*/
if (item.content_type === 'SN|Component') {
item.active = false;
}
}
this.syncManager.sync();
return errorCount;
}
async downloadDataArchive() {
this.archiveManager.downloadBackup(this.state.mutable.backupEncrypted);
}
notesAndTagsCount() {
return this.modelManager.allItemsMatchingTypes([
'Note',
'Tag'
]).length;
}
encryptionStatusForNotes() {
const length = this.notesAndTagsCount();
return length + "/" + length + " notes and tags encrypted";
}
encryptionEnabled() {
return this.passcodeManager.hasPasscode() || !this.authManager.offline();
}
encryptionSource() {
if (!this.authManager.offline()) {
return "Account keys";
} else if (this.passcodeManager.hasPasscode()) {
return "Local Passcode";
} else {
return null;
}
}
encryptionStatusString() {
if (!this.authManager.offline()) {
return STRING_E2E_ENABLED;
} else if (this.passcodeManager.hasPasscode()) {
return STRING_LOCAL_ENC_ENABLED;
} else {
return STRING_ENC_NOT_ENABLED;
}
}
async reloadAutoLockInterval() {
const interval = await this.passcodeManager.getAutoLockInterval();
this.setState({
selectedAutoLockInterval: interval
})
}
async selectAutoLockInterval(interval) {
const run = async () => {
await this.passcodeManager.setAutoLockInterval(interval);
this.reloadAutoLockInterval();
}
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManagePasscode
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManagePasscode,
() => {
run();
}
);
} else {
run();
}
}
hasPasscode() {
return this.passcodeManager.hasPasscode();
}
addPasscodeClicked() {
this.state.formData.showPasscodeForm = true;
}
submitPasscodeForm() {
const passcode = this.state.formData.passcode;
if (passcode !== this.state.formData.confirmPasscode) {
this.alertManager.alert({
text: STRING_NON_MATCHING_PASSCODES
});
return;
}
const func = this.state.formData.changingPasscode
? this.passcodeManager.changePasscode.bind(this.passcodeManager)
: this.passcodeManager.setPasscode.bind(this.passcodeManager);
func(passcode, async () => {
this.setState({
formData: {
...this.state.formData,
passcode: null,
confirmPasscode: null,
showPasscodeForm: false
}
})
if (await this.authManager.offline()) {
this.$rootScope.$broadcast('major-data-change');
this.clearDatabaseAndRewriteAllItems();
}
})
}
async changePasscodePressed() {
const run = () => {
this.state.formData.changingPasscode = true;
this.addPasscodeClicked();
}
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManagePasscode
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManagePasscode,
run
);
} else {
run();
}
}
async removePasscodePressed() {
const run = () => {
const signedIn = !this.authManager.offline();
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
if (!signedIn) {
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
}
this.alertManager.confirm({
text: message,
destructive: true,
onConfirm: () => {
this.passcodeManager.clearPasscode();
if (this.authManager.offline()) {
this.syncManager.markAllItemsDirtyAndSaveOffline();
}
}
})
}
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
PrivilegesManager.ActionManagePasscode
);
if (needsPrivilege) {
this.privilegesManager.presentPrivilegesModal(
PrivilegesManager.ActionManagePasscode,
run
);
} else {
run();
}
}
isDesktopApplication() {
return isDesktopApplication();
}
}
export class AccountMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = AccountMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
closeFunction: '&'
};
}
}

View File

@@ -0,0 +1,104 @@
import template from '%/directives/actions-menu.pug';
import { PureCtrl } from '@Controllers';
class ActionsMenuCtrl extends PureCtrl {
/* @ngInject */
constructor(
$scope,
$timeout,
actionsManager,
) {
super($timeout);
this.$timeout = $timeout;
this.actionsManager = actionsManager;
}
$onInit() {
this.initProps({
item: this.item
})
this.loadExtensions();
};
async loadExtensions() {
const extensions = this.actionsManager.extensions.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
for (const extension of extensions) {
extension.loading = true;
await this.actionsManager.loadExtensionInContextOfItem(extension, this.props.item);
extension.loading = false;
}
this.setState({
extensions: extensions
});
}
async executeAction(action, extension) {
if (action.verb === 'nested') {
if (!action.subrows) {
action.subrows = this.subRowsForAction(action, extension);
} else {
action.subrows = null;
}
return;
}
action.running = true;
const result = await this.actionsManager.executeAction(
action,
extension,
this.props.item
);
if (action.error) {
return;
}
action.running = false;
this.handleActionResult(action, result);
await this.actionsManager.loadExtensionInContextOfItem(extension, this.props.item);
this.setState({
extensions: this.state.extensions
})
}
handleActionResult(action, result) {
switch (action.verb) {
case 'render': {
const item = result.item;
this.actionsManager.presentRevisionPreviewModal(
item.uuid,
item.content
);
}
}
}
subRowsForAction(parentAction, extension) {
if (!parentAction.subactions) {
return null;
}
return parentAction.subactions.map((subaction) => {
return {
onClick: () => {
this.executeAction(subaction, extension, parentAction);
},
label: subaction.label,
subtitle: subaction.desc,
spinnerClass: subaction.running ? 'info' : null
}
})
}
}
export class ActionsMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.replace = true;
this.controller = ActionsMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
item: '='
};
}
}

View File

@@ -0,0 +1,34 @@
import template from '%/directives/component-modal.pug';
export class ComponentModalCtrl {
/* @ngInject */
constructor($scope, $element) {
this.$element = $element;
this.$scope = $scope;
}
dismiss(callback) {
this.$element.remove();
this.$scope.$destroy();
if(this.onDismiss && this.onDismiss()) {
this.onDismiss()(this.component)
}
callback && callback();
}
}
export class ComponentModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = ComponentModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
show: '=',
component: '=',
callback: '=',
onDismiss: '&'
};
}
}

View File

@@ -0,0 +1,276 @@
import template from '%/directives/component-view.pug';
import { isDesktopApplication } from '../../utils';
/**
* The maximum amount of time we'll wait for a component
* to load before displaying error
*/
const MAX_LOAD_THRESHOLD = 4000;
const VISIBILITY_CHANGE_LISTENER_KEY = 'visibilitychange';
class ComponentViewCtrl {
/* @ngInject */
constructor(
$scope,
$rootScope,
$timeout,
componentManager,
desktopManager,
themeManager
) {
this.$rootScope = $rootScope;
this.$timeout = $timeout;
this.themeManager = themeManager;
this.desktopManager = desktopManager;
this.componentManager = componentManager;
this.componentValid = true;
$scope.$watch('ctrl.component', (component, prevComponent) => {
this.componentValueDidSet(component, prevComponent);
});
$scope.$on('ext-reload-complete', () => {
this.reloadStatus(false);
})
$scope.$on('$destroy', () => {
this.destroy();
});
}
$onInit() {
this.registerComponentHandlers();
this.registerPackageUpdateObserver();
};
registerPackageUpdateObserver() {
this.updateObserver = this.desktopManager
.registerUpdateObserver((component) => {
if(component === this.component && component.active) {
this.reloadComponent();
}
})
}
registerComponentHandlers() {
this.themeHandlerIdentifier = 'component-view-' + Math.random();
this.componentManager.registerHandler({
identifier: this.themeHandlerIdentifier,
areas: ['themes'],
activationHandler: (component) => {
this.reloadThemeStatus();
}
});
this.identifier = 'component-view-' + Math.random();
this.componentManager.registerHandler({
identifier: this.identifier,
areas: [this.component.area],
activationHandler: (component) => {
if(component !== this.component) {
return;
}
this.$timeout(() => {
this.handleActivation();
})
},
actionHandler: (component, action, data) => {
if(action === 'set-size') {
this.componentManager.handleSetSizeEvent(component, data);
}
}
});
}
onVisibilityChange() {
if(document.visibilityState === 'hidden') {
return;
}
if(this.issueLoading) {
this.reloadComponent();
}
}
async reloadComponent() {
this.componentValid = false;
await this.componentManager.reloadComponent(this.component);
this.reloadStatus();
}
reloadStatus(doManualReload = true) {
this.reloading = true;
const component = this.component;
const previouslyValid = this.componentValid;
const offlineRestricted = component.offlineOnly && !isDesktopApplication();
const hasUrlError = function(){
if(isDesktopApplication()) {
return !component.local_url && !component.hasValidHostedUrl();
} else {
return !component.hasValidHostedUrl();
}
}();
this.expired = component.valid_until && component.valid_until <= new Date();
if(!component.lockReadonly) {
component.readonly = this.expired;
}
this.componentValid = !offlineRestricted && !hasUrlError;
if(!this.componentValid) {
this.loading = false;
}
if(offlineRestricted) {
this.error = 'offline-restricted'
} else if(hasUrlError) {
this.error = 'url-missing'
} else {
this.error = null;
}
if(this.componentValid !== previouslyValid) {
if(this.componentValid) {
this.componentManager.reloadComponent(component, true);
}
}
if(this.expired && doManualReload) {
this.$rootScope.$broadcast('reload-ext-dat');
}
this.reloadThemeStatus();
this.$timeout(() => {
this.reloading = false;
}, 500)
}
handleActivation() {
if(!this.component.active) {
return;
}
const iframe = this.componentManager.iframeForComponent(
this.component
);
if(!iframe) {
return;
}
this.loading = true;
if(this.loadTimeout) {
this.$timeout.cancel(this.loadTimeout);
}
this.loadTimeout = this.$timeout(() => {
this.handleIframeLoadTimeout();
}, MAX_LOAD_THRESHOLD);
iframe.onload = (event) => {
this.handleIframeLoad(iframe);
};
}
async handleIframeLoadTimeout() {
if(this.loading) {
this.loading = false;
this.issueLoading = true;
if(!this.didAttemptReload) {
this.didAttemptReload = true;
this.reloadComponent();
} else {
document.addEventListener(
VISIBILITY_CHANGE_LISTENER_KEY,
this.onVisibilityChange.bind(this)
);
}
}
}
async handleIframeLoad(iframe) {
let desktopError = false;
if(isDesktopApplication()) {
try {
/** Accessing iframe.contentWindow.origin only allowed in desktop app. */
if(!iframe.contentWindow.origin || iframe.contentWindow.origin === 'null') {
desktopError = true;
}
} catch (e) {}
}
this.$timeout.cancel(this.loadTimeout);
await this.componentManager.registerComponentWindow(
this.component,
iframe.contentWindow
);
const avoidFlickerTimeout = 7;
this.$timeout(() => {
this.loading = false;
this.issueLoading = desktopError ? true : false;
this.onLoad && this.onLoad(this.component);
}, avoidFlickerTimeout)
}
componentValueDidSet(component, prevComponent) {
const dontSync = true;
if(prevComponent && component !== prevComponent) {
this.componentManager.deactivateComponent(
prevComponent,
dontSync
);
}
if(component) {
this.componentManager.activateComponent(
component,
dontSync
);
this.reloadStatus();
}
}
reloadThemeStatus() {
if(this.component.acceptsThemes()) {
return;
}
if(this.themeManager.hasActiveTheme()) {
if(!this.dismissedNoThemesMessage) {
this.showNoThemesMessage = true;
}
} else {
this.showNoThemesMessage = false;
}
}
dismissNoThemesMessage() {
this.showNoThemesMessage = false;
this.dismissedNoThemesMessage = true;
}
disableActiveTheme() {
this.themeManager.deactivateAllThemes();
this.dismissNoThemesMessage();
}
getUrl() {
const url = this.componentManager.urlForComponent(this.component);
this.component.runningLocally = (url === this.component.local_url);
return url;
}
destroy() {
this.componentManager.deregisterHandler(this.themeHandlerIdentifier);
this.componentManager.deregisterHandler(this.identifier);
if(this.component && !this.manualDealloc) {
const dontSync = true;
this.componentManager.deactivateComponent(this.component, dontSync);
}
this.desktopManager.deregisterUpdateObserver(this.updateObserver);
document.removeEventListener(
VISIBILITY_CHANGE_LISTENER_KEY,
this.onVisibilityChange.bind(this)
);
}
}
export class ComponentView {
constructor() {
this.restrict = 'E';
this.template = template;
this.scope = {
component: '=',
onLoad: '=?',
manualDealloc: '=?'
};
this.controller = ComponentViewCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
}
}

View File

@@ -0,0 +1,95 @@
import template from '%/directives/conflict-resolution-modal.pug';
class ConflictResolutionCtrl {
/* @ngInject */
constructor(
$element,
alertManager,
archiveManager,
modelManager,
syncManager
) {
this.$element = $element;
this.alertManager = alertManager;
this.archiveManager = archiveManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
}
$onInit() {
this.contentType = this.item1.content_type;
this.item1Content = this.createContentString(this.item1);
this.item2Content = this.createContentString(this.item2);
};
createContentString(item) {
const data = Object.assign({
created_at: item.created_at,
updated_at: item.updated_at
}, item.content);
return JSON.stringify(data, null, 2);
}
keepItem1() {
this.alertManager.confirm({
text: `Are you sure you want to delete the item on the right?`,
destructive: true,
onConfirm: () => {
this.modelManager.setItemToBeDeleted(this.item2);
this.syncManager.sync().then(() => {
this.applyCallback();
})
this.dismiss();
}
});
}
keepItem2() {
this.alertManager.confirm({
text: `Are you sure you want to delete the item on the left?`,
destructive: true,
onConfirm: () => {
this.modelManager.setItemToBeDeleted(this.item1);
this.syncManager.sync().then(() => {
this.applyCallback();
})
this.dismiss();
}
});
}
keepBoth() {
this.applyCallback();
this.dismiss();
}
export() {
this.archiveManager.downloadBackupOfItems(
[this.item1, this.item2],
true
);
}
applyCallback() {
this.callback && this.callback();
}
dismiss() {
this.$element.remove();
}
}
export class ConflictResolutionModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = ConflictResolutionCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
item1: '=',
item2: '=',
callback: '='
};
}
}

View File

@@ -0,0 +1,110 @@
import { isDesktopApplication } from '@/utils';
import template from '%/directives/editor-menu.pug';
import { PureCtrl } from '@Controllers';
class EditorMenuCtrl extends PureCtrl {
/* @ngInject */
constructor(
$timeout,
componentManager,
modelManager,
syncManager,
) {
super($timeout);
this.$timeout = $timeout;
this.componentManager = componentManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.state = {
isDesktop: isDesktopApplication()
}
}
$onInit() {
const editors = this.componentManager.componentsForArea('editor-editor')
.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0];
this.setState({
editors: editors,
defaultEditor: defaultEditor
})
};
selectComponent(component) {
if(component) {
if(component.content.conflict_of) {
component.content.conflict_of = null;
this.modelManager.setItemDirty(component, true);
this.syncManager.sync();
}
}
this.$timeout(() => {
this.callback()(component);
})
}
toggleDefaultForEditor(editor) {
if(this.defaultEditor === editor) {
this.removeEditorDefault(editor);
} else {
this.makeEditorDefault(editor);
}
}
offlineAvailableForComponent(component) {
return component.local_url && this.state.isDesktop;
}
makeEditorDefault(component) {
const currentDefault = this.componentManager
.componentsForArea('editor-editor')
.filter((e) => e.isDefaultEditor())[0];
if(currentDefault) {
currentDefault.setAppDataItem('defaultEditor', false);
this.modelManager.setItemDirty(currentDefault);
}
component.setAppDataItem('defaultEditor', true);
this.modelManager.setItemDirty(component);
this.syncManager.sync();
this.setState({
defaultEditor: component
})
}
removeEditorDefault(component) {
component.setAppDataItem('defaultEditor', false);
this.modelManager.setItemDirty(component);
this.syncManager.sync();
this.setState({
defaultEditor: null
})
}
shouldDisplayRunningLocallyLabel(component) {
if(!component.runningLocally) {
return false;
}
if(component === this.selectedEditor) {
return true;
} else {
return false;
}
}
}
export class EditorMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = EditorMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
callback: '&',
selectedEditor: '=',
currentItem: '='
};
}
}

View File

@@ -0,0 +1,16 @@
export { AccountMenu } from './accountMenu';
export { ActionsMenu } from './actionsMenu';
export { ComponentModal } from './componentModal';
export { ComponentView } from './componentView';
export { ConflictResolutionModal } from './conflictResolutionModal';
export { EditorMenu } from './editorMenu';
export { InputModal } from './inputModal';
export { MenuRow } from './menuRow';
export { PanelResizer } from './panelResizer';
export { PasswordWizard } from './passwordWizard';
export { PermissionsModal } from './permissionsModal';
export { PrivilegesAuthModal } from './privilegesAuthModal';
export { PrivilegesManagementModal } from './privilegesManagementModal';
export { RevisionPreviewModal } from './revisionPreviewModal';
export { SessionHistoryMenu } from './sessionHistoryMenu';
export { SyncResolutionMenu } from './syncResolutionMenu';

View File

@@ -0,0 +1,37 @@
import template from '%/directives/input-modal.pug';
class InputModalCtrl {
/* @ngInject */
constructor($scope, $element) {
this.$element = $element;
this.formData = {};
}
dismiss() {
this.$element.remove();
this.$scope.$destroy();
}
submit() {
this.callback()(this.formData.input);
this.dismiss();
}
}
export class InputModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = InputModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
type: '=',
title: '=',
message: '=',
placeholder: '=',
callback: '&'
};
}
}

View File

@@ -0,0 +1,48 @@
import template from '%/directives/menu-row.pug';
class MenuRowCtrl {
onClick($event) {
if(this.disabled) {
return;
}
$event.stopPropagation();
this.action();
}
clickAccessoryButton($event) {
if(this.disabled) {
return;
}
$event.stopPropagation();
this.buttonAction();
}
}
export class MenuRow {
constructor() {
this.restrict = 'E';
this.transclude = true;
this.template = template;
this.controller = MenuRowCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
action: '&',
buttonAction: '&',
buttonClass: '=',
buttonText: '=',
desc: '=',
disabled: '=',
circle: '=',
circleAlign: '=',
faded: '=',
hasButton: '=',
label: '=',
spinnerClass: '=',
stylekitClass: '=',
subRows: '=',
subtitle: '=',
};
}
}

View File

@@ -0,0 +1,339 @@
import angular from 'angular';
import template from '%/directives/panel-resizer.pug';
import { debounce } from '@/utils';
const PANEL_SIDE_RIGHT = 'right';
const PANEL_SIDE_LEFT = 'left';
const MOUSE_EVENT_MOVE = 'mousemove';
const MOUSE_EVENT_DOWN = 'mousedown';
const MOUSE_EVENT_UP = 'mouseup';
const WINDOW_EVENT_RESIZE = 'resize';
const PANEL_CSS_CLASS_HOVERABLE = 'hoverable';
const PANEL_CSS_CLASS_ALWAYS_VISIBLE = 'always-visible';
const PANEL_CSS_CLASS_DRAGGING = 'dragging';
const PANEL_CSS_CLASS_NO_SELECTION = 'no-selection';
const PANEL_CSS_CLASS_COLLAPSED = 'collapsed';
const PANEL_CSS_CLASS_ANIMATE_OPACITY = 'animate-opacity';
class PanelResizerCtrl {
/* @ngInject */
constructor(
$compile,
$element,
$scope,
$timeout,
) {
this.$compile = $compile;
this.$element = $element;
this.$scope = $scope;
this.$timeout = $timeout;
}
$onInit() {
this.configureControl();
this.configureDefaults();
this.addDoubleClickHandler();
this.reloadDefaultValues();
this.addMouseDownListener();
this.addMouseMoveListener();
this.addMouseUpListener();
}
configureControl() {
this.control.setWidth = (value) => {
this.setWidth(value, true);
};
this.control.setLeft = (value) => {
this.setLeft(value);
};
this.control.flash = () => {
this.flash();
};
this.control.isCollapsed = () => {
return this.isCollapsed();
};
}
configureDefaults() {
this.panel = document.getElementById(this.panelId);
if (!this.panel) {
console.error('Panel not found for', this.panelId);
}
this.resizerColumn = this.$element[0];
this.currentMinWidth = this.minWidth || this.resizerColumn.offsetWidth;
this.pressed = false;
this.startWidth = this.panel.scrollWidth;
this.lastDownX = 0;
this.collapsed = false;
this.lastWidth = this.startWidth;
this.startLeft = this.panel.offsetLeft;
this.lastLeft = this.startLeft;
this.appFrame = null;
this.widthBeforeLastDblClick = 0;
if (this.property === PANEL_SIDE_RIGHT) {
this.configureRightPanel();
}
if (this.alwaysVisible) {
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ALWAYS_VISIBLE);
}
if (this.hoverable) {
this.resizerColumn.classList.add(PANEL_CSS_CLASS_HOVERABLE);
}
}
configureRightPanel() {
const handleResize = debounce(event => {
this.reloadDefaultValues();
this.handleWidthEvent();
this.$timeout(() => {
this.finishSettingWidth();
});
}, 250);
window.addEventListener(WINDOW_EVENT_RESIZE, handleResize);
this.$scope.$on('$destroy', () => {
window.removeEventListener(WINDOW_EVENT_RESIZE, handleResize);
});
}
getParentRect() {
return this.panel.parentNode.getBoundingClientRect();
}
reloadDefaultValues() {
this.startWidth = this.isAtMaxWidth()
? this.getParentRect().width
: this.panel.scrollWidth;
this.lastWidth = this.startWidth;
this.appFrame = document.getElementById('app').getBoundingClientRect();
}
addDoubleClickHandler() {
this.resizerColumn.ondblclick = () => {
this.$timeout(() => {
const preClickCollapseState = this.isCollapsed();
if (preClickCollapseState) {
this.setWidth(this.widthBeforeLastDblClick || this.defaultWidth);
} else {
this.widthBeforeLastDblClick = this.lastWidth;
this.setWidth(this.currentMinWidth);
}
this.finishSettingWidth();
const newCollapseState = !preClickCollapseState;
this.onResizeFinish()(
this.lastWidth,
this.lastLeft,
this.isAtMaxWidth(),
newCollapseState
);
});
};
}
addMouseDownListener() {
this.resizerColumn.addEventListener(MOUSE_EVENT_DOWN, (event) => {
this.addInvisibleOverlay();
this.pressed = true;
this.lastDownX = event.clientX;
this.startWidth = this.panel.scrollWidth;
this.startLeft = this.panel.offsetLeft;
this.panel.classList.add(PANEL_CSS_CLASS_NO_SELECTION);
if (this.hoverable) {
this.resizerColumn.classList.add(PANEL_CSS_CLASS_DRAGGING);
}
});
}
addMouseMoveListener() {
document.addEventListener(MOUSE_EVENT_MOVE, (event) => {
if (!this.pressed) {
return;
}
event.preventDefault();
if (this.property && this.property === PANEL_SIDE_LEFT) {
this.handleLeftEvent(event);
} else {
this.handleWidthEvent(event);
}
});
}
handleWidthEvent(event) {
let x;
if (event) {
x = event.clientX;
} else {
/** Coming from resize event */
x = 0;
this.lastDownX = 0;
}
const deltaX = x - this.lastDownX;
const newWidth = this.startWidth + deltaX;
this.setWidth(newWidth, false);
if (this.onResize()) {
this.onResize()(this.lastWidth, this.panel);
}
}
handleLeftEvent(event) {
const panelRect = this.panel.getBoundingClientRect();
const x = event.clientX || panelRect.x;
let deltaX = x - this.lastDownX;
let newLeft = this.startLeft + deltaX;
if (newLeft < 0) {
newLeft = 0;
deltaX = -this.startLeft;
}
const parentRect = this.getParentRect();
let newWidth = this.startWidth - deltaX;
if (newWidth < this.currentMinWidth) {
newWidth = this.currentMinWidth;
}
if (newWidth > parentRect.width) {
newWidth = parentRect.width;
}
if (newLeft + newWidth > parentRect.width) {
newLeft = parentRect.width - newWidth;
}
this.setLeft(newLeft, false);
this.setWidth(newWidth, false);
}
addMouseUpListener() {
document.addEventListener(MOUSE_EVENT_UP, event => {
this.removeInvisibleOverlay();
if (this.pressed) {
this.pressed = false;
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_DRAGGING);
this.panel.classList.remove(PANEL_CSS_CLASS_NO_SELECTION);
const isMaxWidth = this.isAtMaxWidth();
if (this.onResizeFinish) {
this.onResizeFinish()(
this.lastWidth,
this.lastLeft,
isMaxWidth,
this.isCollapsed()
);
}
this.finishSettingWidth();
}
});
}
isAtMaxWidth() {
return (
Math.round(this.lastWidth + this.lastLeft) ===
Math.round(this.getParentRect().width)
);
}
isCollapsed() {
return this.lastWidth <= this.currentMinWidth;
}
setWidth(width, finish) {
if (width < this.currentMinWidth) {
width = this.currentMinWidth;
}
const parentRect = this.getParentRect();
if (width > parentRect.width) {
width = parentRect.width;
}
const maxWidth = this.appFrame.width - this.panel.getBoundingClientRect().x;
if (width > maxWidth) {
width = maxWidth;
}
if (Math.round(width + this.lastLeft) === Math.round(parentRect.width)) {
this.panel.style.width = `calc(100% - ${this.lastLeft}px)`;
this.panel.style.flexBasis = `calc(100% - ${this.lastLeft}px)`;
} else {
this.panel.style.flexBasis = width + 'px';
this.panel.style.width = width + 'px';
}
this.lastWidth = width;
if (finish) {
this.finishSettingWidth();
}
}
setLeft(left) {
this.panel.style.left = left + 'px';
this.lastLeft = left;
}
finishSettingWidth() {
if (!this.collapsable) {
return;
}
this.collapsed = this.isCollapsed();
if (this.collapsed) {
this.resizerColumn.classList.add(PANEL_CSS_CLASS_COLLAPSED);
} else {
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_COLLAPSED);
}
}
/**
* If an iframe is displayed adjacent to our panel, and the mouse exits over the iframe,
* document[onmouseup] is not triggered because the document is no longer the same over
* the iframe. We add an invisible overlay while resizing so that the mouse context
* remains in our main document.
*/
addInvisibleOverlay() {
if (this.overlay) {
return;
}
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(this.$scope);
angular.element(document.body).prepend(this.overlay);
}
removeInvisibleOverlay() {
if (this.overlay) {
this.overlay.remove();
this.overlay = null;
}
}
flash() {
const FLASH_DURATION = 3000;
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ANIMATE_OPACITY);
this.$timeout(() => {
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_ANIMATE_OPACITY);
}, FLASH_DURATION);
}
}
export class PanelResizer {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = PanelResizerCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
alwaysVisible: '=',
collapsable: '=',
control: '=',
defaultWidth: '=',
hoverable: '=',
index: '=',
minWidth: '=',
onResize: '&',
onResizeFinish: '&',
panelId: '=',
property: '='
};
}
}

View File

@@ -0,0 +1,284 @@
import { protocolManager } from 'snjs';
import template from '%/directives/password-wizard.pug';
import { STRING_FAILED_PASSWORD_CHANGE } from '@/strings';
const DEFAULT_CONTINUE_TITLE = "Continue";
const Steps = {
IntroStep: 0,
BackupStep: 1,
SignoutStep: 2,
PasswordStep: 3,
SyncStep: 4,
FinishStep: 5
}
class PasswordWizardCtrl {
/* @ngInject */
constructor(
$element,
$scope,
$timeout,
alertManager,
archiveManager,
authManager,
modelManager,
syncManager,
) {
this.$element = $element;
this.$timeout = $timeout;
this.$scope = $scope;
this.alertManager = alertManager;
this.archiveManager = archiveManager;
this.authManager = authManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.registerWindowUnloadStopper();
}
$onInit() {
this.syncStatus = this.syncManager.syncStatus;
this.formData = {};
this.configureDefaults();
}
configureDefaults() {
if (this.type === 'change-pw') {
this.title = "Change Password";
this.changePassword = true;
} else if (this.type === 'upgrade-security') {
this.title = "Security Update";
this.securityUpdate = true;
}
this.continueTitle = DEFAULT_CONTINUE_TITLE;
this.step = Steps.IntroStep;
}
/** Confirms with user before closing tab */
registerWindowUnloadStopper() {
window.onbeforeunload = (e) => {
return true;
};
this.$scope.$on("$destroy", () => {
window.onbeforeunload = null;
});
}
titleForStep(step) {
switch (step) {
case Steps.BackupStep:
return "Download a backup of your data";
case Steps.SignoutStep:
return "Sign out of all your devices";
case Steps.PasswordStep:
return this.changePassword
? "Password information"
: "Enter your current password";
case Steps.SyncStep:
return "Encrypt and sync data with new keys";
case Steps.FinishStep:
return "Sign back in to your devices";
default:
return null;
}
}
async nextStep() {
if (this.lockContinue || this.isContinuing) {
return;
}
this.isContinuing = true;
if (this.step === Steps.FinishStep) {
this.dismiss();
return;
}
const next = () => {
this.step++;
this.initializeStep(this.step);
this.isContinuing = false;
}
const preprocessor = this.preprocessorForStep(this.step);
if (preprocessor) {
await preprocessor().then(next).catch(() => {
this.isContinuing = false;
})
} else {
next();
}
}
preprocessorForStep(step) {
if (step === Steps.PasswordStep) {
return async () => {
this.showSpinner = true;
this.continueTitle = "Generating Keys...";
const success = await this.validateCurrentPassword();
this.showSpinner = false;
this.continueTitle = DEFAULT_CONTINUE_TITLE;
return success;
}
}
}
async initializeStep(step) {
if (step === Steps.SyncStep) {
await this.initializeSyncingStep();
} else if (step === Steps.FinishStep) {
this.continueTitle = "Finish";
}
}
async initializeSyncingStep() {
this.lockContinue = true;
this.formData.status = "Processing encryption keys...";
this.formData.processing = true;
const passwordSuccess = await this.processPasswordChange();
this.formData.statusError = !passwordSuccess;
this.formData.processing = passwordSuccess;
if(!passwordSuccess) {
this.formData.status = "Unable to process your password. Please try again.";
return;
}
this.formData.status = "Encrypting and syncing data with new keys...";
const syncSuccess = await this.resyncData();
this.formData.statusError = !syncSuccess;
this.formData.processing = !syncSuccess;
if (syncSuccess) {
this.lockContinue = false;
if (this.changePassword) {
this.formData.status = "Successfully changed password and synced all items.";
} else if (this.securityUpdate) {
this.formData.status = "Successfully performed security update and synced all items.";
}
} else {
this.formData.status = STRING_FAILED_PASSWORD_CHANGE;
}
}
async validateCurrentPassword() {
const currentPassword = this.formData.currentPassword;
const newPass = this.securityUpdate ? currentPassword : this.formData.newPassword;
if (!currentPassword || currentPassword.length === 0) {
this.alertManager.alert({
text: "Please enter your current password."
});
return false;
}
if (this.changePassword) {
if (!newPass || newPass.length === 0) {
this.alertManager.alert({
text: "Please enter a new password."
});
return false;
}
if (newPass !== this.formData.newPasswordConfirmation) {
this.alertManager.alert({
text: "Your new password does not match its confirmation."
});
this.formData.status = null;
return false;
}
}
if (!this.authManager.user.email) {
this.alertManager.alert({
text: "We don't have your email stored. Please log out then log back in to fix this issue."
});
this.formData.status = null;
return false;
}
/** Validate current password */
const authParams = await this.authManager.getAuthParams();
const password = this.formData.currentPassword;
const keys = await protocolManager.computeEncryptionKeysForUser(
password,
authParams
);
const success = keys.mk === (await this.authManager.keys()).mk;
if (success) {
this.currentServerPw = keys.pw;
} else {
this.alertManager.alert({
text: "The current password you entered is not correct. Please try again."
});
}
return success;
}
async resyncData() {
await this.modelManager.setAllItemsDirty();
const response = await this.syncManager.sync();
if (!response || response.error) {
this.alertManager.alert({
text: STRING_FAILED_PASSWORD_CHANGE
})
return false;
} else {
return true;
}
}
async processPasswordChange() {
const newUserPassword = this.securityUpdate
? this.formData.currentPassword
: this.formData.newPassword;
const currentServerPw = this.currentServerPw;
const results = await protocolManager.generateInitialKeysAndAuthParamsForUser(
this.authManager.user.email,
newUserPassword
);
const newKeys = results.keys;
const newAuthParams = results.authParams;
/**
* Perform a sync beforehand to pull in any last minutes changes before we change
* the encryption key (and thus cant decrypt new changes).
*/
await this.syncManager.sync();
const response = await this.authManager.changePassword(
await this.syncManager.getServerURL(),
this.authManager.user.email,
currentServerPw,
newKeys,
newAuthParams
);
if (response.error) {
this.alertManager.alert({
text: response.error.message
? response.error.message
: "There was an error changing your password. Please try again."
});
return false;
} else {
return true;
}
}
downloadBackup(encrypted) {
this.archiveManager.downloadBackup(encrypted);
}
dismiss() {
if (this.lockContinue) {
this.alertManager.alert({
text: "Cannot close window until pending tasks are complete."
});
} else {
this.$element.remove();
this.$scope.$destroy();
}
}
}
export class PasswordWizard {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = PasswordWizardCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
type: '='
};
}
}

View File

@@ -0,0 +1,38 @@
import template from '%/directives/permissions-modal.pug';
class PermissionsModalCtrl {
/* @ngInject */
constructor($element) {
this.$element = $element;
}
dismiss() {
this.$element.remove();
}
accept() {
this.callback(true);
this.dismiss();
}
deny() {
this.callback(false);
this.dismiss();
}
}
export class PermissionsModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = PermissionsModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
show: '=',
component: '=',
permissionsString: '=',
callback: '='
};
}
}

View File

@@ -0,0 +1,101 @@
import template from '%/directives/privileges-auth-modal.pug';
class PrivilegesAuthModalCtrl {
/* @ngInject */
constructor(
$element,
$timeout,
privilegesManager,
) {
this.$element = $element;
this.$timeout = $timeout;
this.privilegesManager = privilegesManager;
}
$onInit() {
this.authParameters = {};
this.sessionLengthOptions = this.privilegesManager.getSessionLengthOptions();
this.privilegesManager.getSelectedSessionLength().then((length) => {
this.$timeout(() => {
this.selectedSessionLength = length;
})
})
this.privilegesManager.netCredentialsForAction(this.action).then((credentials) => {
this.$timeout(() => {
this.requiredCredentials = credentials.sort();
});
});
}
selectSessionLength(length) {
this.selectedSessionLength = length;
}
promptForCredential(credential) {
return this.privilegesManager.displayInfoForCredential(credential).prompt;
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
isCredentialInFailureState(credential) {
if (!this.failedCredentials) {
return false;
}
return this.failedCredentials.find((candidate) => {
return candidate === credential;
}) != null;
}
validate() {
const failed = [];
for (const cred of this.requiredCredentials) {
const value = this.authParameters[cred];
if (!value || value.length === 0) {
failed.push(cred);
}
}
this.failedCredentials = failed;
return failed.length === 0;
}
async submit() {
if (!this.validate()) {
return;
}
const result = await this.privilegesManager.authenticateAction(
this.action,
this.authParameters
);
this.$timeout(() => {
if (result.success) {
this.privilegesManager.setSessionLength(this.selectedSessionLength);
this.onSuccess();
this.dismiss();
} else {
this.failedCredentials = result.failedCredentials;
}
})
}
dismiss() {
this.$element.remove();
}
}
export class PrivilegesAuthModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesAuthModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
action: '=',
onSuccess: '=',
onCancel: '='
};
}
}

View File

@@ -0,0 +1,89 @@
import { PrivilegesManager } from '@/services/privilegesManager';
import template from '%/directives/privileges-management-modal.pug';
class PrivilegesManagementModalCtrl {
/* @ngInject */
constructor(
$timeout,
$element,
privilegesManager,
authManager,
passcodeManager,
) {
this.$element = $element;
this.$timeout = $timeout;
this.privilegesManager = privilegesManager;
this.hasPasscode = passcodeManager.hasPasscode();
this.hasAccount = !authManager.offline();
this.reloadPrivileges();
}
displayInfoForCredential(credential) {
const info = this.privilegesManager.displayInfoForCredential(credential);
if (credential === PrivilegesManager.CredentialLocalPasscode) {
info.availability = this.hasPasscode;
} else if (credential === PrivilegesManager.CredentialAccountPassword) {
info.availability = this.hasAccount;
} else {
info.availability = true;
}
return info;
}
displayInfoForAction(action) {
return this.privilegesManager.displayInfoForAction(action).label;
}
isCredentialRequiredForAction(action, credential) {
if (!this.privileges) {
return false;
}
return this.privileges.isCredentialRequiredForAction(action, credential);
}
async clearSession() {
await this.privilegesManager.clearSession();
this.reloadPrivileges();
}
async reloadPrivileges() {
this.availableActions = this.privilegesManager.getAvailableActions();
this.availableCredentials = this.privilegesManager.getAvailableCredentials();
const sessionEndDate = await this.privilegesManager.getSessionExpirey();
this.sessionExpirey = sessionEndDate.toLocaleString();
this.sessionExpired = new Date() >= sessionEndDate;
this.credentialDisplayInfo = {};
for (const cred of this.availableCredentials) {
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
}
const privs = await this.privilegesManager.getPrivileges();
this.$timeout(() => {
this.privileges = privs;
})
}
checkboxValueChanged(action, credential) {
this.privileges.toggleCredentialForAction(action, credential);
this.privilegesManager.savePrivileges();
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
dismiss() {
this.$element.remove();
}
}
export class PrivilegesManagementModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesManagementModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {};
}
}

View File

@@ -0,0 +1,133 @@
import { protocolManager, SNComponent, SFItem, SFModelManager } from 'snjs';
import template from '%/directives/revision-preview-modal.pug';
class RevisionPreviewModalCtrl {
/* @ngInject */
constructor(
$element,
$scope,
$timeout,
alertManager,
componentManager,
modelManager,
syncManager,
) {
this.$element = $element;
this.$scope = $scope;
this.$timeout = $timeout;
this.alertManager = alertManager;
this.componentManager = componentManager;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.createNote();
this.configureEditor();
$scope.$on('$destroy', () => {
if (this.identifier) {
this.componentManager.deregisterHandler(this.identifier);
}
});
}
createNote() {
this.note = new SFItem({
content: this.content,
content_type: "Note"
});
}
configureEditor() {
/**
* Set UUID so editoForNote can find proper editor, but then generate new uuid
* for note as not to save changes to original, if editor makes changes.
*/
this.note.uuid = this.uuid;
const editorForNote = this.componentManager.editorForNote(this.note);
this.note.uuid = protocolManager.crypto.generateUUIDSync();
if (editorForNote) {
/**
* Create temporary copy, as a lot of componentManager is uuid based, so might
* interfere with active editor. Be sure to copy only the content, as the top level
* editor object has non-copyable properties like .window, which cannot be transfered
*/
const editorCopy = new SNComponent({
content: editorForNote.content
});
editorCopy.readonly = true;
editorCopy.lockReadonly = true;
this.identifier = editorCopy.uuid;
this.componentManager.registerHandler({
identifier: this.identifier,
areas: ['editor-editor'],
contextRequestHandler: (component) => {
if (component === this.editor) {
return this.note;
}
},
componentForSessionKeyHandler: (key) => {
if (key === this.editor.sessionKey) {
return this.editor;
}
}
});
this.editor = editorCopy;
}
}
restore(asCopy) {
const run = () => {
let item;
if (asCopy) {
const contentCopy = Object.assign({}, this.content);
if (contentCopy.title) {
contentCopy.title += " (copy)";
}
item = this.modelManager.createItem({
content_type: 'Note',
content: contentCopy
});
this.modelManager.addItem(item);
} else {
const uuid = this.uuid;
item = this.modelManager.findItem(uuid);
item.content = Object.assign({}, this.content);
this.modelManager.mapResponseItemsToLocalModels(
[item],
SFModelManager.MappingSourceRemoteActionRetrieved
);
}
this.modelManager.setItemDirty(item);
this.syncManager.sync();
this.dismiss();
}
if (!asCopy) {
this.alertManager.confirm({
text: "Are you sure you want to replace the current note's contents with what you see in this preview?",
destructive: true,
onConfirm: run
})
} else {
run();
}
}
dismiss() {
this.$element.remove();
this.$scope.$destroy();
}
}
export class RevisionPreviewModal {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = RevisionPreviewModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
uuid: '=',
content: '='
};
}
}

View File

@@ -0,0 +1,118 @@
import template from '%/directives/session-history-menu.pug';
class SessionHistoryMenuCtrl {
/* @ngInject */
constructor(
$timeout,
actionsManager,
alertManager,
sessionHistory,
) {
this.$timeout = $timeout;
this.alertManager = alertManager;
this.actionsManager = actionsManager;
this.sessionHistory = sessionHistory;
this.diskEnabled = this.sessionHistory.diskEnabled;
this.autoOptimize = this.sessionHistory.autoOptimize;
}
$onInit() {
this.reloadHistory();
}
reloadHistory() {
const history = this.sessionHistory.historyForItem(this.item);
this.entries = history.entries.slice(0).sort((a, b) => {
return a.item.updated_at < b.item.updated_at ? 1 : -1;
})
this.history = history;
}
openRevision(revision) {
this.actionsManager.presentRevisionPreviewModal(
revision.item.uuid,
revision.item.content
);
}
classForRevision(revision) {
const vector = revision.operationVector();
if (vector === 0) {
return 'default';
} else if (vector === 1) {
return 'success';
} else if (vector === -1) {
return 'danger';
}
}
clearItemHistory() {
this.alertManager.confirm({
text: "Are you sure you want to delete the local session history for this note?",
destructive: true,
onConfirm: () => {
this.sessionHistory.clearHistoryForItem(this.item).then(() => {
this.$timeout(() => {
this.reloadHistory();
})
});
}
})
}
clearAllHistory() {
this.alertManager.confirm({
text: "Are you sure you want to delete the local session history for all notes?",
destructive: true,
onConfirm: () => {
this.sessionHistory.clearAllHistory().then(() => {
this.$timeout(() => {
this.reloadHistory();
})
});
}
})
}
toggleDiskSaving() {
const run = () => {
this.sessionHistory.toggleDiskSaving().then(() => {
this.$timeout(() => {
this.diskEnabled = this.sessionHistory.diskEnabled;
})
});
}
if (!this.sessionHistory.diskEnabled) {
this.alertManager.confirm({
text: `Are you sure you want to save history to disk? This will decrease general
performance, especially as you type. You are advised to disable this feature
if you experience any lagging.`,
destructive: true,
onConfirm: run
})
} else {
run();
}
}
toggleAutoOptimize() {
this.sessionHistory.toggleAutoOptimize().then(() => {
this.$timeout(() => {
this.autoOptimize = this.sessionHistory.autoOptimize;
})
});
}
}
export class SessionHistoryMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = SessionHistoryMenuCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
item: '='
};
}
}

View File

@@ -0,0 +1,57 @@
import template from '%/directives/sync-resolution-menu.pug';
class SyncResolutionMenuCtrl {
/* @ngInject */
constructor(
$timeout,
archiveManager,
syncManager,
) {
this.$timeout = $timeout;
this.archiveManager = archiveManager;
this.syncManager = syncManager;
this.status = {};
}
downloadBackup(encrypted) {
this.archiveManager.downloadBackup(encrypted);
this.status.backupFinished = true;
}
skipBackup() {
this.status.backupFinished = true;
}
async performSyncResolution() {
this.status.resolving = true;
await this.syncManager.resolveOutOfSync();
this.$timeout(() => {
this.status.resolving = false;
this.status.attemptedResolution = true;
if (this.syncManager.isOutOfSync()) {
this.status.fail = true;
} else {
this.status.success = true;
}
})
}
close() {
this.$timeout(() => {
this.closeFunction()();
})
}
}
export class SyncResolutionMenu {
constructor() {
this.restrict = 'E';
this.template = template;
this.controller = SyncResolutionMenuCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
closeFunction: '&'
};
}
}