feat: select multiple notes in list

This commit is contained in:
Baptiste Grob
2021-04-06 18:09:40 +02:00
parent 9599f30ad4
commit 0f53361689
11 changed files with 354 additions and 273 deletions

View File

@@ -1,8 +0,0 @@
export { AlertService } from './alertService';
export { ArchiveManager } from './archiveManager';
export { DesktopManager } from './desktopManager';
export { KeyboardManager } from './keyboardManager';
export { AutolockService } from './autolock_service';
export { NativeExtManager } from './nativeExtManager';
export { StatusManager } from './statusManager';
export { ThemeManager } from './themeManager';

View File

@@ -0,0 +1,171 @@
import { removeFromArray } from '@standardnotes/snjs';
export enum KeyboardKey {
Tab = 'Tab',
Backspace = 'Backspace',
Up = 'ArrowUp',
Down = 'ArrowDown',
}
export enum KeyboardModifier {
Shift = 'Shift',
Ctrl = 'Control',
/** ⌘ key on Mac, ⊞ key on Windows */
Meta = 'Meta',
Alt = 'Alt',
}
enum KeyboardKeyEvent {
Down = 'KeyEventDown',
Up = 'KeyEventUp',
}
type KeyboardObserver = {
key?: KeyboardKey | string;
modifiers?: KeyboardModifier[];
onKeyDown?: (event: KeyboardEvent) => void;
onKeyUp?: (event: KeyboardEvent) => void;
element?: HTMLElement;
elements?: HTMLElement[];
notElement?: HTMLElement;
notElementIds?: string[];
};
export class IOService {
readonly activeModifiers = new Set<KeyboardModifier>();
private observers: KeyboardObserver[] = [];
constructor(private isMac: boolean) {
window.addEventListener('keydown', this.handleKeyDown);
window.addEventListener('keyup', this.handleKeyUp);
}
public deinit() {
this.observers.length = 0;
window.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener('keyup', this.handleKeyUp);
(this.handleKeyDown as unknown) = undefined;
(this.handleKeyUp as unknown) = undefined;
}
handleKeyDown = (event: KeyboardEvent) => {
for (const modifier of this.modifiersForEvent(event)) {
switch (modifier) {
case KeyboardModifier.Meta: {
if (this.isMac) {
this.activeModifiers.add(modifier);
}
break;
}
case KeyboardModifier.Ctrl: {
if (!this.isMac) {
this.activeModifiers.add(modifier);
}
break;
}
default: {
this.activeModifiers.add(modifier);
break;
}
}
}
this.notifyObserver(event, KeyboardKeyEvent.Down);
};
handleKeyUp = (event: KeyboardEvent) => {
for (const modifier of this.modifiersForEvent(event)) {
this.activeModifiers.delete(modifier);
}
this.notifyObserver(event, KeyboardKeyEvent.Up);
};
modifiersForEvent(event: KeyboardEvent) {
const allModifiers = Object.values(KeyboardModifier);
const eventModifiers = allModifiers.filter((modifier) => {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
const matches =
((event.ctrlKey || event.key === KeyboardModifier.Ctrl) &&
modifier === KeyboardModifier.Ctrl) ||
((event.metaKey || event.key === KeyboardModifier.Meta) &&
modifier === KeyboardModifier.Meta) ||
((event.altKey || event.key === KeyboardModifier.Alt) &&
modifier === KeyboardModifier.Alt) ||
((event.shiftKey || event.key === KeyboardModifier.Shift) &&
modifier === KeyboardModifier.Shift);
return matches;
});
return eventModifiers;
}
eventMatchesKeyAndModifiers(
event: KeyboardEvent,
key: KeyboardKey | string,
modifiers: KeyboardModifier[] = []
) {
const eventModifiers = this.modifiersForEvent(event);
if (eventModifiers.length !== modifiers.length) {
return false;
}
for (const modifier of modifiers) {
if (!eventModifiers.includes(modifier)) {
return false;
}
}
// Modifers match, check key
if (!key) {
return true;
}
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
// In our case we don't differentiate between the two.
return key.toLowerCase() === event.key.toLowerCase();
}
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent) {
const target = event.target as HTMLElement;
for (const observer of this.observers) {
if (observer.element && event.target !== observer.element) {
continue;
}
if (observer.elements && !observer.elements.includes(target)) {
continue;
}
if (observer.notElement && observer.notElement === event.target) {
continue;
}
if (
observer.notElementIds &&
observer.notElementIds.includes(target.id)
) {
continue;
}
if (
this.eventMatchesKeyAndModifiers(
event,
observer.key!,
observer.modifiers
)
) {
const callback =
keyEvent === KeyboardKeyEvent.Down
? observer.onKeyDown
: observer.onKeyUp;
if (callback) {
callback(event);
}
}
}
}
addKeyObserver(observer: KeyboardObserver) {
this.observers.push(observer);
return () => {
removeFromArray(this.observers, observer);
};
}
}

