Files
standardnotes-app-web/app/assets/javascripts/views/application/application_view.ts
Vardan Hakobyan f120af3b43 feat: handle unprotected session expiration (#779)
* feat: hide note contents if the protection expires when the protected note is open and wasn't edited for a while

* feat: handle session expiration for opened protected note for both plain advanced editors

* fix: if after canceling  session expiry modal only one unprotected note stays selected, show its contents in the editor

* refactor: handle session expiration for opened protected note (move the logic to web client)

* feat: handle the case of selecting "Don't remember" option in session expiry dialog

* test (WIP): add unit tests for protecting opened note after the session has expired

* test: add remaining unit tests

* refactor: move the opened note protection logic to "editor_view"

* refactor: reviewer comments
- don't rely on user signed-in/out status to require authentication for protected note
- remove unnecessary async/awaits
- better wording on ui

* refactor: reviewer's comments:
 - use snjs method to check if "Don't remember" option is selected in authentication modal
 - move the constant to snjs
 - fix eslint error

* refactor: avoid `any` type for `appEvent` payload

* test: add unit tests

* chore: update function name

* refactor: use simpler protection session event types

* refactor: protected access terminology

* refactor: start counting idle timer after every edit (instead of counting by interval in spite of edits)

* test: unit tests

* style: don't give extra brightness to the "View Note"/"Authenticate" button on hover/focus

* chore: bump snjs version

* chore: put snjs "beta" version

* fix: run protection timeout when the note is marked as protected

* chore: snjs version bump

* refactor: immediately lock the note if it's marked as "Protected"

* refactor: rename component, directive and some props

* refactor: remove extra check

* refactor: rename the method

* chore: update snjs version

Co-authored-by: Mo Bitar <me@bitar.io>
2021-12-20 20:54:37 +04:00

200 lines
5.4 KiB
TypeScript

import { RootScopeMessages } from './../../messages';
import { WebDirective } from '@/types';
import { getPlatformString } from '@/utils';
import template from './application-view.pug';
import { AppStateEvent, PanelResizedData } from '@/ui_models/app_state';
import { ApplicationEvent, Challenge, removeFromArray } from '@standardnotes/snjs';
import {
PANEL_NAME_NOTES,
PANEL_NAME_TAGS
} from '@/views/constants';
import {
STRING_DEFAULT_FILE_ERROR
} from '@/strings';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { alertDialog } from '@/services/alertService';
class ApplicationViewCtrl extends PureViewCtrl<unknown, {
ready?: boolean,
needsUnlock?: boolean,
appClass: string,
}> {
public platformString: string;
private notesCollapsed = false;
private tagsCollapsed = false;
/**
* To prevent stale state reads (setState is async),
* challenges is a mutable array
*/
private challenges: Challenge[] = [];
/* @ngInject */
constructor(
private $location: ng.ILocationService,
private $rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService
) {
super($timeout);
this.$location = $location;
this.$rootScope = $rootScope;
this.platformString = getPlatformString();
this.state = this.getInitialState();
this.onDragDrop = this.onDragDrop.bind(this);
this.onDragOver = this.onDragOver.bind(this);
this.addDragDropHandlers();
}
deinit() {
(this.$location as unknown) = undefined;
(this.$rootScope as unknown) = undefined;
(this.application as unknown) = undefined;
window.removeEventListener('dragover', this.onDragOver, true);
window.removeEventListener('drop', this.onDragDrop, true);
(this.onDragDrop as unknown) = undefined;
(this.onDragOver as unknown) = undefined;
super.deinit();
}
$onInit() {
super.$onInit();
this.loadApplication();
}
getInitialState() {
return {
appClass: '',
challenges: [],
};
}
async loadApplication() {
this.application.componentManager.setDesktopManager(
this.application.getDesktopService()
);
await this.application.prepareForLaunch({
receiveChallenge: async (challenge) => {
this.$timeout(() => {
this.challenges.push(challenge);
});
}
});
await this.application.launch();
}
public async removeChallenge(challenge: Challenge) {
this.$timeout(() => {
removeFromArray(this.challenges, challenge);
});
}
async onAppStart() {
super.onAppStart();
this.setState({
ready: true,
needsUnlock: this.application.hasPasscode()
});
}
async onAppLaunch() {
super.onAppLaunch();
this.setState({ needsUnlock: false });
this.handleDemoSignInFromParams();
}
onUpdateAvailable() {
this.$rootScope.$broadcast(RootScopeMessages.NewUpdateAvailable);
}
/** @override */
async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName);
switch (eventName) {
case ApplicationEvent.LocalDatabaseReadError:
alertDialog({
text: 'Unable to load local database. Please restart the app and try again.'
});
break;
case ApplicationEvent.LocalDatabaseWriteError:
alertDialog({
text: 'Unable to write to local database. Please restart the app and try again.'
});
break;
}
}
/** @override */
async onAppStateEvent(eventName: AppStateEvent, data?: unknown) {
if (eventName === AppStateEvent.PanelResized) {
const { panel, collapsed } = data as PanelResizedData;
if (panel === PANEL_NAME_NOTES) {
this.notesCollapsed = collapsed;
}
if (panel === PANEL_NAME_TAGS) {
this.tagsCollapsed = collapsed;
}
let appClass = "";
if (this.notesCollapsed) { appClass += "collapsed-notes"; }
if (this.tagsCollapsed) { appClass += " collapsed-tags"; }
this.setState({ appClass });
} else if (eventName === AppStateEvent.WindowDidFocus) {
if (!(await this.application.isLocked())) {
this.application.sync();
}
}
}
addDragDropHandlers() {
/**
* Disable dragging and dropping of files (but allow text) into main SN interface.
* both 'dragover' and 'drop' are required to prevent dropping of files.
* This will not prevent extensions from receiving drop events.
*/
window.addEventListener('dragover', this.onDragOver, true);
window.addEventListener('drop', this.onDragDrop, true);
}
onDragOver(event: DragEvent) {
if (event.dataTransfer?.files.length) {
event.preventDefault();
}
}
onDragDrop(event: DragEvent) {
if (event.dataTransfer?.files.length) {
event.preventDefault();
void alertDialog({
text: STRING_DEFAULT_FILE_ERROR
});
}
}
async handleDemoSignInFromParams() {
if (
this.$location.search().demo === 'true' &&
!this.application.hasAccount()
) {
await this.application.setCustomHost(
'https://syncing-server-demo.standardnotes.com'
);
this.application.signIn(
'demo@standardnotes.org',
'password',
);
}
}
}
export class ApplicationView extends WebDirective {
constructor() {
super();
this.template = template;
this.controller = ApplicationViewCtrl;
this.replace = true;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
application: '='
};
}
}