View File

@@ -1,147 +0,0 @@
import { removeFromArray } from '@standardnotes/snjs';
export enum KeyboardKey {
Tab = "Tab",
Backspace = "Backspace",
Up = "ArrowUp",
Down = "ArrowDown",
}
export enum KeyboardModifier {
Shift = "Shift",
Ctrl = "Control",
/** ⌘ key on Mac, ⊞ key on Windows */
Meta = "Meta",
Alt = "Alt",
}
enum KeyboardKeyEvent {
Down = "KeyEventDown",
Up = "KeyEventUp"
}
type KeyboardObserver = {
key?: KeyboardKey | string
modifiers?: KeyboardModifier[]
onKeyDown?: (event: KeyboardEvent) => void
onKeyUp?: (event: KeyboardEvent) => void
element?: HTMLElement
elements?: HTMLElement[]
notElement?: HTMLElement
notElementIds?: string[]
}
export class KeyboardManager {
private observers: KeyboardObserver[] = []
private handleKeyDown: any
private handleKeyUp: any
constructor() {
this.handleKeyDown = (event: KeyboardEvent) => {
this.notifyObserver(event, KeyboardKeyEvent.Down);
};
this.handleKeyUp = (event: KeyboardEvent) => {
this.notifyObserver(event, KeyboardKeyEvent.Up);
};
window.addEventListener('keydown', this.handleKeyDown);
window.addEventListener('keyup', this.handleKeyUp);
}
public deinit() {
this.observers.length = 0;
window.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener('keyup', this.handleKeyUp);
this.handleKeyDown = undefined;
this.handleKeyUp = undefined;
}
modifiersForEvent(event: KeyboardEvent) {
const allModifiers = Object.values(KeyboardModifier);
const eventModifiers = allModifiers.filter((modifier) => {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
const matches = (
(
(event.ctrlKey || event.key === KeyboardModifier.Ctrl)
&& modifier === KeyboardModifier.Ctrl
) ||
(
(event.metaKey || event.key === KeyboardModifier.Meta)
&& modifier === KeyboardModifier.Meta
) ||
(
(event.altKey || event.key === KeyboardModifier.Alt)
&& modifier === KeyboardModifier.Alt
) ||
(
(event.shiftKey || event.key === KeyboardModifier.Shift)
&& modifier === KeyboardModifier.Shift
)
);
return matches;
});
return eventModifiers;
}
eventMatchesKeyAndModifiers(
event: KeyboardEvent,
key: KeyboardKey | string,
modifiers: KeyboardModifier[] = []
) {
const eventModifiers = this.modifiersForEvent(event);
if (eventModifiers.length !== modifiers.length) {
return false;
}
for (const modifier of modifiers) {
if (!eventModifiers.includes(modifier)) {
return false;
}
}
// Modifers match, check key
if (!key) {
return true;
}
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
// In our case we don't differentiate between the two.
return key.toLowerCase() === event.key.toLowerCase();
}
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent) {
const target = event.target as HTMLElement;
for (const observer of this.observers) {
if (observer.element && event.target !== observer.element) {
continue;
}
if (observer.elements && !observer.elements.includes(target)) {
continue;
}
if (observer.notElement && observer.notElement === event.target) {
continue;
}
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
continue;
}
if (this.eventMatchesKeyAndModifiers(event, observer.key!, observer.modifiers)) {
const callback = keyEvent === KeyboardKeyEvent.Down
? observer.onKeyDown
: observer.onKeyUp;
if (callback) {
callback(event);
}
}
}
}
addKeyObserver(observer: KeyboardObserver) {
this.observers.push(observer);
return () => {
removeFromArray(this.observers, observer);
};
}
}

View File

@@ -19,6 +19,7 @@ import { ActionsMenuState } from './actions_menu_state';
import { NoAccountWarningState } from './no_account_warning_state'; import { NoAccountWarningState } from './no_account_warning_state';
import { SyncState } from './sync_state'; import { SyncState } from './sync_state';
import { SearchOptionsState } from './search_options_state'; import { SearchOptionsState } from './search_options_state';
import { NotesState } from './notes_state';
export enum AppStateEvent { export enum AppStateEvent {
TagChanged, TagChanged,
@@ -62,7 +63,8 @@ export class AppState {
readonly actionsMenu = new ActionsMenuState(); readonly actionsMenu = new ActionsMenuState();
readonly noAccountWarning: NoAccountWarningState; readonly noAccountWarning: NoAccountWarningState;
readonly sync = new SyncState(); readonly sync = new SyncState();
readonly searchOptions; readonly searchOptions: SearchOptionsState;
readonly notes: NotesState;
isSessionsModalVisible = false; isSessionsModalVisible = false;
private appEventObserverRemovers: (() => void)[] = []; private appEventObserverRemovers: (() => void)[] = [];
@@ -77,6 +79,12 @@ export class AppState {
this.$timeout = $timeout; this.$timeout = $timeout;
this.$rootScope = $rootScope; this.$rootScope = $rootScope;
this.application = application; this.application = application;
this.notes = new NotesState(
this.application,
async () => {
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
}
);
this.noAccountWarning = new NoAccountWarningState( this.noAccountWarning = new NoAccountWarningState(
application, application,
this.appEventObserverRemovers this.appEventObserverRemovers
@@ -175,28 +183,6 @@ export class AppState {
} }
} }
async openEditor(noteUuid: string): Promise<void> {
if (this.getActiveEditor()?.note?.uuid === noteUuid) {
return;
}
const note = this.application.findItem(noteUuid) as SNNote;
if (!note) {
console.warn('Tried accessing a non-existant note of UUID ' + noteUuid);
return;
}
if (await this.application.authorizeNoteAccess(note)) {
const activeEditor = this.getActiveEditor();
if (!activeEditor) {
this.application.editorGroup.createEditor(noteUuid);
} else {
activeEditor.setNote(note);
}
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
}
}
getActiveEditor() { getActiveEditor() {
return this.application.editorGroup.editors[0]; return this.application.editorGroup.editors[0];
} }

View File

@@ -0,0 +1,66 @@
import { KeyboardModifier } from "@/services/ioService";
import { UuidString, SNNote } from "@standardnotes/snjs";
import { makeObservable, observable, action } from "mobx";
import { WebApplication } from "../application";
import { Editor } from "../editor";
export class NotesState {
selectedNotes: Record<UuidString, SNNote> = {};
constructor(
private application: WebApplication,
private onActiveEditorChanged: () => Promise<void>
) {
makeObservable(this, {
selectedNotes: observable,
selectNote: action,
});
}
get activeEditor(): Editor | undefined {
return this.application.editorGroup.editors[0];
}
async selectNote(note: SNNote): Promise<void> {
if (
this.io.activeModifiers.has(KeyboardModifier.Meta) ||
this.io.activeModifiers.has(KeyboardModifier.Ctrl)
) {
this.selectedNotes[note.uuid] = note;
} else {
this.selectedNotes = {
[note.uuid]: note,
};
}
await this.openEditor(note.uuid);
}
async openEditor(noteUuid: string): Promise<void> {
if (this.activeEditor?.note?.uuid === noteUuid) {
return;
}
const note = this.application.findItem(noteUuid) as SNNote | undefined;
if (!note) {
console.warn('Tried accessing a non-existant note of UUID ' + noteUuid);
return;
}
if (await this.application.authorizeNoteAccess(note)) {
if (!this.activeEditor) {
this.application.editorGroup.createEditor(noteUuid);
} else {
this.activeEditor.setNote(note);
}
await this.onActiveEditorChanged();
if (note.waitingForKey) {
this.application.presentKeyRecoveryWizard();
}
}
}
private get io() {
return this.application.io;
}
}

View File

@@ -9,23 +9,21 @@ import {
SNComponent, SNComponent,
PermissionDialog, PermissionDialog,
DeinitSource, DeinitSource,
Platform,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import angular from 'angular'; import angular from 'angular';
import { getPlatform } from '@/utils';
import { AlertService } from '@/services/alertService';
import { WebDeviceInterface } from '@/web_device_interface'; import { WebDeviceInterface } from '@/web_device_interface';
import {
DesktopManager,
AutolockService,
ArchiveManager,
NativeExtManager,
StatusManager,
ThemeManager,
KeyboardManager
} from '@/services';
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { Bridge } from '@/services/bridge'; import { Bridge } from '@/services/bridge';
import { WebCrypto } from '@/crypto'; import { WebCrypto } from '@/crypto';
import { AlertService } from '@/services/alertService';
import { AutolockService } from '@/services/autolock_service';
import { ArchiveManager } from '@/services/archiveManager';
import { DesktopManager } from '@/services/desktopManager';
import { IOService } from '@/services/ioService';
import { NativeExtManager } from '@/services/nativeExtManager';
import { StatusManager } from '@/services/statusManager';
import { ThemeManager } from '@/services/themeManager';
type WebServices = { type WebServices = {
appState: AppState; appState: AppState;
@@ -35,7 +33,7 @@ type WebServices = {
nativeExtService: NativeExtManager; nativeExtService: NativeExtManager;
statusManager: StatusManager; statusManager: StatusManager;
themeService: ThemeManager; themeService: ThemeManager;
keyboardService: KeyboardManager; io: IOService;
} }
export class WebApplication extends SNApplication { export class WebApplication extends SNApplication {
@@ -49,6 +47,7 @@ export class WebApplication extends SNApplication {
/* @ngInject */ /* @ngInject */
constructor( constructor(
deviceInterface: WebDeviceInterface, deviceInterface: WebDeviceInterface,
platform: Platform,
identifier: string, identifier: string,
private $compile: angular.ICompileService, private $compile: angular.ICompileService,
scope: angular.IScope, scope: angular.IScope,
@@ -57,7 +56,7 @@ export class WebApplication extends SNApplication {
) { ) {
super( super(
bridge.environment, bridge.environment,
getPlatform(), platform,
deviceInterface, deviceInterface,
WebCrypto, WebCrypto,
new AlertService(), new AlertService(),
@@ -139,8 +138,8 @@ export class WebApplication extends SNApplication {
return this.webServices.themeService; return this.webServices.themeService;
} }
public getKeyboardService() { public get io() {
return this.webServices.keyboardService; return this.webServices.io;
} }
async checkForSecurityUpdate() { async checkForSecurityUpdate() {

View File

@@ -1,24 +1,26 @@
import { WebDeviceInterface } from '@/web_device_interface'; import { WebDeviceInterface } from '@/web_device_interface';
import { WebApplication } from './application'; import { WebApplication } from './application';
import { ApplicationDescriptor, SNApplicationGroup, DeviceInterface } from '@standardnotes/snjs';
import { import {
ArchiveManager, ApplicationDescriptor,
DesktopManager, SNApplicationGroup,
KeyboardManager, DeviceInterface,
AutolockService, Platform,
NativeExtManager, } from '@standardnotes/snjs';
StatusManager,
ThemeManager
} from '@/services';
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { Bridge } from '@/services/bridge'; import { Bridge } from '@/services/bridge';
import { isDesktopApplication } from '@/utils'; import { getPlatform, isDesktopApplication } from '@/utils';
import { ArchiveManager } from '@/services/archiveManager';
import { DesktopManager } from '@/services/desktopManager';
import { IOService } from '@/services/ioService';
import { AutolockService } from '@/services/autolock_service';
import { StatusManager } from '@/services/statusManager';
import { NativeExtManager } from '@/services/nativeExtManager';
import { ThemeManager } from '@/services/themeManager';
export class ApplicationGroup extends SNApplicationGroup { export class ApplicationGroup extends SNApplicationGroup {
$compile: ng.ICompileService;
$compile: ng.ICompileService $rootScope: ng.IRootScopeService;
$rootScope: ng.IRootScopeService $timeout: ng.ITimeoutService;
$timeout: ng.ITimeoutService
/* @ngInject */ /* @ngInject */
constructor( constructor(
@@ -26,75 +28,72 @@ export class ApplicationGroup extends SNApplicationGroup {
$rootScope: ng.IRootScopeService, $rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService, $timeout: ng.ITimeoutService,
private defaultSyncServerHost: string, private defaultSyncServerHost: string,
private bridge: Bridge, private bridge: Bridge
) { ) {
super(new WebDeviceInterface( super(new WebDeviceInterface($timeout, bridge));
$timeout,
bridge
));
this.$compile = $compile; this.$compile = $compile;
this.$timeout = $timeout; this.$timeout = $timeout;
this.$rootScope = $rootScope; this.$rootScope = $rootScope;
} }
async initialize(callback?: any) { async initialize(callback?: any): Promise<void> {
await super.initialize({ await super.initialize({
applicationCreator: this.createApplication applicationCreator: this.createApplication,
}); });
if (isDesktopApplication()) { if (isDesktopApplication()) {
Object.defineProperty(window, 'desktopManager', { Object.defineProperty(window, 'desktopManager', {
get: () => (this.primaryApplication as WebApplication).getDesktopService() get: () =>
(this.primaryApplication as WebApplication).getDesktopService(),
}); });
} }
} }
private createApplication = (descriptor: ApplicationDescriptor, deviceInterface: DeviceInterface) => { private createApplication = (
descriptor: ApplicationDescriptor,
deviceInterface: DeviceInterface
) => {
const scope = this.$rootScope.$new(true); const scope = this.$rootScope.$new(true);
const platform = getPlatform();
const application = new WebApplication( const application = new WebApplication(
deviceInterface as WebDeviceInterface, deviceInterface as WebDeviceInterface,
platform,
descriptor.identifier, descriptor.identifier,
this.$compile, this.$compile,
scope, scope,
this.defaultSyncServerHost, this.defaultSyncServerHost,
this.bridge, this.bridge
); );
const appState = new AppState( const appState = new AppState(
this.$rootScope, this.$rootScope,
this.$timeout, this.$timeout,
application, application,
this.bridge, this.bridge
);
const archiveService = new ArchiveManager(
application
); );
const archiveService = new ArchiveManager(application);
const desktopService = new DesktopManager( const desktopService = new DesktopManager(
this.$rootScope, this.$rootScope,
this.$timeout, this.$timeout,
application, application,
this.bridge, this.bridge
); );
const keyboardService = new KeyboardManager(); const io = new IOService(
const autolockService = new AutolockService( platform === Platform.MacWeb || platform === Platform.MacDesktop
application
);
const nativeExtService = new NativeExtManager(
application
);
const statusService = new StatusManager();
const themeService = new ThemeManager(
application,
); );
const autolockService = new AutolockService(application);
const nativeExtService = new NativeExtManager(application);
const statusManager = new StatusManager();
const themeService = new ThemeManager(application);
application.setWebServices({ application.setWebServices({
appState, appState,
archiveService, archiveService,
desktopService, desktopService,
keyboardService, io,
autolockService, autolockService,
nativeExtService, nativeExtService,
statusManager: statusService, statusManager,
themeService themeService,
}); });
return application; return application;
} };
} }

View File

@@ -1,5 +1,7 @@
import { ApplicationEvent } from '@standardnotes/snjs'; import { ApplicationEvent } from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { autorun, IReactionDisposer, IReactionPublic } from 'mobx';
export type CtrlState = Partial<Record<string, any>> export type CtrlState = Partial<Record<string, any>>
export type CtrlProps = Partial<Record<string, any>> export type CtrlProps = Partial<Record<string, any>>
@@ -17,6 +19,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
* no Angular handlebars/syntax render in the UI before display data is ready. * no Angular handlebars/syntax render in the UI before display data is ready.
*/ */
protected templateReady = false protected templateReady = false
private reactionDisposers: IReactionDisposer[] = [];
/* @ngInject */ /* @ngInject */
constructor( constructor(
@@ -26,7 +29,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
this.$timeout = $timeout; this.$timeout = $timeout;
} }
$onInit() { $onInit(): void {
this.state = { this.state = {
...this.getInitialState(), ...this.getInitialState(),
...this.state, ...this.state,
@@ -36,9 +39,13 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
this.templateReady = true; this.templateReady = true;
} }
deinit() { deinit(): void {
this.unsubApp(); this.unsubApp();
this.unsubState(); this.unsubState();
for (const disposer of this.reactionDisposers) {
disposer();
}
this.reactionDisposers.length = 0;
this.unsubApp = undefined; this.unsubApp = undefined;
this.unsubState = undefined; this.unsubState = undefined;
if (this.stateTimeout) { if (this.stateTimeout) {
@@ -46,16 +53,16 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
} }
} }
$onDestroy() { $onDestroy(): void {
this.deinit(); this.deinit();
} }
public get appState() { public get appState(): AppState {
return this.application!.getAppState(); return this.application.getAppState();
} }
/** @private */ /** @private */
async resetState() { async resetState(): Promise<void> {
this.state = this.getInitialState(); this.state = this.getInitialState();
await this.setState(this.state); await this.setState(this.state);
} }
@@ -65,7 +72,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
return {} as any; return {} as any;
} }
async setState(state: Partial<S>) { async setState(state: Partial<S>): Promise<void> {
if (!this.$timeout) { if (!this.$timeout) {
return; return;
} }
@@ -88,17 +95,21 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
} }
/** @returns a promise that resolves after the UI has been updated. */ /** @returns a promise that resolves after the UI has been updated. */
flushUI() { flushUI(): angular.IPromise<void> {
return this.$timeout(); return this.$timeout();
} }
initProps(props: CtrlProps) { initProps(props: CtrlProps): void {
if (Object.keys(this.props).length > 0) { if (Object.keys(this.props).length > 0) {
throw 'Already init-ed props.'; throw 'Already init-ed props.';
} }
this.props = Object.freeze(Object.assign({}, this.props, props)); this.props = Object.freeze(Object.assign({}, this.props, props));
} }
autorun(view: (r: IReactionPublic) => void): void {
this.reactionDisposers.push(autorun(view));
}
addAppStateObserver() { addAppStateObserver() {
this.unsubState = this.application!.getAppState().addObserver( this.unsubState = this.application!.getAppState().addObserver(
async (eventName, data) => { async (eventName, data) => {

View File

@@ -24,7 +24,7 @@ import {
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import find from 'lodash/find'; import find from 'lodash/find';
import { isDesktopApplication } from '@/utils'; import { isDesktopApplication } from '@/utils';
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager'; import { KeyboardModifier, KeyboardKey } from '@/services/ioService';
import template from './editor-view.pug'; import template from './editor-view.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { EventSource } from '@/ui_models/app_state'; import { EventSource } from '@/ui_models/app_state';
@@ -1106,7 +1106,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
registerKeyboardShortcuts() { registerKeyboardShortcuts() {
this.removeAltKeyObserver = this.application this.removeAltKeyObserver = this.application
.getKeyboardService() .io
.addKeyObserver({ .addKeyObserver({
modifiers: [KeyboardModifier.Alt], modifiers: [KeyboardModifier.Alt],
onKeyDown: () => { onKeyDown: () => {
@@ -1122,7 +1122,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
}); });
this.removeTrashKeyObserver = this.application this.removeTrashKeyObserver = this.application
.getKeyboardService() .io
.addKeyObserver({ .addKeyObserver({
key: KeyboardKey.Backspace, key: KeyboardKey.Backspace,
notElementIds: [ElementIds.NoteTextEditor, ElementIds.NoteTitleEditor], notElementIds: [ElementIds.NoteTextEditor, ElementIds.NoteTitleEditor],
@@ -1147,7 +1147,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
ElementIds.NoteTextEditor ElementIds.NoteTextEditor
)! as HTMLInputElement; )! as HTMLInputElement;
this.removeTabObserver = this.application this.removeTabObserver = this.application
.getKeyboardService() .io
.addKeyObserver({ .addKeyObserver({
element: editor, element: editor,
key: KeyboardKey.Tab, key: KeyboardKey.Tab,

View File

@@ -127,7 +127,7 @@
) )
.note( .note(
ng-repeat='note in self.state.renderedNotes track by note.uuid' ng-repeat='note in self.state.renderedNotes track by note.uuid'
ng-class="{'selected' : self.activeEditorNote.uuid == note.uuid}" ng-class="{'selected' : self.isNoteSelected(note.uuid) }"
ng-click='self.selectNote(note)' ng-click='self.selectNote(note)'
) )
.note-flags(ng-show='self.noteFlags[note.uuid].length > 0') .note-flags(ng-show='self.noteFlags[note.uuid].length > 0')

View File

@@ -14,17 +14,17 @@ import {
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { AppStateEvent } from '@/ui_models/app_state'; import { AppStateEvent } from '@/ui_models/app_state';
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager'; import { KeyboardKey, KeyboardModifier } from '@/services/ioService';
import { import {
PANEL_NAME_NOTES PANEL_NAME_NOTES
} from '@/views/constants'; } from '@/views/constants';
import { autorun, IReactionDisposer } from 'mobx';
type NotesState = { type NotesCtrlState = {
panelTitle: string panelTitle: string
notes: SNNote[] notes: SNNote[]
renderedNotes: SNNote[] renderedNotes: SNNote[]
renderedNotesTags: string[], renderedNotesTags: string[],
selectedNotes: Record<UuidString, SNNote>,
sortBy?: string sortBy?: string
sortReverse?: boolean sortReverse?: boolean
showArchived?: boolean showArchived?: boolean
@@ -65,7 +65,7 @@ const DEFAULT_LIST_NUM_NOTES = 20;
const ELEMENT_ID_SEARCH_BAR = 'search-bar'; const ELEMENT_ID_SEARCH_BAR = 'search-bar';
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable'; const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> { class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
private panelPuppet?: PanelPuppet private panelPuppet?: PanelPuppet
private reloadNotesPromise?: any private reloadNotesPromise?: any
@@ -78,7 +78,6 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
private searchKeyObserver: any private searchKeyObserver: any
private noteFlags: Partial<Record<UuidString, NoteFlag[]>> = {} private noteFlags: Partial<Record<UuidString, NoteFlag[]>> = {}
private removeObservers: Array<() => void> = []; private removeObservers: Array<() => void> = [];
private appStateObserver?: IReactionDisposer;
/* @ngInject */ /* @ngInject */
constructor($timeout: ng.ITimeoutService,) { constructor($timeout: ng.ITimeoutService,) {
@@ -95,7 +94,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
this.onPanelResize = this.onPanelResize.bind(this); this.onPanelResize = this.onPanelResize.bind(this);
window.addEventListener('resize', this.onWindowResize, true); window.addEventListener('resize', this.onWindowResize, true);
this.registerKeyboardShortcuts(); this.registerKeyboardShortcuts();
this.appStateObserver = autorun(async () => { this.autorun(async () => {
const { const {
includeProtectedContents, includeProtectedContents,
includeArchived, includeArchived,
@@ -113,6 +112,11 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
this.reloadNotes(); this.reloadNotes();
} }
}); });
this.autorun(() => {
this.setState({
selectedNotes: this.appState.notes.selectedNotes,
});
});
} }
onWindowResize() { onWindowResize() {
@@ -131,7 +135,6 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
this.nextNoteKeyObserver(); this.nextNoteKeyObserver();
this.previousNoteKeyObserver(); this.previousNoteKeyObserver();
this.searchKeyObserver(); this.searchKeyObserver();
this.appStateObserver?.();
this.newNoteKeyObserver = undefined; this.newNoteKeyObserver = undefined;
this.nextNoteKeyObserver = undefined; this.nextNoteKeyObserver = undefined;
this.previousNoteKeyObserver = undefined; this.previousNoteKeyObserver = undefined;
@@ -139,15 +142,16 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
super.deinit(); super.deinit();
} }
async setNotesState(state: Partial<NotesState>) { async setNotesState(state: Partial<NotesCtrlState>) {
return this.setState(state); return this.setState(state);
} }
getInitialState(): NotesState { getInitialState(): NotesCtrlState {
return { return {
notes: [], notes: [],
renderedNotes: [], renderedNotes: [],
renderedNotesTags: [], renderedNotesTags: [],
selectedNotes: {},
mutable: { showMenu: false }, mutable: { showMenu: false },
noteFilter: { noteFilter: {
text: '', text: '',
@@ -180,9 +184,13 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
} }
} }
private get activeEditorNote() {
return this.appState.notes.activeEditor?.note;
}
/** @template */ /** @template */
public get activeEditorNote() { public isNoteSelected(uuid: UuidString) {
return this.appState?.getActiveEditor()?.note; return !!this.state.selectedNotes[uuid];
} }
public get editorNotes() { public get editorNotes() {
@@ -288,12 +296,8 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
)); ));
} }
async selectNote(note: SNNote) { selectNote(note: SNNote): Promise<void> {
await this.appState.openEditor(note.uuid); return this.appState.notes.selectNote(note);
if (note.waitingForKey) {
this.application.presentKeyRecoveryWizard();
}
this.reloadNotes();
} }
async createNewNote() { async createNewNote() {
@@ -461,7 +465,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
} }
async reloadPreferences() { async reloadPreferences() {
const viewOptions = {} as NotesState; const viewOptions = {} as NotesCtrlState;
const prevSortValue = this.state.sortBy; const prevSortValue = this.state.sortBy;
let sortBy = this.application.getPreference( let sortBy = this.application.getPreference(
PrefKey.SortNotesBy, PrefKey.SortNotesBy,
@@ -673,7 +677,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
selectNextNote() { selectNextNote() {
const displayableNotes = this.state.notes; const displayableNotes = this.state.notes;
const currentIndex = displayableNotes.findIndex((candidate) => { const currentIndex = displayableNotes.findIndex((candidate) => {
return candidate.uuid === this.activeEditorNote.uuid; return candidate.uuid === this.activeEditorNote?.uuid;
}); });
if (currentIndex + 1 < displayableNotes.length) { if (currentIndex + 1 < displayableNotes.length) {
this.selectNote(displayableNotes[currentIndex + 1]); this.selectNote(displayableNotes[currentIndex + 1]);
@@ -791,7 +795,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
* use Control modifier as well. These rules don't apply to desktop, but * use Control modifier as well. These rules don't apply to desktop, but
* probably better to be consistent. * probably better to be consistent.
*/ */
this.newNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({ this.newNoteKeyObserver = this.application.io.addKeyObserver({
key: 'n', key: 'n',
modifiers: [ modifiers: [
KeyboardModifier.Meta, KeyboardModifier.Meta,
@@ -803,7 +807,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
} }
}); });
this.nextNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({ this.nextNoteKeyObserver = this.application.io.addKeyObserver({
key: KeyboardKey.Down, key: KeyboardKey.Down,
elements: [ elements: [
document.body, document.body,
@@ -818,7 +822,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
} }
}); });
this.previousNoteKeyObserver = this.application.getKeyboardService().addKeyObserver({ this.previousNoteKeyObserver = this.application.io.addKeyObserver({
key: KeyboardKey.Up, key: KeyboardKey.Up,
element: document.body, element: document.body,
onKeyDown: () => { onKeyDown: () => {
@@ -826,7 +830,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
} }
}); });
this.searchKeyObserver = this.application.getKeyboardService().addKeyObserver({ this.searchKeyObserver = this.application.io.addKeyObserver({
key: "f", key: "f",
modifiers: [ modifiers: [
KeyboardModifier.Meta, KeyboardModifier.Meta,