Release/3.6.0 (#527)

* feat: (wip) authorize note access

* fix: remove multiEditorEnabled

* refactor: update SNJS + eslint

* refactor: remove privileges in favor of SNJS protections

* fix: do not close editor when editing an archived note

* chore: remove progress indicator for webpack dev server

* fix: add rel="noreferrer" to bugsnag links

* chore(deps): upgrade snjs

* chore(deps): upgrade snjs

* feat: batch manager protection + react challenge modal + eslint fix

* fix: lint errors

* fix: launch state error

* fix: challenge modal: cancel instead of dismiss when pressing escape

* feat: improve focus styles

* fix: cancel session revoking when pressing escape on confirm dialog

* fix: lint warning

* chore(deps): upgrade minor versions

* feat: make SNWebCrypto a constant

* feat: add random identifier to bugsnag reports

* fix: check onKeyUp instead of onKeyDown

* feat: implement SNJS backup file password retrieval

* chore(deps): upgrade snjs

* feat: display warning banner when using the app with no account

* fix: properly color svg button

* fix: wording

* fix: hide account warning after login + improve key storage wording

* chore(deps): upgrade stylekit

* feat: use stylekit fonts for the editor

* chore(deps): bump nokogiri from 1.10.8 to 1.11.1 (#511)

Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.8 to 1.11.1.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.10.8...v1.11.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>

* chore(deps): bump ini from 1.3.5 to 1.3.8 (#504)

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>

* fix: rename master branch to main

* fix: add missing placeholders for submodules (#516)

Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>

* chore(deps): upgrade snjs, babel, typescript, reach, mobx, preact

* feat: clear protection session

* fix: use correct close icon size

* fix: hide protections paragraph when no account or passcode exist

* chore(deps): remove unused dependencies

* fix: button casing

* feat: implement SNApplication.hasProtectionSources

* chore(version): 3.6.0

* feat: enable sessions management for every build

* feat: make "Protected" flag more subtle

* fix: only match protected note title

* fix: remove inconsistencies between protected note label and date

* feat: show warning when protecting a note with no protection source

* feat: make unprotecting a note a protected action

* chore(deps): upgrade snjs

* chore(version): 3.6.0-beta01

* fix: run docker with root to fix crashing on Linux (undoes 62da387d3a) (#525)

* feat: make encrypted backups protected (#524)

Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: proletarius101 <54175165+proletarius101@users.noreply.github.com>
Co-authored-by: Darius JJ Chuck <79410894+standarius@users.noreply.github.com>
Co-authored-by: Antonella Sgarlatta <antonella@standardnotes.org>
This commit is contained in:
Baptiste Grob
2021-03-02 15:44:40 +01:00
committed by GitHub
parent 38707cc977
commit bef17ef534
84 changed files with 3410 additions and 2526 deletions

View File

@@ -0,0 +1,3 @@
declare module '*.svg' {
export default function SvgComponent(props: React.SVGProps<SVGSVGElement>): JSX.Element;
}

View File

@@ -44,8 +44,6 @@ import {
PanelResizer,
PasswordWizard,
PermissionsModal,
PrivilegesAuthModal,
PrivilegesManagementModal,
RevisionPreviewModal,
HistoryMenu,
SyncResolutionMenu,
@@ -57,7 +55,8 @@ import { BrowserBridge } from './services/browserBridge';
import { startErrorReporting } from './services/errorReporting';
import { StartApplication } from './startApplication';
import { Bridge } from './services/bridge';
import { SessionsModalDirective } from './directives/views/sessionsModal';
import { SessionsModalDirective } from './components/SessionsModal';
import { NoAccountWarningDirective } from './components/NoAccountWarning';
function reloadHiddenFirefoxTab(): boolean {
@@ -140,15 +139,11 @@ const startApplication: StartApplication = async function startApplication(
.directive('panelResizer', () => new PanelResizer())
.directive('passwordWizard', () => new PasswordWizard())
.directive('permissionsModal', () => new PermissionsModal())
.directive('privilegesAuthModal', () => new PrivilegesAuthModal())
.directive(
'privilegesManagementModal',
() => new PrivilegesManagementModal()
)
.directive('revisionPreviewModal', () => new RevisionPreviewModal())
.directive('historyMenu', () => new HistoryMenu())
.directive('syncResolutionMenu', () => new SyncResolutionMenu())
.directive('sessionsModal', SessionsModalDirective);
.directive('sessionsModal', SessionsModalDirective)
.directive('noAccountWarning', NoAccountWarningDirective);
// Filters
angular.module('app').filter('trusted', ['$sce', trusted]);

View File

@@ -0,0 +1,39 @@
import { toDirective, useAutorunValue } from './utils';
import Close from '../../icons/ic_close.svg';
import { AppState } from '@/ui_models/app_state';
function NoAccountWarning({ appState }: { appState: AppState }) {
const canShow = useAutorunValue(() => appState.noAccountWarning.show);
if (!canShow) {
return null;
}
return (
<div className="mt-5 p-5 rounded-md shadow-sm grid grid-template-cols-1fr">
<h1 className="sk-h3 m-0 font-semibold">Data not backed up</h1>
<p className="m-0 mt-1 col-start-1 col-end-3">
Sign in or register to back up your notes.
</p>
<button
className="sn-btn mt-3 col-start-1 col-end-3 justify-self-start"
onClick={(event) => {
event.stopPropagation();
appState.accountMenu.setShow(true);
}}
>
Open Account menu
</button>
<button
onClick={() => {
appState.noAccountWarning.hide();
}}
title="Ignore"
label="Ignore"
className="border-0 m-0 p-0 bg-transparent cursor-pointer rounded-md col-start-2 row-start-1 color-neutral hover:color-info"
>
<Close className="fill-current" />
</button>
</div>
);
}
export const NoAccountWarningDirective = toDirective(NoAccountWarning);

View File

@@ -1,14 +1,13 @@
import { AppState } from '@/ui_models/app_state';
import { PureViewCtrl } from '@/views';
import {
SNApplication,
RemoteSession,
SessionStrings,
UuidString,
isNullOrUndefined,
RemoteSession,
} from '@standardnotes/snjs';
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
import { render, FunctionComponent } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import { FunctionComponent } from 'preact';
import { useState, useEffect, useRef, useMemo } from 'preact/hooks';
import { Dialog } from '@reach/dialog';
import { Alert } from '@reach/alert';
import {
@@ -16,10 +15,8 @@ import {
AlertDialogDescription,
AlertDialogLabel,
} from '@reach/alert-dialog';
function useAutorun(view: (r: IReactionPublic) => any, opts?: IAutorunOptions) {
useEffect(() => autorun(view, opts), []);
}
import { toDirective, useAutorun } from './utils';
import { WebApplication } from '@/ui_models/application';
type Session = RemoteSession & {
revoking?: true;
@@ -56,16 +53,16 @@ function useSessions(
}
setRefreshing(false);
})();
}, [lastRefreshDate]);
}, [application, lastRefreshDate]);
function refresh() {
setLastRefreshDate(Date.now());
}
async function revokeSession(uuid: UuidString) {
const responsePromise = application.revokeSession(uuid);
const sessionsBeforeRevoke = sessions;
let sessionsBeforeRevoke = sessions;
const responsePromise = application.revokeSession(uuid);
const sessionsDuringRevoke = sessions.slice();
const toRemoveIndex = sessions.findIndex(
@@ -78,7 +75,9 @@ function useSessions(
setSessions(sessionsDuringRevoke);
const response = await responsePromise;
if ('error' in response) {
if (isNullOrUndefined(response)) {
setSessions(sessionsBeforeRevoke);
} else if ('error' in response) {
if (response.error?.message) {
setErrorMessage(response.error?.message);
} else {
@@ -111,19 +110,23 @@ const SessionsModal: FunctionComponent<{
const closeRevokeSessionAlert = () => setRevokingSessionUuid('');
const cancelRevokeRef = useRef<HTMLButtonElement>();
const formatter = new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
});
const formatter = useMemo(
() =>
new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
}),
[]
);
return (
<>
<Dialog onDismiss={close}>
<div className="sk-modal-content sessions-modal">
<Dialog onDismiss={close} className="sessions-modal">
<div className="sk-modal-content">
<div class="sn-component">
<div class="sk-panel">
<div class="sk-panel-header">
@@ -190,7 +193,12 @@ const SessionsModal: FunctionComponent<{
</div>
</Dialog>
{confirmRevokingSessionUuid && (
<AlertDialog leastDestructiveRef={cancelRevokeRef}>
<AlertDialog
onDismiss={() => {
setRevokingSessionUuid('');
}}
leastDestructiveRef={cancelRevokeRef}
>
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
@@ -235,7 +243,7 @@ const SessionsModal: FunctionComponent<{
const Sessions: FunctionComponent<{
appState: AppState;
application: SNApplication;
application: WebApplication;
}> = ({ appState, application }) => {
const [showModal, setShowModal] = useState(false);
useAutorun(() => setShowModal(appState.isSessionsModalVisible));
@@ -247,26 +255,4 @@ const Sessions: FunctionComponent<{
}
};
class SessionsModalCtrl extends PureViewCtrl<{}, {}> {
/* @ngInject */
constructor(private $element: JQLite, $timeout: ng.ITimeoutService) {
super($timeout);
this.$element = $element;
}
$onChanges() {
render(
<Sessions appState={this.appState} application={this.application} />,
this.$element[0]
);
}
}
export function SessionsModalDirective() {
return {
controller: SessionsModalCtrl,
bindToController: true,
scope: {
application: '=',
},
};
}
export const SessionsModalDirective = toDirective(Sessions);

View File

@@ -0,0 +1,56 @@
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
import { FunctionComponent, h, render } from 'preact';
import { useEffect } from 'preact/hooks';
import { useState } from 'react';
export function useAutorunValue<T>(query: () => T): T {
const [value, setValue] = useState(query);
useAutorun(() => {
setValue(query());
});
return value;
}
export function useAutorun(
view: (r: IReactionPublic) => unknown,
opts?: IAutorunOptions
): void {
useEffect(() => autorun(view, opts), [view, opts]);
}
export function toDirective(
component: FunctionComponent<{
application: WebApplication;
appState: AppState;
}>
) {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
return function () {
return {
controller: [
'$element',
'$scope',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
($element: JQLite, $scope: any) => {
return {
$onChanges() {
render(
h(component, {
application: $scope.application,
appState: $scope.appState,
}),
$element[0]
);
},
};
},
],
scope: {
application: '=',
appState: '=',
},
};
};
}

View File

@@ -0,0 +1,3 @@
import { SNWebCrypto } from "@standardnotes/sncrypto-web";
export const WebCrypto = new SNWebCrypto();

View File

@@ -135,6 +135,7 @@ export class Database {
const db = (await this.openDatabase())!;
const transaction = db.transaction(STORE_NAME, READ_WRITE);
return new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
transaction.oncomplete = () => { };
transaction.onerror = (event) => {
const target = event!.target! as any;

View File

@@ -22,7 +22,7 @@ export function clickOutside($document: ng.IDocumentService) {
$scope.$apply(attrs.clickOutside);
didApplyClickOutside = true;
}
};
}
$scope.$on('$destroy', () => {
attrs.clickOutside = undefined;

View File

@@ -16,23 +16,23 @@ export function delayHide($timeout: ng.ITimeoutService) {
scopeAny.hidePromise = null;
}
showElement(true);
}
};
const hideSpinner = () => {
scopeAny.hidePromise = $timeout(
showElement.bind(this as any, false),
getDelay()
);
}
};
const showElement = (show: boolean) => {
show ? elem.css({ display: '' }) : elem.css({ display: 'none' });
}
};
const getDelay = () => {
const delay = parseInt(scopeAny.delay);
return angular.isNumber(delay) ? delay : 200;
}
};
showElement(false);
// Whenever the scope variable updates we simply

View File

@@ -5,7 +5,7 @@ export function elemReady($parse: ng.IParseService) {
link: function($scope: ng.IScope, elem: JQLite, attrs: any) {
elem.ready(function() {
$scope.$apply(function() {
var func = $parse(attrs.elemReady);
const func = $parse(attrs.elemReady);
func($scope);
});
});

View File

@@ -19,7 +19,7 @@ export function infiniteScroll() {
};
elem.on('scroll', scopeAny.onScroll);
scope.$on('$destroy', () => {
elem.off('scroll', scopeAny.onScroll);;
elem.off('scroll', scopeAny.onScroll);
});
}
};

View File

@@ -1,7 +1,6 @@
import { WebDirective } from './../../types';
import { isDesktopApplication, preventRefreshing } from '@/utils';
import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils';
import template from '%/directives/account-menu.pug';
import { ProtectedAction, ContentType } from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
@@ -10,8 +9,6 @@ import {
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,
@@ -20,38 +17,47 @@ import {
StringImportError,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
STRING_UNSUPPORTED_BACKUP_FILE_VERSION
STRING_UNSUPPORTED_BACKUP_FILE_VERSION,
Strings,
} from '@/strings';
import { PasswordWizardType } from '@/types';
import { BackupFile } from '@standardnotes/snjs';
import {
ApplicationEvent,
BackupFile,
ContentType,
Platform,
} from '@standardnotes/snjs';
import { confirmDialog, alertDialog } from '@/services/alertService';
import { autorun, IReactionDisposer } from 'mobx';
import { storage, StorageKey } from '@/services/localStorage';
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
import {
disableErrorReporting,
enableErrorReporting,
errorReportingId,
} from '@/services/errorReporting';
const ELEMENT_NAME_AUTH_EMAIL = 'email';
const ELEMENT_NAME_AUTH_PASSWORD = 'password';
const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf';
type FormData = {
email: string
user_password: string
password_conf: string
confirmPassword: boolean
showLogin: boolean
showRegister: boolean
showPasscodeForm: boolean
strictSignin?: boolean
ephemeral: boolean
mergeLocal?: boolean
url: string
authenticating: boolean
status: string
passcode: string
confirmPasscode: string
changingPasscode: boolean
}
email: string;
user_password: string;
password_conf: string;
confirmPassword: boolean;
showLogin: boolean;
showRegister: boolean;
showPasscodeForm: boolean;
strictSignin?: boolean;
ephemeral: boolean;
mergeLocal?: boolean;
url: string;
authenticating: boolean;
status: string;
passcode: string;
confirmPasscode: string;
changingPasscode: boolean;
};
type AccountMenuState = {
formData: Partial<FormData>;
@@ -60,30 +66,30 @@ type AccountMenuState = {
user: any;
mutable: any;
importData: any;
encryptionStatusString: string;
server: string;
encryptionEnabled: boolean;
selectedAutoLockInterval: any;
encryptionStatusString?: string;
server?: string;
encryptionEnabled?: boolean;
selectedAutoLockInterval?: unknown;
showBetaWarning: boolean;
errorReportingEnabled: boolean;
syncInProgress: boolean;
syncError: string;
syncError?: string;
showSessions: boolean;
}
errorReportingId: string | null;
keyStorageInfo: string | null;
protectionsDisabledUntil: string | null;
};
class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
public appVersion: string
class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
public appVersion: string;
/** @template */
private closeFunction?: () => void
private removeBetaWarningListener?: IReactionDisposer
private removeSyncObserver?: IReactionDisposer
private closeFunction?: () => void;
private removeBetaWarningListener?: IReactionDisposer;
private removeSyncObserver?: IReactionDisposer;
private removeProtectionLengthObserver?: () => void;
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
appVersion: string,
) {
constructor($timeout: ng.ITimeoutService, appVersion: string) {
super($timeout);
this.appVersion = appVersion;
}
@@ -92,17 +98,25 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
getInitialState() {
return {
appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion),
passcodeAutoLockOptions: this.application!.getAutolockService().getAutoLockIntervalOptions(),
user: this.application!.getUser(),
passcodeAutoLockOptions: this.application
.getAutolockService()
.getAutoLockIntervalOptions(),
user: this.application.getUser(),
formData: {
mergeLocal: true,
ephemeral: false,
},
mutable: {},
showBetaWarning: false,
errorReportingEnabled: storage.get(StorageKey.DisableErrorReporting) === false,
errorReportingEnabled:
storage.get(StorageKey.DisableErrorReporting) === false,
showSessions: false,
} as AccountMenuState;
errorReportingId: errorReportingId(),
keyStorageInfo: Strings.keyStorageInfo(this.application),
importData: null,
syncInProgress: false,
protectionsDisabledUntil: this.getProtectionsDisabledUntil(),
};
}
getState() {
@@ -124,17 +138,17 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
refreshedCredentialState() {
return {
user: this.application!.getUser(),
canAddPasscode: !this.application!.isEphemeralSession(),
hasPasscode: this.application!.hasPasscode(),
showPasscodeForm: false
user: this.application.getUser(),
canAddPasscode: !this.application.isEphemeralSession(),
hasPasscode: this.application.hasPasscode(),
showPasscodeForm: false,
};
}
async $onInit() {
super.$onInit();
this.setState({
showSessions: this.appState.enableUnfinishedFeatures && await this.application.userCanManageSessions()
showSessions: await this.application.userCanManageSessions()
});
const sync = this.appState.sync;
@@ -143,17 +157,27 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
syncInProgress: sync.inProgress,
syncError: sync.errorMessage,
});
})
});
this.removeBetaWarningListener = autorun(() => {
this.setState({
showBetaWarning: this.appState.showBetaWarning
showBetaWarning: this.appState.showBetaWarning,
});
});
this.removeProtectionLengthObserver = this.application.addEventObserver(
async () => {
this.setState({
protectionsDisabledUntil: this.getProtectionsDisabledUntil(),
});
},
ApplicationEvent.ProtectionSessionExpiryDateChanged
);
}
deinit() {
this.removeSyncObserver?.();
this.removeBetaWarningListener?.();
this.removeProtectionLengthObserver?.();
super.deinit();
}
@@ -163,17 +187,50 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
});
}
hasProtections() {
return this.application.hasProtectionSources();
}
private getProtectionsDisabledUntil(): string | null {
const protectionExpiry = this.application.getProtectionSessionExpiryDate();
const now = new Date();
if (protectionExpiry > now) {
let f: Intl.DateTimeFormat;
if (isSameDay(protectionExpiry, now)) {
f = new Intl.DateTimeFormat(undefined, {
hour: 'numeric',
minute: 'numeric',
});
} else {
f = new Intl.DateTimeFormat(undefined, {
weekday: 'long',
day: 'numeric',
month: 'short',
hour: 'numeric',
minute: 'numeric',
});
}
return f.format(protectionExpiry);
}
return null;
}
async loadHost() {
const host = await this.application!.getHost();
const host = await this.application.getHost();
this.setState({
server: host,
formData: {
...this.getState().formData,
url: host
}
url: host,
},
});
}
enableProtections() {
this.application.clearProtectionSession();
}
onHostInputChange() {
const url = this.getState().formData.url!;
this.application!.setHost(url);
@@ -188,13 +245,13 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
encryptionStatusString: hasUser
? STRING_E2E_ENABLED
: hasPasscode
? STRING_LOCAL_ENC_ENABLED
: STRING_ENC_NOT_ENABLED,
? STRING_LOCAL_ENC_ENABLED
: STRING_ENC_NOT_ENABLED,
encryptionEnabled,
mutable: {
...this.getState().mutable,
backupEncrypted: encryptionEnabled
}
backupEncrypted: encryptionEnabled,
},
});
}
@@ -206,7 +263,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
const names = [
ELEMENT_NAME_AUTH_EMAIL,
ELEMENT_NAME_AUTH_PASSWORD,
ELEMENT_NAME_AUTH_PASSWORD_CONF
ELEMENT_NAME_AUTH_PASSWORD_CONF,
];
for (const name of names) {
const element = document.getElementsByName(name)[0];
@@ -217,7 +274,10 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
}
submitAuthForm() {
if (!this.getState().formData.email || !this.getState().formData.user_password) {
if (
!this.getState().formData.email ||
!this.getState().formData.user_password
) {
return;
}
this.blurAuthFields();
@@ -232,15 +292,15 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
return this.setState({
formData: {
...this.getState().formData,
...formData
}
...formData,
},
});
}
async login() {
await this.setFormDataState({
status: STRING_GENERATING_LOGIN_KEYS,
authenticating: true
authenticating: true,
});
const formData = this.getState().formData;
const response = await this.application!.signIn(
@@ -254,7 +314,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
if (!error) {
await this.setFormDataState({
authenticating: false,
user_password: undefined
user_password: undefined,
});
this.close();
return;
@@ -262,28 +322,26 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
await this.setFormDataState({
showLogin: true,
status: undefined,
user_password: undefined
user_password: undefined,
});
if (error.message) {
this.application!.alertService!.alert(error.message);
}
await this.setFormDataState({
authenticating: false
authenticating: false,
});
}
async register() {
const confirmation = this.getState().formData.password_conf;
if (confirmation !== this.getState().formData.user_password) {
this.application!.alertService!.alert(
STRING_NON_MATCHING_PASSWORDS
);
this.application!.alertService!.alert(STRING_NON_MATCHING_PASSWORDS);
return;
}
await this.setFormDataState({
confirmPassword: false,
status: STRING_GENERATING_REGISTER_KEYS,
authenticating: true
authenticating: true,
});
const response = await this.application!.register(
this.getState().formData.email!,
@@ -294,14 +352,12 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
const error = response.error;
if (error) {
await this.setFormDataState({
status: undefined
status: undefined,
});
await this.setFormDataState({
authenticating: false
authenticating: false,
});
this.application!.alertService!.alert(
error.message
);
this.application!.alertService!.alert(error.message);
} else {
await this.setFormDataState({ authenticating: false });
this.close();
@@ -313,8 +369,8 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
this.setFormDataState({
mergeLocal: !(await confirmDialog({
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
confirmButtonStyle: 'danger'
}))
confirmButtonStyle: 'danger',
})),
});
}
}
@@ -329,45 +385,20 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
this.appState.openSessionsModal();
}
async openPrivilegesModal() {
const run = () => {
this.application!.presentPrivilegesManagementModal();
this.close();
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePrivileges
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManagePrivileges,
() => {
run();
}
);
} else {
run();
}
}
async destroyLocalData() {
if (await confirmDialog({
text: STRING_SIGN_OUT_CONFIRMATION,
confirmButtonStyle: "danger"
})) {
if (
await confirmDialog({
text: STRING_SIGN_OUT_CONFIRMATION,
confirmButtonStyle: 'danger',
})
) {
this.application.signOut();
}
}
async submitImportPassword() {
await this.performImport(
this.getState().importData.data,
this.getState().importData.password
);
}
showRegister() {
this.setFormDataState({
showRegister: true
showRegister: true,
});
}
@@ -379,9 +410,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
const data = JSON.parse(e.target!.result as string);
resolve(data);
} catch (e) {
this.application!.alertService!.alert(
STRING_INVALID_IMPORT_FILE
);
this.application!.alertService!.alert(STRING_INVALID_IMPORT_FILE);
}
};
reader.readAsText(file);
@@ -392,128 +421,84 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
* @template
*/
async importFileSelected(files: File[]) {
const run = async () => {
const file = files[0];
const data = await this.readFile(file);
if (!data) {
return;
}
if (data.version || data.auth_params || data.keyParams) {
const version = data.version || data.keyParams?.version || data.auth_params?.version;
if (
!this.application!.protocolService!.supportedVersions().includes(version)
) {
await this.setState({ importData: null });
alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION });
return;
}
if (data.keyParams || data.auth_params) {
await this.setState({
importData: {
...this.getState().importData,
requestPassword: true,
data,
}
});
const element = document.getElementById(
ELEMENT_ID_IMPORT_PASSWORD_INPUT
);
if (element) {
element.scrollIntoView(false);
}
} else {
await this.performImport(data, undefined);
}
const file = files[0];
const data = await this.readFile(file);
if (!data) {
return;
}
if (data.version || data.auth_params || data.keyParams) {
const version =
data.version || data.keyParams?.version || data.auth_params?.version;
if (
this.application.protocolService.supportedVersions().includes(version)
) {
await this.performImport(data);
} else {
await this.performImport(data, undefined);
await this.setState({ importData: null });
void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION });
}
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManageBackups
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManageBackups,
run
);
} else {
run();
await this.performImport(data);
}
}
async performImport(data: BackupFile, password?: string) {
async performImport(data: BackupFile) {
await this.setState({
importData: {
...this.getState().importData,
loading: true
}
loading: true,
},
});
const result = await this.application!.importData(
data,
password
);
const result = await this.application.importData(data);
this.setState({
importData: null
importData: null,
});
if ('error' in result) {
this.application!.alertService!.alert(
result.error
);
if (!result) {
return;
} else if ('error' in result) {
void alertDialog({
text: result.error,
});
} else if (result.errorCount) {
const message = StringImportError(result.errorCount);
this.application!.alertService!.alert(
message
);
void alertDialog({
text: StringImportError(result.errorCount),
});
} else {
this.application!.alertService!.alert(
STRING_IMPORT_SUCCESS
);
void alertDialog({
text: STRING_IMPORT_SUCCESS,
});
}
}
async downloadDataArchive() {
this.application!.getArchiveService().downloadBackup(this.getState().mutable.backupEncrypted);
this.application
.getArchiveService()
.downloadBackup(this.getState().mutable.backupEncrypted);
}
notesAndTagsCount() {
return this.application!.getItems(
[
ContentType.Note,
ContentType.Tag
]
).length;
return this.application.getItems([ContentType.Note, ContentType.Tag])
.length;
}
encryptionStatusForNotes() {
const length = this.notesAndTagsCount();
return length + "/" + length + " notes and tags encrypted";
return length + '/' + length + ' notes and tags encrypted';
}
async reloadAutoLockInterval() {
const interval = await this.application!.getAutolockService().getAutoLockInterval();
this.setState({
selectedAutoLockInterval: interval
selectedAutoLockInterval: interval,
});
}
async selectAutoLockInterval(interval: number) {
const run = async () => {
await this.application!.getAutolockService().setAutoLockInterval(interval);
this.reloadAutoLockInterval();
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePasscode
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManagePasscode,
() => {
run();
}
);
} else {
run();
if (!(await this.application.authorizeAutolockIntervalChange())) {
return;
}
await this.application!.getAutolockService().setAutoLockInterval(interval);
this.reloadAutoLockInterval();
}
hidePasswordForm() {
@@ -521,7 +506,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
showLogin: false,
showRegister: false,
user_password: undefined,
password_conf: undefined
password_conf: undefined,
});
}
@@ -531,91 +516,62 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
addPasscodeClicked() {
this.setFormDataState({
showPasscodeForm: true
showPasscodeForm: true,
});
}
async submitPasscodeForm() {
const passcode = this.getState().formData.passcode!;
if (passcode !== this.getState().formData.confirmPasscode!) {
this.application!.alertService!.alert(
STRING_NON_MATCHING_PASSCODES
);
this.application!.alertService!.alert(STRING_NON_MATCHING_PASSCODES);
return;
}
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, async () => {
if (this.application!.hasPasscode()) {
await this.application!.changePasscode(passcode);
} else {
await this.application!.setPasscode(passcode);
await preventRefreshing(
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
async () => {
if (this.application!.hasPasscode()) {
await this.application!.changePasscode(passcode);
} else {
await this.application!.setPasscode(passcode);
}
}
});
);
this.setFormDataState({
passcode: undefined,
confirmPasscode: undefined,
showPasscodeForm: false
showPasscodeForm: false,
});
this.refreshEncryptionStatus();
}
async changePasscodePressed() {
const run = () => {
this.getState().formData.changingPasscode = true;
this.addPasscodeClicked();
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePasscode
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManagePasscode,
run
);
} else {
run();
}
this.getState().formData.changingPasscode = true;
this.addPasscodeClicked();
}
async removePasscodePressed() {
const run = async () => {
const signedIn = this.application!.hasAccount();
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
if (!signedIn) {
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
}
if (await confirmDialog({
text: message,
confirmButtonStyle: 'danger'
})) {
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => {
await this.application.getAutolockService().deleteAutolockPreference();
await this.application!.removePasscode();
await preventRefreshing(
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
async () => {
if (await this.application!.removePasscode()) {
await this.application
.getAutolockService()
.deleteAutolockPreference();
await this.reloadAutoLockInterval();
});
this.refreshEncryptionStatus();
this.refreshEncryptionStatus();
}
}
};
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePasscode
);
if (needsPrivilege) {
this.application!.presentPrivilegesModal(
ProtectedAction.ManagePasscode,
run
);
} else {
run();
}
}
openErrorReportingDialog() {
alertDialog({
title: 'Data sent during automatic error reporting',
text: `
We use <a target="_blank" href="https://www.bugsnag.com/">Bugsnag</a>
We use <a target="_blank" rel="noreferrer" href="https://www.bugsnag.com/">Bugsnag</a>
to automatically report errors that occur while the app is running. See
<a target="_blank" href="https://docs.bugsnag.com/platforms/javascript/#sending-diagnostic-data">
<a target="_blank" rel="noreferrer" href="https://docs.bugsnag.com/platforms/javascript/#sending-diagnostic-data">
this article, paragraph 'Browser' under 'Sending diagnostic data',
</a>
to see what data is included in error reports.
@@ -624,15 +580,15 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
anonymized. We use error reports to be alerted when something in our
code is causing unexpected errors and crashes in your application
experience.
`
`,
});
}
toggleErrorReportingEnabled() {
if (this.state.errorReportingEnabled) {
storage.set(StorageKey.DisableErrorReporting, true);
disableErrorReporting();
} else {
storage.set(StorageKey.DisableErrorReporting, false);
enableErrorReporting();
}
if (!this.state.syncInProgress) {
window.location.reload();
@@ -654,7 +610,7 @@ export class AccountMenu extends WebDirective {
this.bindToController = true;
this.scope = {
closeFunction: '&',
application: '='
application: '=',
};
}
}

View File

@@ -40,7 +40,7 @@ type ActionsMenuState = {
}[]
}
class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements ActionsMenuScope {
class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements ActionsMenuScope {
application!: WebApplication
item!: SNItem
private removeHiddenExtensionsListener?: IReactionDisposer;
@@ -63,7 +63,7 @@ class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements Acti
hiddenExtensions: this.appState.actionsMenu.hiddenExtensions
});
});
};
}
deinit() {
this.removeHiddenExtensionsListener?.();
@@ -74,7 +74,7 @@ class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements Acti
const extensions = this.application.actionsManager!.getExtensions().sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
let extensionsState: Record<UuidString, ExtensionState> = {};
const extensionsState: Record<UuidString, ExtensionState> = {};
extensions.map((extension) => {
extensionsState[extension.uuid] = {
loading: false,
@@ -114,7 +114,7 @@ class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements Acti
return {
...action,
subrows: this.subRowsForAction(action, extension)
}
};
} else {
return action;
}

View File

@@ -159,7 +159,7 @@ class ComponentViewCtrl implements ComponentViewScope {
this.$timeout(() => {
this.reloading = false;
});
})
});
}
private onVisibilityChange() {
@@ -228,6 +228,7 @@ class ComponentViewCtrl implements ComponentViewScope {
if (!iframe.contentWindow!.origin || iframe.contentWindow!.origin === 'null') {
desktopError = true;
}
// eslint-disable-next-line no-empty
} catch (e) { }
}
this.$timeout.cancel(this.loadTimeout);

View File

@@ -52,14 +52,14 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
editors: editors,
defaultEditor: defaultEditor
});
};
}
selectComponent(component: SNComponent) {
if (component) {
if (component.conflictOf) {
this.application.changeAndSaveItem(component.uuid, (mutator) => {
mutator.conflictOf = undefined;
})
});
}
}
this.$timeout(() => {
@@ -87,7 +87,7 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
this.application.changeItem(currentDefault.uuid, (m) => {
const mutator = m as ComponentMutator;
mutator.defaultEditor = false;
})
});
}
this.application.changeAndSaveItem(component.uuid, (m) => {
const mutator = m as ComponentMutator;

View File

@@ -16,7 +16,7 @@ interface HistoryScope {
item: SNItem
}
class HistoryMenuCtrl extends PureViewCtrl<{}, HistoryState> implements HistoryScope {
class HistoryMenuCtrl extends PureViewCtrl<unknown, HistoryState> implements HistoryScope {
diskEnabled = false
autoOptimize = false

View File

@@ -8,8 +8,6 @@ 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 { HistoryMenu } from './historyMenu';
export { SyncResolutionMenu } from './syncResolutionMenu';

View File

@@ -6,12 +6,12 @@ import { debounce } from '@/utils';
enum PanelSide {
Right = 'right',
Left = 'left'
};
}
enum MouseEventType {
Move = 'mousemove',
Down = 'mousedown',
Up = 'mouseup'
};
}
enum CssClass {
Hoverable = 'hoverable',
AlwaysVisible = 'always-visible',
@@ -19,7 +19,7 @@ enum CssClass {
NoSelection = 'no-selection',
Collapsed = 'collapsed',
AnimateOpacity = 'animate-opacity',
};
}
const WINDOW_EVENT_RESIZE = 'resize';
type ResizeFinishCallback = (

View File

@@ -7,7 +7,7 @@ const DEFAULT_CONTINUE_TITLE = "Continue";
enum Steps {
PasswordStep = 1,
FinishStep = 2
};
}
type FormData = {
currentPassword?: string,

View File

@@ -1,128 +0,0 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/ui_models/application';
import { ProtectedAction, PrivilegeCredential, PrivilegeSessionLength } from '@standardnotes/snjs';
import template from '%/directives/privileges-auth-modal.pug';
type PrivilegesAuthModalScope = {
application: WebApplication
action: ProtectedAction
onSuccess: () => void
onCancel: () => void
}
class PrivilegesAuthModalCtrl implements PrivilegesAuthModalScope {
$element: JQLite
$timeout: ng.ITimeoutService
application!: WebApplication
action!: ProtectedAction
onSuccess!: () => void
onCancel!: () => void
authParameters: Partial<Record<PrivilegeCredential, string>> = {}
sessionLengthOptions!: { value: PrivilegeSessionLength, label: string }[]
selectedSessionLength!: PrivilegeSessionLength
requiredCredentials!: PrivilegeCredential[]
failedCredentials!: PrivilegeCredential[]
/* @ngInject */
constructor(
$element: JQLite,
$timeout: ng.ITimeoutService
) {
this.$element = $element;
this.$timeout = $timeout;
}
$onInit() {
this.sessionLengthOptions = this.application!.privilegesService!
.getSessionLengthOptions();
this.application.privilegesService!.getSelectedSessionLength()
.then((length) => {
this.$timeout(() => {
this.selectedSessionLength = length;
});
});
this.application.privilegesService!.netCredentialsForAction(this.action)
.then((credentials) => {
this.$timeout(() => {
this.requiredCredentials = credentials.sort();
});
});
}
selectSessionLength(length: PrivilegeSessionLength) {
this.selectedSessionLength = length;
}
promptForCredential(credential: PrivilegeCredential) {
return this.application.privilegesService!.displayInfoForCredential(credential).prompt;
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
isCredentialInFailureState(credential: PrivilegeCredential) {
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.application.privilegesService!.authenticateAction(
this.action,
this.authParameters
);
this.$timeout(() => {
if (result.success) {
this.application.privilegesService!.setSessionLength(this.selectedSessionLength);
this.onSuccess();
this.dismiss();
} else {
this.failedCredentials = result.failedCredentials;
}
});
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class PrivilegesAuthModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesAuthModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
action: '=',
onSuccess: '=',
onCancel: '=',
application: '='
};
}
}

View File

@@ -1,118 +0,0 @@
import { WebDirective } from './../../types';
import { WebApplication } from '@/ui_models/application';
import template from '%/directives/privileges-management-modal.pug';
import { PrivilegeCredential, ProtectedAction, SNPrivileges, PrivilegeSessionLength } from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { PrivilegeMutator } from '@standardnotes/snjs';
type DisplayInfo = {
label: string
prompt: string
}
class PrivilegesManagementModalCtrl extends PureViewCtrl {
hasPasscode = false
hasAccount = false
$element: JQLite
application!: WebApplication
privileges!: SNPrivileges
availableActions!: ProtectedAction[]
availableCredentials!: PrivilegeCredential[]
sessionExpirey!: string
sessionExpired = true
credentialDisplayInfo: Partial<Record<PrivilegeCredential, DisplayInfo>> = {}
onCancel!: () => void
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
$element: JQLite
) {
super($timeout);
this.$element = $element;
}
async onAppLaunch() {
super.onAppLaunch();
this.hasPasscode = this.application.hasPasscode();
this.hasAccount = !this.application.noAccount();
this.reloadPrivileges();
}
displayInfoForCredential(credential: PrivilegeCredential) {
const info: any = this.application.privilegesService!.displayInfoForCredential(credential);
if (credential === PrivilegeCredential.LocalPasscode) {
info.availability = this.hasPasscode;
} else if (credential === PrivilegeCredential.AccountPassword) {
info.availability = this.hasAccount;
} else {
info.availability = true;
}
return info;
}
displayInfoForAction(action: ProtectedAction) {
return this.application.privilegesService!.displayInfoForAction(action).label;
}
isCredentialRequiredForAction(action: ProtectedAction, credential: PrivilegeCredential) {
if (!this.privileges) {
return false;
}
return this.privileges.isCredentialRequiredForAction(action, credential);
}
async clearSession() {
await this.application.privilegesService!.clearSession();
this.reloadPrivileges();
}
async reloadPrivileges() {
this.availableActions = this.application.privilegesService!.getAvailableActions();
this.availableCredentials = this.application.privilegesService!.getAvailableCredentials();
const sessionEndDate = await this.application.privilegesService!.getSessionExpirey();
this.sessionExpirey = sessionEndDate.toLocaleString();
this.sessionExpired = new Date() >= sessionEndDate;
for (const cred of this.availableCredentials) {
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
}
const privs = await this.application.privilegesService!.getPrivileges();
this.$timeout(() => {
this.privileges = privs;
});
}
checkboxValueChanged(action: ProtectedAction, credential: PrivilegeCredential) {
this.application.changeAndSaveItem(this.privileges.uuid, (m) => {
const mutator = m as PrivilegeMutator;
mutator.toggleCredentialForAction(action, credential);
})
}
cancel() {
this.dismiss();
this.onCancel && this.onCancel();
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class PrivilegesManagementModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = PrivilegesManagementModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
application: '='
};
}
}

View File

@@ -15,5 +15,4 @@ import '../../../vendor/assets/javascripts/zip/zip';
import '../../../vendor/assets/javascripts/zip/z-worker';
// entry point
// eslint-disable-next-line import/first
import './app';

View File

@@ -1,5 +1,11 @@
import { WebApplication } from '@/ui_models/application';
import { EncryptionIntent, ProtectedAction, ContentType, SNNote, BackupFile, PayloadContent } from '@standardnotes/snjs';
import {
EncryptionIntent,
ContentType,
SNNote,
BackupFile,
PayloadContent,
} from '@standardnotes/snjs';
function zippableTxtName(name: string, suffix = ""): string {
const sanitizedName = name
@@ -22,44 +28,29 @@ export class ArchiveManager {
}
public async downloadBackup(encrypted: boolean) {
const run = async () => {
const intent = encrypted
? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted;
const intent = encrypted
? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted;
const data = await this.application.createBackupFile(intent);
if (!data) {
return;
}
const blobData = new Blob(
[JSON.stringify(data, null, 2)],
{ type: 'text/json' }
const data = await this.application.createBackupFile(intent, true);
if (!data) {
return;
}
const blobData = new Blob(
[JSON.stringify(data, null, 2)],
{ type: 'text/json' }
);
if (encrypted) {
this.downloadData(
blobData,
`Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt`
);
if (encrypted) {
this.downloadData(
blobData,
`Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt`
);
} else {
/** Remove auth/keyParams as they won't be needed to decrypt the file */
delete data.auth_params;
delete data.keyParams;
/** download as zipped plain text files */
this.downloadZippedDecryptedItems(data);
}
};
if (
await this.application.privilegesService!
.actionRequiresPrivilege(ProtectedAction.ManageBackups)
) {
this.application.presentPrivilegesModal(
ProtectedAction.ManageBackups,
() => {
run();
});
} else {
run();
/** Remove auth/keyParams as they won't be needed to decrypt the file */
delete data.auth_params;
delete data.keyParams;
/** download as zipped plain text files */
this.downloadZippedDecryptedItems(data);
}
}

View File

@@ -24,9 +24,10 @@ export class BrowserBridge implements Bridge {
/** No-ops */
syncComponents() {}
onMajorDataChange() {}
onInitialDataLoad() {}
onSearch() {}
downloadBackup() {}
/* eslint-disable @typescript-eslint/no-empty-function */
syncComponents(): void {}
onMajorDataChange(): void {}
onInitialDataLoad(): void {}
onSearch(): void {}
downloadBackup(): void {}
}

View File

@@ -1,9 +1,17 @@
import { SNComponent, PurePayload, ComponentMutator, AppDataField, ContentType } from '@standardnotes/snjs';
import {
SNComponent,
PurePayload,
ComponentMutator,
AppDataField,
EncryptionIntent,
ApplicationService,
ApplicationEvent,
removeFromArray,
} from '@standardnotes/snjs';
/* eslint-disable camelcase */
import { WebApplication } from '@/ui_models/application';
// An interface used by the Desktop app to interact with SN
import { isDesktopApplication } from '@/utils';
import { EncryptionIntent, ApplicationService, ApplicationEvent, removeFromArray } from '@standardnotes/snjs';
import { Bridge } from './bridge';
type UpdateObserverCallback = (component: SNComponent) => void
@@ -67,7 +75,7 @@ export class DesktopManager extends ApplicationService {
getExtServerHost() {
console.assert(
this.bridge.extensionsServerHost,
!!this.bridge.extensionsServerHost,
'extServerHost is null'
);
return this.bridge.extensionsServerHost;

View File

@@ -2,6 +2,7 @@ import { isNullOrUndefined, SNLog } from '@standardnotes/snjs';
import { isDesktopApplication, isDev } from '@/utils';
import { storage, StorageKey } from './localStorage';
import Bugsnag from '@bugsnag/js';
import { WebCrypto } from '../crypto';
declare const __VERSION__: string;
declare global {
@@ -21,7 +22,7 @@ function redactFilePath(line: string): string {
}
}
export function startErrorReporting() {
export function startErrorReporting(): void {
const disableErrorReporting = storage.get(StorageKey.DisableErrorReporting);
if (
/**
@@ -37,6 +38,15 @@ export function startErrorReporting() {
return;
}
try {
const storedUserId = storage.get(StorageKey.AnonymousUserId);
let anonymousUserId: string;
if (storedUserId === null) {
anonymousUserId = WebCrypto.generateUUIDSync();
storage.set(StorageKey.AnonymousUserId, anonymousUserId);
} else {
anonymousUserId = storedUserId;
}
Bugsnag.start({
apiKey: window._bugsnag_api_key,
appType: isDesktopApplication() ? 'desktop' : 'web',
@@ -46,6 +56,8 @@ export function startErrorReporting() {
releaseStage: isDev ? 'development' : undefined,
enabledBreadcrumbTypes: ['error', 'log'],
onError(event) {
event.setUser(anonymousUserId);
/**
* Redact any data that could be used to identify user,
* such as file paths.
@@ -95,3 +107,16 @@ export function startErrorReporting() {
SNLog.onError = console.error;
}
}
export function disableErrorReporting() {
storage.remove(StorageKey.AnonymousUserId);
storage.set(StorageKey.DisableErrorReporting, true);
}
export function enableErrorReporting() {
storage.set(StorageKey.DisableErrorReporting, false);
}
export function errorReportingId() {
return storage.get(StorageKey.AnonymousUserId);
}

View File

@@ -4,7 +4,7 @@ export enum KeyboardKey {
Backspace = "Backspace",
Up = "ArrowUp",
Down = "ArrowDown",
};
}
export enum KeyboardModifier {
Shift = "Shift",
@@ -12,12 +12,12 @@ export enum KeyboardModifier {
/** ⌘ key on Mac, ⊞ key on Windows */
Meta = "Meta",
Alt = "Alt",
};
}
enum KeyboardKeyEvent {
Down = "KeyEventDown",
Up = "KeyEventUp"
};
}
type KeyboardObserver = {
key?: KeyboardKey | string
@@ -39,10 +39,10 @@ export class KeyboardManager {
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);
}

View File

@@ -1,9 +1,15 @@
export enum StorageKey {
DisableErrorReporting = 'DisableErrorReporting',
AnonymousUserId = 'AnonymousUserId',
ShowBetaWarning = 'ShowBetaWarning',
ShowNoAccountWarning = 'ShowNoAccountWarning',
}
export type StorageValue = {
[StorageKey.DisableErrorReporting]: boolean;
[StorageKey.AnonymousUserId]: string;
[StorageKey.ShowBetaWarning]: boolean;
[StorageKey.ShowNoAccountWarning]: boolean;
}
export const storage = {
@@ -11,10 +17,10 @@ export const storage = {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
},
set<K extends StorageKey>(key: K, value: StorageValue[K]) {
set<K extends StorageKey>(key: K, value: StorageValue[K]): void {
localStorage.setItem(key, JSON.stringify(value));
},
remove(key: StorageKey) {
remove(key: StorageKey): void {
localStorage.removeItem(key);
},
};

View File

@@ -9,9 +9,9 @@ import {
ComponentMutator,
Copy,
dictToArray
} from '@standardnotes/snjs';
import { PayloadContent } from '@standardnotes/snjs';
import { ComponentPermission } from '@standardnotes/snjs';
, PayloadContent , ComponentPermission } from '@standardnotes/snjs';
/** A class for handling installation of system extensions */
export class NativeExtManager extends ApplicationService {
@@ -82,7 +82,7 @@ export class NativeExtManager extends ApplicationService {
// Handle addition of SN|ExtensionRepo permission
const permissions = Copy(extensionsManager!.permissions) as ComponentPermission[];
const permission = permissions.find((p) => {
return p.name === ComponentAction.StreamItems
return p.name === ComponentAction.StreamItems;
});
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
permission.content_types!.push(ContentType.ExtensionRepo);
@@ -160,7 +160,7 @@ export class NativeExtManager extends ApplicationService {
// Handle addition of SN|ExtensionRepo permission
const permissions = Copy(batchManager!.permissions) as ComponentPermission[];
const permission = permissions.find((p) => {
return p.name === ComponentAction.StreamItems
return p.name === ComponentAction.StreamItems;
});
if (permission && !permission.content_types!.includes(ContentType.ExtensionRepo)) {
permission.content_types!.push(ContentType.ExtensionRepo);

View File

@@ -22,8 +22,6 @@ export class ThemeManager extends ApplicationService {
this.deactivateAllThemes();
} else if (event === ApplicationEvent.StorageReady) {
await this.activateCachedThemes();
if (!this.webApplication.getDesktopService().isDesktop) {
}
}
}
@@ -75,7 +73,7 @@ export class ThemeManager extends ApplicationService {
this.deactivateTheme(theme.uuid);
}
}
})
});
}
private clearAppThemeState() {

View File

@@ -1,27 +1,45 @@
import { Platform, SNApplication } from '@standardnotes/snjs';
import { getPlatform, isDesktopApplication } from './utils';
/** @generic */
export const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.";
export const STRING_DEFAULT_FILE_ERROR = "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe.";
export const STRING_GENERIC_SYNC_ERROR = "There was an error syncing. Please try again. If all else fails, try signing out and signing back in.";
export const STRING_SESSION_EXPIRED =
'Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.';
export const STRING_DEFAULT_FILE_ERROR =
'Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe.';
export const STRING_GENERIC_SYNC_ERROR =
'There was an error syncing. Please try again. If all else fails, try signing out and signing back in.';
export function StringSyncException(data: any) {
return `There was an error while trying to save your items. Please contact support and share this message: ${JSON.stringify(data)}.`;
return `There was an error while trying to save your items. Please contact support and share this message: ${JSON.stringify(
data
)}.`;
}
/** @footer */
export const STRING_NEW_UPDATE_READY = "A new update is ready to install. Please use the top-level 'Updates' menu to manage installation.";
export const STRING_NEW_UPDATE_READY =
"A new update is ready to install. Please use the top-level 'Updates' menu to manage installation.";
/** @tags */
export const STRING_DELETE_TAG = "Are you sure you want to delete this tag? Note: deleting a tag will not delete its notes.";
export const STRING_DELETE_TAG =
'Are you sure you want to delete this tag? Note: deleting a tag will not delete its notes.';
/** @editor */
export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN = 'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.'
export const STRING_DELETED_NOTE = "The note you are attempting to edit has been deleted, and is awaiting sync. Changes you make will be disregarded.";
export const STRING_INVALID_NOTE = "The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.";
export const STRING_ELLIPSES = "...";
export const STRING_GENERIC_SAVE_ERROR = "There was an error saving your note. Please try again.";
export const STRING_DELETE_PLACEHOLDER_ATTEMPT = "This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note.";
export const STRING_ARCHIVE_LOCKED_ATTEMPT = "This note is locked. If you'd like to archive it, unlock it, and try again.";
export const STRING_UNARCHIVE_LOCKED_ATTEMPT = "This note is locked. If you'd like to archive it, unlock it, and try again.";
export const STRING_DELETE_LOCKED_ATTEMPT = "This note is locked. If you'd like to delete it, unlock it, and try again.";
export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN =
'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.';
export const STRING_DELETED_NOTE =
'The note you are attempting to edit has been deleted, and is awaiting sync. Changes you make will be disregarded.';
export const STRING_INVALID_NOTE =
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.";
export const STRING_ELLIPSES = '...';
export const STRING_GENERIC_SAVE_ERROR =
'There was an error saving your note. Please try again.';
export const STRING_DELETE_PLACEHOLDER_ATTEMPT =
'This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note.';
export const STRING_ARCHIVE_LOCKED_ATTEMPT =
"This note is locked. If you'd like to archive it, unlock it, and try again.";
export const STRING_UNARCHIVE_LOCKED_ATTEMPT =
"This note is locked. If you'd like to archive it, unlock it, and try again.";
export const STRING_DELETE_LOCKED_ATTEMPT =
"This note is locked. If you'd like to delete it, unlock it, and try again.";
export function StringDeleteNote(title: string, permanently: boolean) {
return permanently
? `Are you sure you want to permanently delete ${title}?`
@@ -32,44 +50,80 @@ export function StringEmptyTrash(count: number) {
}
/** @account */
export const STRING_ACCOUNT_MENU_UNCHECK_MERGE = "Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?";
export const STRING_SIGN_OUT_CONFIRMATION = "Are you sure you want to end your session? This will delete all local items and extensions.";
export const STRING_ERROR_DECRYPTING_IMPORT = "There was an error decrypting your items. Make sure the password you entered is correct and try again.";
export const STRING_E2E_ENABLED = "End-to-end encryption is enabled. Your data is encrypted on your device first, then synced to your private cloud.";
export const STRING_LOCAL_ENC_ENABLED = "Encryption is enabled. Your data is encrypted using your passcode before it is saved to your device storage.";
export const STRING_ENC_NOT_ENABLED = "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.";
export const STRING_IMPORT_SUCCESS = "Your data has been successfully imported.";
export const STRING_REMOVE_PASSCODE_CONFIRMATION = "Are you sure you want to remove your application passcode?";
export const STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM = " This will remove encryption from your local data.";
export const STRING_NON_MATCHING_PASSCODES = "The two passcodes you entered do not match. Please try again.";
export const STRING_NON_MATCHING_PASSWORDS = "The two passwords you entered do not match. Please try again.";
export const STRING_GENERATING_LOGIN_KEYS = "Generating Login Keys...";
export const STRING_GENERATING_REGISTER_KEYS = "Generating Account Keys...";
export const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a proper JSON file and try again.";
export const STRING_ACCOUNT_MENU_UNCHECK_MERGE =
'Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?';
export const STRING_SIGN_OUT_CONFIRMATION =
'Are you sure you want to end your session? This will delete all local items and extensions.';
export const STRING_ERROR_DECRYPTING_IMPORT =
'There was an error decrypting your items. Make sure the password you entered is correct and try again.';
export const STRING_E2E_ENABLED =
'End-to-end encryption is enabled. Your data is encrypted on your device first, then synced to your private cloud.';
export const STRING_LOCAL_ENC_ENABLED =
'Encryption is enabled. Your data is encrypted using your passcode before it is saved to your device storage.';
export const STRING_ENC_NOT_ENABLED =
'Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.';
export const STRING_IMPORT_SUCCESS =
'Your data has been successfully imported.';
export const STRING_REMOVE_PASSCODE_CONFIRMATION =
'Are you sure you want to remove your application passcode?';
export const STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM =
' This will remove encryption from your local data.';
export const STRING_NON_MATCHING_PASSCODES =
'The two passcodes you entered do not match. Please try again.';
export const STRING_NON_MATCHING_PASSWORDS =
'The two passwords you entered do not match. Please try again.';
export const STRING_GENERATING_LOGIN_KEYS = 'Generating Login Keys...';
export const STRING_GENERATING_REGISTER_KEYS = 'Generating Account Keys...';
export const STRING_INVALID_IMPORT_FILE =
'Unable to open file. Ensure it is a proper JSON file and try again.';
export function StringImportError(errorCount: number) {
return `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`;
}
export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = 'This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.';
export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION =
'This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.';
/** @password_change */
export const STRING_FAILED_PASSWORD_CHANGE = "There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.";
export const STRING_FAILED_PASSWORD_CHANGE =
'There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.';
export const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE =
"The encryption upgrade is in progress. You may lose data if you quit the app. " +
"Are you sure you want to quit?"
'The encryption upgrade is in progress. You may lose data if you quit the app. ' +
'Are you sure you want to quit?';
export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE =
"A passcode change is in progress. You may lose data if you quit the app. " +
"Are you sure you want to quit?"
'A passcode change is in progress. You may lose data if you quit the app. ' +
'Are you sure you want to quit?';
export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL =
"A passcode removal is in progress. You may lose data if you quit the app. " +
"Are you sure you want to quit?"
'A passcode removal is in progress. You may lose data if you quit the app. ' +
'Are you sure you want to quit?';
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = 'Encryption upgrade available';
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE =
'Encryption upgrade available';
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT =
'Encryption version 004 is available. ' +
'This version strengthens the encryption algorithms your account and ' +
'local storage use. To learn more about this upgrade, visit our ' +
'<a href="https://standardnotes.org/help/security" target="_blank">Security Upgrade page.</a>';
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade';
export const Strings = {
keyStorageInfo(application: SNApplication): string | null {
if (!isDesktopApplication()) {
return null;
}
if (!application.hasAccount()) {
return null;
}
const platform = getPlatform();
const keychainName =
platform === Platform.WindowsDesktop
? 'credential manager'
: platform === Platform.MacDesktop
? 'keychain'
: 'password manager';
return `Your keys are currently stored in your operating system's ${keychainName}. Adding a passcode prevents even your operating system from reading them.`;
},
protectingNoteWithoutProtectionSources: 'Access to this note will not be restricted until you set up a passcode or account.',
openAccountMenu: 'Open Account Menu'
};

View File

@@ -6,7 +6,7 @@
"allowJs": true,
"noEmit": true,
"strict": true,
"isolatedModules": true,
"isolatedModules": false,
"esModuleInterop": true,
"declaration": true,
"newLine": "lf",
@@ -14,6 +14,7 @@
"baseUrl": ".",
"jsx": "react-jsx",
"jsxImportSource": "preact",
"typeRoots": ["./@types"],
"paths": {
"%/*": ["../templates/*"],
"@/*": ["./*"],

View File

@@ -1,5 +1,5 @@
declare module "*.pug" {
import { compileTemplate } from 'pug'
import { compileTemplate } from 'pug';
const content: compileTemplate;
export default content;
}

View File

@@ -1,22 +1,22 @@
import { isDesktopApplication, isDev } from '@/utils';
import pull from 'lodash/pull';
import {
ProtectedAction,
ApplicationEvent,
SNTag,
SNNote,
SNUserPrefs,
ContentType,
SNSmartTag,
PayloadSource,
DeinitSource,
UuidString,
SyncOpStatus,
PrefKey,
SNApplication,
} from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application';
import { Editor } from '@/ui_models/editor';
import { action, makeObservable, observable } from 'mobx';
import { action, makeObservable, observable, runInAction } from 'mobx';
import { Bridge } from '@/services/bridge';
import { storage, StorageKey } from '@/services/localStorage';
export enum AppStateEvent {
TagChanged,
@@ -29,6 +29,11 @@ export enum AppStateEvent {
WindowDidBlur,
}
export type PanelResizedData = {
panel: string;
collapsed: boolean;
};
export enum EventSource {
UserInteraction,
Script,
@@ -36,8 +41,6 @@ export enum EventSource {
type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void>;
const SHOW_BETA_WARNING_KEY = 'show_beta_warning';
class ActionsMenuState {
hiddenExtensions: Record<UuidString, boolean> = {};
@@ -45,7 +48,7 @@ class ActionsMenuState {
makeObservable(this, {
hiddenExtensions: observable,
toggleExtensionVisibility: action,
deinit: action,
reset: action,
});
}
@@ -53,7 +56,7 @@ class ActionsMenuState {
this.hiddenExtensions[uuid] = !this.hiddenExtensions[uuid];
}
deinit() {
reset() {
this.hiddenExtensions = {};
}
}
@@ -72,7 +75,7 @@ export class SyncState {
});
}
update(status: SyncOpStatus) {
update(status: SyncOpStatus): void {
this.errorMessage = status.error?.message;
this.inProgress = status.syncInProgress;
const stats = status.getStats();
@@ -92,6 +95,59 @@ export class SyncState {
}
}
class AccountMenuState {
show = false;
constructor() {
makeObservable(this, {
show: observable,
setShow: action,
toggleShow: action,
});
}
setShow(show: boolean) {
this.show = show;
}
toggleShow() {
this.show = !this.show;
}
}
class NoAccountWarningState {
show: boolean;
constructor(application: SNApplication, appObservers: (() => void)[]) {
this.show = application.hasAccount()
? false
: storage.get(StorageKey.ShowNoAccountWarning) ?? true;
appObservers.push(
application.addEventObserver(async () => {
runInAction(() => {
this.show = false;
});
}, ApplicationEvent.SignedIn),
application.addEventObserver(async () => {
if (application.hasAccount()) {
runInAction(() => {
this.show = false;
});
}
}, ApplicationEvent.Started)
);
makeObservable(this, {
show: observable,
hide: action,
});
}
hide() {
this.show = false;
storage.set(StorageKey.ShowNoAccountWarning, false);
}
reset() {
storage.remove(StorageKey.ShowNoAccountWarning);
}
}
export class AppState {
readonly enableUnfinishedFeatures =
isDev || location.host.includes('app-dev.standardnotes.org');
@@ -106,12 +162,15 @@ export class AppState {
rootScopeCleanup2: any;
onVisibilityChange: any;
selectedTag?: SNTag;
multiEditorEnabled = false;
showBetaWarning = false;
showBetaWarning: boolean;
readonly accountMenu = new AccountMenuState();
readonly actionsMenu = new ActionsMenuState();
readonly noAccountWarning: NoAccountWarningState;
readonly sync = new SyncState();
isSessionsModalVisible = false;
private appEventObserverRemovers: (() => void)[] = [];
/* @ngInject */
constructor(
$rootScope: ng.IRootScopeService,
@@ -122,15 +181,10 @@ export class AppState {
this.$timeout = $timeout;
this.$rootScope = $rootScope;
this.application = application;
makeObservable(this, {
showBetaWarning: observable,
isSessionsModalVisible: observable,
enableBetaWarning: action,
disableBetaWarning: action,
openSessionsModal: action,
closeSessionsModal: action,
});
this.noAccountWarning = new NoAccountWarningState(
application,
this.appEventObserverRemovers
);
this.addAppEventObserver();
this.streamNotesAndTags();
this.onVisibilityChange = () => {
@@ -141,17 +195,35 @@ export class AppState {
this.notifyEvent(event);
};
this.registerVisibilityObservers();
this.determineBetaWarningValue();
if (this.bridge.appVersion.includes('-beta')) {
this.showBetaWarning = storage.get(StorageKey.ShowBetaWarning) ?? true;
} else {
this.showBetaWarning = false;
}
makeObservable(this, {
showBetaWarning: observable,
isSessionsModalVisible: observable,
enableBetaWarning: action,
disableBetaWarning: action,
openSessionsModal: action,
closeSessionsModal: action,
});
}
deinit(source: DeinitSource) {
deinit(source: DeinitSource): void {
if (source === DeinitSource.SignOut) {
localStorage.removeItem(SHOW_BETA_WARNING_KEY);
storage.remove(StorageKey.ShowBetaWarning);
this.noAccountWarning.reset();
}
this.actionsMenu.deinit();
this.actionsMenu.reset();
this.unsubApp();
this.unsubApp = undefined;
this.observers.length = 0;
this.appEventObserverRemovers.forEach((remover) => remover());
this.appEventObserverRemovers.length = 0;
if (this.rootScopeCleanup1) {
this.rootScopeCleanup1();
this.rootScopeCleanup2();
@@ -172,30 +244,12 @@ export class AppState {
disableBetaWarning() {
this.showBetaWarning = false;
localStorage.setItem(SHOW_BETA_WARNING_KEY, 'false');
storage.set(StorageKey.ShowBetaWarning, false);
}
enableBetaWarning() {
this.showBetaWarning = true;
localStorage.setItem(SHOW_BETA_WARNING_KEY, 'true');
}
clearBetaWarning() {
localStorage.setItem(SHOW_BETA_WARNING_KEY, 'true');
}
private determineBetaWarningValue() {
if (this.bridge.appVersion.includes('-beta')) {
switch (localStorage.getItem(SHOW_BETA_WARNING_KEY)) {
case 'true':
default:
this.enableBetaWarning();
break;
case 'false':
this.disableBetaWarning();
break;
}
}
storage.set(StorageKey.ShowBetaWarning, true);
}
/**
@@ -210,7 +264,7 @@ export class AppState {
: this.selectedTag.uuid
: undefined;
if (!activeEditor || this.multiEditorEnabled) {
if (!activeEditor) {
this.application.editorGroup.createEditor(
undefined,
title,
@@ -221,35 +275,25 @@ export class AppState {
}
}
async openEditor(noteUuid: string) {
async openEditor(noteUuid: string): Promise<void> {
if (this.getActiveEditor()?.note?.uuid === noteUuid) {
return;
}
const note = this.application.findItem(noteUuid) as SNNote;
if (this.getActiveEditor()?.note?.uuid === noteUuid) return;
const run = async () => {
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.multiEditorEnabled) {
if (!activeEditor) {
this.application.editorGroup.createEditor(noteUuid);
} else {
activeEditor.setNote(note);
}
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
};
if (
note &&
note.safeContent.protected &&
(await this.application.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ViewProtectedNotes
))
) {
return new Promise((resolve) => {
this.application.presentPrivilegesModal(
ProtectedAction.ViewProtectedNotes,
() => {
run().then(resolve);
}
);
});
} else {
return run();
}
}
@@ -299,7 +343,11 @@ export class AppState {
this.closeEditor(editor);
} else if (note.trashed && !this.selectedTag?.isTrashTag) {
this.closeEditor(editor);
} else if (note.archived && !this.selectedTag?.isArchiveTag) {
} else if (
note.archived &&
!this.selectedTag?.isArchiveTag &&
!this.application.getPreference(PrefKey.NotesShowArchived, false)
) {
this.closeEditor(editor);
}
}
@@ -400,10 +448,11 @@ export class AppState {
}
panelDidResize(name: string, collapsed: boolean) {
this.notifyEvent(AppStateEvent.PanelResized, {
const data: PanelResizedData = {
panel: name,
collapsed: collapsed,
});
};
this.notifyEvent(AppStateEvent.PanelResized, data);
}
editorDidFocus(eventSource: EventSource) {

View File

@@ -1,4 +1,3 @@
import { PermissionDialog } from '@standardnotes/snjs';
import { ComponentModalScope } from './../directives/views/componentModal';
import { AccountSwitcherScope, PermissionsModalScope } from './../types';
import { ComponentGroup } from './component_group';
@@ -8,11 +7,12 @@ import { PasswordWizardType, PasswordWizardScope } from '@/types';
import {
SNApplication,
platformFromString,
Challenge,
ProtectedAction, SNComponent
SNComponent,
PermissionDialog,
DeinitSource,
} from '@standardnotes/snjs';
import angular from 'angular';
import { getPlatformString } from '@/utils';
import { getPlatform, getPlatformString } from '@/utils';
import { AlertService } from '@/services/alertService';
import { WebDeviceInterface } from '@/web_device_interface';
import {
@@ -25,26 +25,25 @@ import {
KeyboardManager
} from '@/services';
import { AppState } from '@/ui_models/app_state';
import { SNWebCrypto } from '@standardnotes/sncrypto-web';
import { Bridge } from '@/services/bridge';
import { DeinitSource } from '@standardnotes/snjs';
import { WebCrypto } from '@/crypto';
type WebServices = {
appState: AppState
desktopService: DesktopManager
autolockService: AutolockService
archiveService: ArchiveManager
nativeExtService: NativeExtManager
statusManager: StatusManager
themeService: ThemeManager
keyboardService: KeyboardManager
appState: AppState;
desktopService: DesktopManager;
autolockService: AutolockService;
archiveService: ArchiveManager;
nativeExtService: NativeExtManager;
statusManager: StatusManager;
themeService: ThemeManager;
keyboardService: KeyboardManager;
}
export class WebApplication extends SNApplication {
private scope?: ng.IScope
private scope?: angular.IScope
private webServices!: WebServices
private currentAuthenticationElement?: JQLite
private currentAuthenticationElement?: angular.IRootElementService
public editorGroup: EditorGroup
public componentGroup: ComponentGroup
@@ -52,16 +51,16 @@ export class WebApplication extends SNApplication {
constructor(
deviceInterface: WebDeviceInterface,
identifier: string,
private $compile: ng.ICompileService,
scope: ng.IScope,
private $compile: angular.ICompileService,
scope: angular.IScope,
defaultSyncServerHost: string,
private bridge: Bridge,
) {
super(
bridge.environment,
platformFromString(getPlatformString()),
getPlatform(),
deviceInterface,
new SNWebCrypto(),
WebCrypto,
new AlertService(),
identifier,
undefined,
@@ -78,7 +77,7 @@ export class WebApplication extends SNApplication {
}
/** @override */
deinit(source: DeinitSource) {
deinit(source: DeinitSource): void {
for (const service of Object.values(this.webServices)) {
if ('deinit' in service) {
service.deinit?.(source);
@@ -98,24 +97,24 @@ export class WebApplication extends SNApplication {
* to complete before destroying the global application instance and all its services */
setTimeout(() => {
super.deinit(source);
}, 0)
}, 0);
}
onStart() {
onStart(): void {
super.onStart();
this.componentManager!.openModalComponent = this.openModalComponent;
this.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog;
}
setWebServices(services: WebServices) {
setWebServices(services: WebServices): void {
this.webServices = services;
}
public getAppState() {
public getAppState(): AppState {
return this.webServices.appState;
}
public getDesktopService() {
public getDesktopService(): DesktopManager {
return this.webServices.desktopService;
}
@@ -158,58 +157,6 @@ export class WebApplication extends SNApplication {
this.applicationElement.append(el);
}
promptForChallenge(challenge: Challenge) {
const scope: any = this.scope!.$new(true);
scope.challenge = challenge;
scope.application = this;
const el = this.$compile!(
"<challenge-modal " +
"class='sk-modal' application='application' challenge='challenge'>" +
"</challenge-modal>"
)(scope);
this.applicationElement.append(el);
}
async presentPrivilegesModal(
action: ProtectedAction,
onSuccess?: any,
onCancel?: any
) {
if (this.authenticationInProgress()) {
onCancel && onCancel();
return;
}
const customSuccess = async () => {
onSuccess && await onSuccess();
this.currentAuthenticationElement = undefined;
};
const customCancel = async () => {
onCancel && await onCancel();
this.currentAuthenticationElement = undefined;
};
const scope: any = this.scope!.$new(true);
scope.action = action;
scope.onSuccess = customSuccess;
scope.onCancel = customCancel;
scope.application = this;
const el = this.$compile!(`
<privileges-auth-modal application='application' action='action' on-success='onSuccess'
on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>
`)(scope);
this.applicationElement.append(el);
this.currentAuthenticationElement = el;
}
presentPrivilegesManagementModal() {
const scope: any = this.scope!.$new(true);
scope.application = this;
const el = this.$compile!("<privileges-management-modal application='application' class='sk-modal'></privileges-management-modal>")(scope);
this.applicationElement.append(el);
}
authenticationInProgress() {
return this.currentAuthenticationElement != null;
}
@@ -254,7 +201,12 @@ export class WebApplication extends SNApplication {
this.applicationElement.append(el);
}
openModalComponent(component: SNComponent) {
async openModalComponent(component: SNComponent): Promise<void> {
if (component.package_info?.identifier === "org.standardnotes.batch-manager") {
if (!await this.authorizeBatchManagerAccess()) {
return;
}
}
const scope = this.scope!.$new(true) as Partial<ComponentModalScope>;
scope.componentUuid = component.uuid;
scope.application = this;

View File

@@ -1,13 +1,13 @@
import { SNComponent, ComponentArea, removeFromArray, addIfUnique } from '@standardnotes/snjs';
import { SNComponent, ComponentArea, removeFromArray, addIfUnique , UuidString } from '@standardnotes/snjs';
import { WebApplication } from './application';
import { UuidString } from '@standardnotes/snjs';
/** Areas that only allow a single component to be active */
const SingleComponentAreas = [
ComponentArea.Editor,
ComponentArea.NoteTags,
ComponentArea.TagsList
]
];
export class ComponentGroup {
@@ -20,7 +20,7 @@ export class ComponentGroup {
}
get componentManager() {
return this.application?.componentManager!;
return this.application.componentManager!;
}
public deinit() {
@@ -91,7 +91,7 @@ export class ComponentGroup {
callback();
return () => {
removeFromArray(this.changeObservers, callback);
}
};
}
private notifyObservers() {

View File

@@ -68,7 +68,7 @@ export class EditorGroup {
}
return () => {
removeFromArray(this.changeObservers, callback);
}
};
}
private notifyObservers() {

View File

@@ -1,3 +1,11 @@
import { Platform, platformFromString } from '@standardnotes/snjs';
declare const process: {
env: {
NODE_ENV: string | null | undefined;
};
};
export const isDev = process.env.NODE_ENV === 'development';
export function getPlatformString() {
@@ -20,15 +28,18 @@ export function getPlatformString() {
}
}
export function getPlatform(): Platform {
return platformFromString(getPlatformString());
}
let sharedDateFormatter: Intl.DateTimeFormat;
export function dateToLocalizedString(date: Date) {
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
if (!sharedDateFormatter) {
const locale = (
(navigator.languages && navigator.languages.length)
const locale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language
);
: navigator.language;
sharedDateFormatter = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'numeric',
@@ -46,11 +57,26 @@ export function dateToLocalizedString(date: Date) {
}
}
export function isSameDay(dateA: Date, dateB: Date): boolean {
return (
dateA.getFullYear() === dateB.getFullYear() &&
dateA.getMonth() === dateB.getMonth() &&
dateA.getDate() === dateB.getDate()
);
}
/** Via https://davidwalsh.name/javascript-debounce-function */
export function debounce(this: any, func: any, wait: number, immediate = false) {
export function debounce(
this: any,
func: any,
wait: number,
immediate = false
) {
let timeout: any;
return () => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
// eslint-disable-next-line prefer-rest-params
const args = arguments;
const later = function () {
timeout = null;
@@ -61,7 +87,7 @@ export function debounce(this: any, func: any, wait: number, immediate = false)
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
}
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
@@ -73,10 +99,10 @@ if (!Array.prototype.includes) {
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
const o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
const len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
@@ -85,14 +111,14 @@ if (!Array.prototype.includes) {
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
const n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
let k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x: number, y: number) {
return (
@@ -117,7 +143,7 @@ if (!Array.prototype.includes) {
// 8. Return false
return false;
}
},
});
}
@@ -139,7 +165,9 @@ declare const __WEB__: boolean;
declare const __DESKTOP__: boolean;
if (!__WEB__ && !__DESKTOP__) {
throw Error('Neither __WEB__ nor __DESKTOP__ is true. Check your configuration files.');
throw Error(
'Neither __WEB__ nor __DESKTOP__ is true. Check your configuration files.'
);
}
export function isDesktopApplication() {

View File

@@ -30,7 +30,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
this.state = {
...this.getInitialState(),
...this.state,
}
};
this.addAppEventObserver();
this.addAppStateObserver();
this.templateReady = true;
@@ -77,10 +77,16 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
*/
this.state = Object.freeze(Object.assign({}, this.state, state));
resolve();
this.afterStateChange();
});
});
}
/** @override */
// eslint-disable-next-line @typescript-eslint/no-empty-function
afterStateChange(): void {
}
/** @returns a promise that resolves after the UI has been updated. */
flushUI() {
return this.$timeout();

View File

@@ -7,7 +7,7 @@ import {
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { WebDirective } from '@/types';
class AccountSwitcherCtrl extends PureViewCtrl<{}, {
class AccountSwitcherCtrl extends PureViewCtrl<unknown, {
descriptors: ApplicationDescriptor[];
editingDescriptor?: ApplicationDescriptor
}> {
@@ -38,7 +38,7 @@ class AccountSwitcherCtrl extends PureViewCtrl<{}, {
reloadApplications() {
this.setState({
descriptors: this.mainApplicationGroup.getDescriptors()
})
});
}
/** @template */
@@ -63,7 +63,7 @@ class AccountSwitcherCtrl extends PureViewCtrl<{}, {
this.setState({ editingDescriptor: descriptor }).then(() => {
const input = this.inputForDescriptor(descriptor);
input?.focus();
})
});
}
/** @template */
@@ -71,7 +71,7 @@ class AccountSwitcherCtrl extends PureViewCtrl<{}, {
this.mainApplicationGroup.renameDescriptor(
this.state.editingDescriptor!,
this.state.editingDescriptor!.label
)
);
this.setState({ editingDescriptor: undefined });
}

View File

@@ -26,4 +26,12 @@
path(d="M480 256l-75.53-33.53L256.1 290.6l-148.77-68.17L32 256l224 102 224-102z")
sessions-modal(
application='self.application'
app-state='self.appState'
)
challenge-modal(
ng-repeat="challenge in self.challenges track by challenge.id"
class="sk-modal"
application="self.application"
challenge="challenge"
on-dismiss="self.removeChallenge(challenge)"
)

View File

@@ -2,8 +2,8 @@ import { RootScopeMessages } from './../../messages';
import { WebDirective } from '@/types';
import { getPlatformString } from '@/utils';
import template from './application-view.pug';
import { AppStateEvent } from '@/ui_models/app_state';
import { ApplicationEvent } from '@standardnotes/snjs';
import { AppStateEvent, PanelResizedData } from '@/ui_models/app_state';
import { ApplicationEvent, Challenge, removeFromArray } from '@standardnotes/snjs';
import {
PANEL_NAME_NOTES,
PANEL_NAME_TAGS
@@ -14,37 +14,44 @@ import {
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { alertDialog } from '@/services/alertService';
class ApplicationViewCtrl extends PureViewCtrl {
private $location?: ng.ILocationService
private $rootScope?: ng.IRootScopeService
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(
$location: ng.ILocationService,
$rootScope: ng.IRootScopeService,
private $location: ng.ILocationService,
private $rootScope: ng.IRootScopeService,
$timeout: ng.ITimeoutService
) {
super($timeout);
this.$location = $location;
this.$rootScope = $rootScope;
this.platformString = getPlatformString();
this.state = { appClass: '' };
this.state = this.getInitialState();
this.onDragDrop = this.onDragDrop.bind(this);
this.onDragOver = this.onDragOver.bind(this);
this.addDragDropHandlers();
}
deinit() {
this.$location = undefined;
this.$rootScope = undefined;
(this.application as any) = undefined;
(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 any) = undefined;
(this.onDragOver as any) = undefined;
(this.onDragDrop as unknown) = undefined;
(this.onDragOver as unknown) = undefined;
super.deinit();
}
@@ -53,23 +60,38 @@ class ApplicationViewCtrl extends PureViewCtrl {
this.loadApplication();
}
getInitialState() {
return {
appClass: '',
challenges: [],
};
}
async loadApplication() {
this.application!.componentManager!.setDesktopManager(
this.application!.getDesktopService()
this.application.componentManager.setDesktopManager(
this.application.getDesktopService()
);
await this.application!.prepareForLaunch({
await this.application.prepareForLaunch({
receiveChallenge: async (challenge) => {
this.application!.promptForChallenge(challenge);
this.$timeout(() => {
this.challenges.push(challenge);
});
}
});
await this.application!.launch();
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()
needsUnlock: this.application.hasPasscode()
});
}
@@ -80,8 +102,8 @@ class ApplicationViewCtrl extends PureViewCtrl {
}
onUpdateAvailable() {
this.$rootScope!.$broadcast(RootScopeMessages.NewUpdateAvailable);
};
this.$rootScope.$broadcast(RootScopeMessages.NewUpdateAvailable);
}
/** @override */
async onAppEvent(eventName: ApplicationEvent) {
@@ -98,21 +120,22 @@ class ApplicationViewCtrl extends PureViewCtrl {
}
/** @override */
async onAppStateEvent(eventName: AppStateEvent, data?: any) {
async onAppStateEvent(eventName: AppStateEvent, data?: unknown) {
if (eventName === AppStateEvent.PanelResized) {
if (data.panel === PANEL_NAME_NOTES) {
this.notesCollapsed = data.collapsed;
const { panel, collapsed } = data as PanelResizedData;
if (panel === PANEL_NAME_NOTES) {
this.notesCollapsed = collapsed;
}
if (data.panel === PANEL_NAME_TAGS) {
this.tagsCollapsed = data.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();
if (!(await this.application.isLocked())) {
this.application.sync();
}
}
}
@@ -128,29 +151,29 @@ class ApplicationViewCtrl extends PureViewCtrl {
}
onDragOver(event: DragEvent) {
if (event.dataTransfer!.files.length > 0) {
if (event.dataTransfer?.files.length) {
event.preventDefault();
}
}
onDragDrop(event: DragEvent) {
if (event.dataTransfer!.files.length > 0) {
if (event.dataTransfer?.files.length) {
event.preventDefault();
this.application!.alertService!.alert(
STRING_DEFAULT_FILE_ERROR
);
void alertDialog({
text: STRING_DEFAULT_FILE_ERROR
});
}
}
async handleDemoSignInFromParams() {
if (
this.$location!.search().demo === 'true' &&
this.$location.search().demo === 'true' &&
!this.application.hasAccount()
) {
await this.application!.setHost(
await this.application.setHost(
'https://syncing-server-demo.standardnotes.org'
);
this.application!.signIn(
this.application.signIn(
'demo@standardnotes.org',
'password',
);

View File

@@ -1,59 +0,0 @@
.sk-modal-background(ng-click="ctrl.cancel()")
.challenge-modal.sk-modal-content(ng-if='ctrl.templateReady')
.sn-component
.sk-panel
.sk-panel-header
.sk-panel-header-title {{ctrl.challenge.modalTitle}}
.sk-panel-content
.sk-panel-section
.sk-p.sk-panel-row.centered.prompt
strong {{ctrl.challenge.heading}}
.sk-p.sk-panel-row.centered.subprompt(ng-if='ctrl.challenge.subheading')
| {{ctrl.challenge.subheading}}
.sk-panel-section
div(ng-repeat="prompt in ctrl.state.prompts track by prompt.id")
.sk-panel-row
input.sk-input.contrast(
ng-model="ctrl.state.values[prompt.id].value"
should-focus="$index == 0"
sn-autofocus="true"
sn-enter="ctrl.submit()" ,
ng-change="ctrl.onTextValueChange(prompt)"
ng-attr-type="{{prompt.secureTextEntry ? 'password' : 'text'}}",
ng-attr-placeholder="{{prompt.title}}"
)
.sk-panel-row.centered
label.sk-label.danger(
ng-if="ctrl.state.values[prompt.id].invalid"
) Invalid authentication. Please try again.
.sk-panel-footer.extra-padding
.sk-button.info.big.block.bold(
ng-click="ctrl.submit()",
ng-class="{'info' : !ctrl.state.processing, 'neutral': ctrl.state.processing}"
ng-disabled="ctrl.state.processing"
)
.sk-label {{ctrl.state.processing ? 'Generating Keys...' : 'Submit'}}
.sk-panel-row(ng-if="ctrl.challenge.cancelable")
a.sk-panel-row.sk-a.info.centered(
ng-if="ctrl.challenge.cancelable"
ng-click="ctrl.cancel()"
) Cancel
.sk-panel-footer(ng-if="ctrl.state.showForgotPasscodeLink")
a.sk-panel-row.sk-a.info.centered(
ng-if="!ctrl.state.forgotPasscode"
ng-click="ctrl.onForgotPasscodeClick()"
) Forgot your passcode?
p.sk-panel-row.sk-p(ng-if="ctrl.state.forgotPasscode").
{{
ctrl.state.hasAccount
? "If you forgot your application passcode, your only option is to clear
your local data from this device and sign back in to your account."
: "If you forgot your application passcode, your only option is
to delete your data."
}}
a.sk-panel-row.sk-a.danger.centered(
ng-if="ctrl.state.forgotPasscode"
ng-click="ctrl.destroyLocalData()"
) Delete Local Data
.sk-panel-row

View File

@@ -1,208 +0,0 @@
import { WebApplication } from '@/ui_models/application';
import template from './challenge-modal.pug';
import {
ChallengeValue,
removeFromArray,
Challenge,
ChallengeReason,
ChallengePrompt
} from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { WebDirective } from '@/types';
import { confirmDialog } from '@/services/alertService';
import {
STRING_SIGN_OUT_CONFIRMATION,
} from '@/strings';
type InputValue = {
prompt: ChallengePrompt;
value: string;
invalid: boolean;
}
type Values = Record<number, InputValue>
type ChallengeModalState = {
prompts: ChallengePrompt[]
values: Partial<Values>
processing: boolean,
forgotPasscode: boolean,
showForgotPasscodeLink: boolean,
processingPrompts: ChallengePrompt[],
hasAccount: boolean,
}
class ChallengeModalCtrl extends PureViewCtrl<{}, ChallengeModalState> {
private $element: JQLite
application!: WebApplication
challenge!: Challenge
/* @ngInject */
constructor(
$element: JQLite,
$timeout: ng.ITimeoutService
) {
super($timeout);
this.$element = $element;
}
getState() {
return this.state as ChallengeModalState;
}
$onInit() {
super.$onInit();
const values = {} as Values;
const prompts = this.challenge.prompts;
for (const prompt of prompts) {
values[prompt.id] = {
prompt,
value: '',
invalid: false
};
}
const showForgotPasscodeLink = [
ChallengeReason.ApplicationUnlock,
ChallengeReason.Migration
].includes(this.challenge.reason);
this.setState({
prompts,
values,
processing: false,
forgotPasscode: false,
showForgotPasscodeLink,
hasAccount: this.application.hasAccount(),
processingPrompts: []
});
this.application.addChallengeObserver(
this.challenge,
{
onValidValue: (value) => {
this.getState().values[value.prompt.id]!.invalid = false;
removeFromArray(this.state.processingPrompts, value.prompt);
this.reloadProcessingStatus();
},
onInvalidValue: (value) => {
this.getState().values[value.prompt.id]!.invalid = true;
/** If custom validation, treat all values together and not individually */
if (!value.prompt.validates) {
this.setState({ processingPrompts: [], processing: false });
} else {
removeFromArray(this.state.processingPrompts, value.prompt);
this.reloadProcessingStatus();
}
},
onComplete: () => {
this.dismiss();
},
onCancel: () => {
this.dismiss();
},
}
);
}
deinit() {
(this.application as any) = undefined;
(this.challenge as any) = undefined;
super.deinit();
}
reloadProcessingStatus() {
return this.setState({
processing: this.state.processingPrompts.length > 0
});
}
async destroyLocalData() {
if (await confirmDialog({
text: STRING_SIGN_OUT_CONFIRMATION,
confirmButtonStyle: "danger"
})) {
await this.application.signOut();
this.dismiss();
};
}
/** @template */
cancel() {
if (this.challenge.cancelable) {
this.application!.cancelChallenge(this.challenge);
}
}
onForgotPasscodeClick() {
this.setState({
forgotPasscode: true
});
}
onTextValueChange(prompt: ChallengePrompt) {
const values = this.getState().values;
values[prompt.id]!.invalid = false;
this.setState({ values });
}
validate() {
const failed = [];
for (const prompt of this.getState().prompts) {
const value = this.getState().values[prompt.id];
if (!value || value.value.length === 0) {
this.getState().values[prompt.id]!.invalid = true;
}
}
return failed.length === 0;
}
async submit() {
if (!this.validate()) {
return;
}
await this.setState({ processing: true });
const values: ChallengeValue[] = [];
for (const inputValue of Object.values(this.getState().values)) {
const rawValue = inputValue!!.value;
const value = new ChallengeValue(inputValue!.prompt, rawValue);
values.push(value);
}
const processingPrompts = values.map((v) => v.prompt);
await this.setState({
processingPrompts: processingPrompts,
processing: processingPrompts.length > 0
})
/**
* Unfortunately neccessary to wait 50ms so that the above setState call completely
* updates the UI to change processing state, before we enter into UI blocking operation
* (crypto key generation)
*/
this.$timeout(() => {
if (values.length > 0) {
this.application.submitValuesForChallenge(this.challenge, values);
} else {
this.setState({ processing: false });
}
}, 50)
}
dismiss() {
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class ChallengeModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = ChallengeModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
challenge: '=',
application: '='
};
}
}

View File

@@ -0,0 +1,409 @@
import { WebApplication } from '@/ui_models/application';
import { Dialog } from '@reach/dialog';
import {
ChallengeValue,
removeFromArray,
Challenge,
ChallengeReason,
ChallengePrompt,
ChallengeValidation,
ProtectionSessionDurations,
} from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { WebDirective } from '@/types';
import { confirmDialog } from '@/services/alertService';
import { STRING_SIGN_OUT_CONFIRMATION } from '@/strings';
import { Ref, render } from 'preact';
import { useRef } from 'preact/hooks';
import ng from 'angular';
type InputValue = {
prompt: ChallengePrompt;
value: string | number | boolean;
invalid: boolean;
};
type Values = Record<number, InputValue>;
type ChallengeModalState = {
prompts: ChallengePrompt[];
values: Partial<Values>;
processing: boolean;
forgotPasscode: boolean;
showForgotPasscodeLink: boolean;
processingPrompts: ChallengePrompt[];
hasAccount: boolean;
protectedNoteAccessDuration: number;
};
class ChallengeModalCtrl extends PureViewCtrl<unknown, ChallengeModalState> {
application!: WebApplication;
challenge!: Challenge;
onDismiss!: () => void;
submitting = false;
/** @template */
protectionsSessionDurations = ProtectionSessionDurations;
protectionsSessionValidation = ChallengeValidation.ProtectionSessionDuration;
/* @ngInject */
constructor(
private $element: ng.IRootElementService,
$timeout: ng.ITimeoutService
) {
super($timeout);
}
getState() {
return this.state as ChallengeModalState;
}
$onInit() {
super.$onInit();
const values = {} as Values;
const prompts = this.challenge.prompts;
for (const prompt of prompts) {
values[prompt.id] = {
prompt,
value: prompt.initialValue ?? '',
invalid: false,
};
}
const showForgotPasscodeLink = [
ChallengeReason.ApplicationUnlock,
ChallengeReason.Migration,
].includes(this.challenge.reason);
this.setState({
prompts,
values,
processing: false,
forgotPasscode: false,
showForgotPasscodeLink,
hasAccount: this.application.hasAccount(),
processingPrompts: [],
protectedNoteAccessDuration: ProtectionSessionDurations[0].valueInSeconds,
});
this.application.addChallengeObserver(this.challenge, {
onValidValue: (value) => {
this.state.values[value.prompt.id]!.invalid = false;
removeFromArray(this.state.processingPrompts, value.prompt);
this.reloadProcessingStatus();
/** Trigger UI update */
this.afterStateChange();
},
onInvalidValue: (value) => {
this.state.values[value.prompt.id]!.invalid = true;
/** If custom validation, treat all values together and not individually */
if (!value.prompt.validates) {
this.setState({ processingPrompts: [], processing: false });
} else {
removeFromArray(this.state.processingPrompts, value.prompt);
this.reloadProcessingStatus();
}
/** Trigger UI update */
this.afterStateChange();
},
onComplete: () => {
this.dismiss();
},
onCancel: () => {
this.dismiss();
},
});
}
deinit() {
(this.application as any) = undefined;
(this.challenge as any) = undefined;
super.deinit();
}
reloadProcessingStatus() {
return this.setState({
processing: this.state.processingPrompts.length > 0,
});
}
async destroyLocalData() {
if (
await confirmDialog({
text: STRING_SIGN_OUT_CONFIRMATION,
confirmButtonStyle: 'danger',
})
) {
await this.application.signOut();
this.dismiss();
}
}
/** @template */
cancel() {
if (this.challenge.cancelable) {
this.application!.cancelChallenge(this.challenge);
}
}
onForgotPasscodeClick() {
this.setState({
forgotPasscode: true,
});
}
onTextValueChange(prompt: ChallengePrompt) {
const values = this.getState().values;
values[prompt.id]!.invalid = false;
this.setState({ values });
}
onNumberValueChange(prompt: ChallengePrompt, value: number) {
const values = this.state.values;
values[prompt.id]!.invalid = false;
values[prompt.id]!.value = value;
this.setState({ values });
}
validate() {
let failed = 0;
for (const prompt of this.state.prompts) {
const value = this.state.values[prompt.id]!;
if (typeof value.value === 'string' && value.value.length === 0) {
this.state.values[prompt.id]!.invalid = true;
failed++;
}
}
return failed === 0;
}
async submit() {
if (!this.validate()) {
return;
}
if (this.submitting) {
return;
}
this.submitting = true;
await this.setState({ processing: true });
const values: ChallengeValue[] = [];
for (const inputValue of Object.values(this.getState().values)) {
const rawValue = inputValue!.value;
const value = new ChallengeValue(inputValue!.prompt, rawValue);
values.push(value);
}
const processingPrompts = values.map((v) => v.prompt);
await this.setState({
processingPrompts: processingPrompts,
processing: processingPrompts.length > 0,
});
/**
* Unfortunately neccessary to wait 50ms so that the above setState call completely
* updates the UI to change processing state, before we enter into UI blocking operation
* (crypto key generation)
*/
this.$timeout(() => {
if (values.length > 0) {
this.application.submitValuesForChallenge(this.challenge, values);
} else {
this.setState({ processing: false });
}
this.submitting = false;
}, 50);
}
afterStateChange() {
this.render();
}
dismiss() {
this.onDismiss();
}
$onDestroy() {
render(<></>, this.$element[0]);
}
private render() {
if (!this.state.prompts) return;
render(<ChallengeModalView ctrl={this} />, this.$element[0]);
}
}
export class ChallengeModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
// this.template = template;
this.controller = ChallengeModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
challenge: '=',
application: '=',
onDismiss: '&',
};
}
}
function ChallengeModalView({ ctrl }: { ctrl: ChallengeModalCtrl }) {
const initialFocusRef = useRef<HTMLInputElement>();
return (
<Dialog
initialFocusRef={initialFocusRef}
onDismiss={() => {
if (ctrl.challenge.cancelable) {
ctrl.cancel();
}
}}
>
<div className="challenge-modal sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
<div className="sk-panel-header">
<div className="sk-panel-header-title">
{ctrl.challenge.modalTitle}
</div>
</div>
<div className="sk-panel-content">
<div className="sk-panel-section">
<div className="sk-p sk-panel-row centered prompt">
<strong>{ctrl.challenge.heading}</strong>
</div>
{ctrl.challenge.subheading && (
<div className="sk-p sk-panel-row centered subprompt">
{ctrl.challenge.subheading}
</div>
)}
</div>
<div className="sk-panel-section">
{ChallengePrompts({ ctrl, initialFocusRef })}
</div>
</div>
<div className="sk-panel-footer extra-padding">
<div
className={
'sk-button big block bold ' +
(ctrl.state.processing ? 'neutral' : 'info')
}
disabled={ctrl.state.processing}
onClick={() => ctrl.submit()}
>
<div className="sk-label">
{ctrl.state.processing ? 'Generating Keys…' : 'Submit'}
</div>
</div>
{ctrl.challenge.cancelable && (
<>
<div className="sk-panel-row"></div>
<a
className="sk-panel-row sk-a info centered"
onClick={() => ctrl.cancel()}
>
Cancel
</a>
</>
)}
</div>
{ctrl.state.showForgotPasscodeLink && (
<div className="sk-panel-footer">
{ctrl.state.forgotPasscode ? (
<>
<p className="sk-panel-row sk-p">
{ctrl.state.hasAccount
? 'If you forgot your application passcode, your ' +
'only option is to clear your local data from this ' +
'device and sign back in to your account.'
: 'If you forgot your application passcode, your ' +
'only option is to delete your data.'}
</p>
<a
className="sk-panel-row sk-a danger centered"
onClick={() => {
ctrl.destroyLocalData();
}}
>
Delete Local Data
</a>
</>
) : (
<a
className="sk-panel-row sk-a info centered"
onClick={() => ctrl.onForgotPasscodeClick()}
>
Forgot your passcode?
</a>
)}
<div className="sk-panel-row"></div>
</div>
)}
</div>
</div>
</div>
</Dialog>
);
}
function ChallengePrompts({
ctrl,
initialFocusRef,
}: {
ctrl: ChallengeModalCtrl;
initialFocusRef: Ref<HTMLInputElement>;
}) {
return ctrl.state.prompts.map((prompt, index) => (
<>
{/** ProtectionSessionDuration can't just be an input field */}
{prompt.validation === ChallengeValidation.ProtectionSessionDuration ? (
<div key={prompt.id} className="sk-panel-row">
<div className="sk-horizontal-group">
<div className="sk-p sk-bold">Remember For</div>
{ProtectionSessionDurations.map((option) => (
<a
className={
'sk-a info ' +
(option.valueInSeconds === ctrl.state.values[prompt.id]!.value
? 'boxed'
: '')
}
onClick={(event) => {
event.preventDefault();
ctrl.onNumberValueChange(prompt, option.valueInSeconds);
}}
>
{option.label}
</a>
))}
</div>
</div>
) : (
<div key={prompt.id} className="sk-panel-row">
<input
className="sk-input contrast"
value={ctrl.state.values[prompt.id]!.value as string | number}
onChange={(event) => {
const value = (event.target as HTMLInputElement).value;
ctrl.state.values[prompt.id]!.value = value;
ctrl.onTextValueChange(prompt);
}}
onKeyUp={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
ctrl.submit();
}
}}
ref={index === 0 ? initialFocusRef : undefined}
placeholder={prompt.title}
type={prompt.secureTextEntry ? 'password' : 'text'}
/>
</div>
)}
{ctrl.state.values[prompt.id]!.invalid && (
<div className="sk-panel-row centered">
<label className="sk-label danger">
Invalid authentication. Please try again.
</label>
</div>
)}
</>
));
}

View File

@@ -80,8 +80,7 @@
)
menu-row(
action='self.selectedMenuItem(true); self.toggleProtectNote()'
desc=`'Protecting a note will require credentials to view
it (Manage Privileges via Account menu)'`,
desc=`'Protecting a note will require credentials to view it'`,
label="self.note.protected ? 'Unprotect' : 'Protect'"
)
menu-row(
@@ -210,7 +209,7 @@
on-load='self.onEditorLoad',
application='self.application'
)
textarea#note-text-editor.editable(
textarea#note-text-editor.editable.font-editor(
dir='auto',
ng-attr-spellcheck='{{self.state.spellcheck}}',
ng-change='self.contentChanged()',

View File

@@ -1,4 +1,4 @@
import { STRING_ARCHIVE_LOCKED_ATTEMPT, STRING_SAVING_WHILE_DOCUMENT_HIDDEN, STRING_UNARCHIVE_LOCKED_ATTEMPT } from './../../strings';
import { Strings, STRING_ARCHIVE_LOCKED_ATTEMPT, STRING_SAVING_WHILE_DOCUMENT_HIDDEN, STRING_UNARCHIVE_LOCKED_ATTEMPT } from './../../strings';
import { Editor } from '@/ui_models/editor';
import { WebApplication } from '@/ui_models/application';
import { PanelPuppet, WebDirective } from '@/types';
@@ -8,7 +8,6 @@ import {
isPayloadSourceRetrieved,
isPayloadSourceInternalChange,
ContentType,
ProtectedAction,
SNComponent,
SNNote,
SNTag,
@@ -24,7 +23,7 @@ import { isDesktopApplication } from '@/utils';
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
import template from './editor-view.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { AppStateEvent, EventSource } from '@/ui_models/app_state';
import { EventSource } from '@/ui_models/app_state';
import {
STRING_DELETED_NOTE,
STRING_INVALID_NOTE,
@@ -48,11 +47,6 @@ const ElementIds = {
EditorContent: 'editor-content',
NoteTagsComponentContainer: 'note-tags-component-container'
};
const Fonts = {
DesktopMonospaceFamily: `Menlo,Consolas,'DejaVu Sans Mono',monospace`,
WebMonospaceFamily: `monospace`,
SansSerifFamily: `inherit`
};
type NoteStatus = {
message?: string
@@ -85,7 +79,7 @@ type EditorState = {
* then re-initialized. Used when reloading spellcheck status. */
textareaUnloading: boolean
/** Fields that can be directly mutated by the template */
mutable: {}
mutable: any
}
type EditorValues = {
@@ -98,7 +92,7 @@ function sortAlphabetically(array: SNComponent[]): SNComponent[] {
return array.sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1);
}
class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
/** Passed through template */
readonly application!: WebApplication
readonly editor!: Editor
@@ -143,7 +137,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this);
this.onEditorLoad = () => {
this.application!.getDesktopService().redoSearch();
}
};
}
deinit() {
@@ -200,7 +194,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
if (note.lastSyncBegan) {
if (note.lastSyncEnd) {
if (note.lastSyncBegan!.getTime() > note.lastSyncEnd!.getTime()) {
this.showSavingStatus()
this.showSavingStatus();
} else if (note.lastSyncEnd!.getTime() > note.lastSyncBegan!.getTime()) {
this.showAllChangesSavedStatus();
}
@@ -248,7 +242,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
case ApplicationEvent.HighLatencySync:
this.setState({ syncTakingTooLong: true });
break;
case ApplicationEvent.CompletedFullSync:
case ApplicationEvent.CompletedFullSync: {
this.setState({ syncTakingTooLong: false });
const isInErrorState = this.state.saveError;
/** if we're still dirty, don't change status, a sync is likely upcoming. */
@@ -256,6 +250,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
this.showAllChangesSavedStatus();
}
break;
}
case ApplicationEvent.FailedSync:
/**
* Only show error status in editor if the note is dirty.
@@ -412,7 +407,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
await this.application.changeItem(this.note.uuid, (mutator) => {
const noteMutator = mutator as NoteMutator;
noteMutator.prefersPlainEditor = false;
})
});
}
await this.associateComponentWithCurrentNote(component);
}
@@ -471,7 +466,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
(mutator) => {
mutator.addItemAsRelationship(note);
}
)
);
}
if (!this.application.findItem(note.uuid)) {
this.application.alertService!.alert(
@@ -494,7 +489,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
noteMutator.preview_plain = previewPlain;
noteMutator.preview_html = undefined;
}
}, isUserModified)
}, isUserModified);
if (this.saveTimeout) {
this.$timeout.cancel(this.saveTimeout);
}
@@ -549,7 +544,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
this.statusTimeout = this.$timeout(() => {
this.setState({
noteStatus: status
})
});
}, MINIMUM_STATUS_DURATION);
} else {
this.setState({
@@ -601,10 +596,12 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
this.setMenuState('showOptionsMenu', false);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTitleFocus() {
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTitleBlur() {
}
@@ -627,50 +624,35 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
);
return;
}
const run = async () => {
if (this.note.locked) {
this.application.alertService!.alert(
STRING_DELETE_LOCKED_ATTEMPT
);
return;
}
const title = this.note.safeTitle().length
? `'${this.note.title}'`
: "this note";
const text = StringDeleteNote(
title,
permanently
if (this.note.locked) {
this.application.alertService!.alert(
STRING_DELETE_LOCKED_ATTEMPT
);
if (await confirmDialog({
text,
confirmButtonStyle: 'danger'
})) {
if (permanently) {
this.performNoteDeletion(this.note);
} else {
this.saveNote(
true,
false,
true,
(mutator) => {
mutator.trashed = true;
}
);
}
};
};
const requiresPrivilege = await this.application.privilegesService!.actionRequiresPrivilege(
ProtectedAction.DeleteNote
return;
}
const title = this.note.safeTitle().length
? `'${this.note.title}'`
: "this note";
const text = StringDeleteNote(
title,
permanently
);
if (requiresPrivilege) {
this.application.presentPrivilegesModal(
ProtectedAction.DeleteNote,
() => {
run();
}
);
} else {
run();
if (await confirmDialog({
text,
confirmButtonStyle: 'danger'
})) {
if (permanently) {
this.performNoteDeletion(this.note);
} else {
this.saveNote(
true,
false,
true,
(mutator) => {
mutator.trashed = true;
}
);
}
}
}
@@ -715,7 +697,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
false,
true,
(mutator) => {
mutator.pinned = !this.note.pinned
mutator.pinned = !this.note.pinned;
}
);
}
@@ -726,28 +708,26 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
false,
true,
(mutator) => {
mutator.locked = !this.note.locked
mutator.locked = !this.note.locked;
}
);
}
toggleProtectNote() {
this.saveNote(
true,
false,
true,
(mutator) => {
mutator.protected = !this.note.protected
async toggleProtectNote() {
if (this.note.protected) {
void this.application.unprotectNote(this.note);
} else {
const note = await this.application.protectNote(this.note);
if (note?.protected && !this.application.hasProtectionSources()) {
if (await confirmDialog({
text: Strings.protectingNoteWithoutProtectionSources,
confirmButtonText: Strings.openAccountMenu,
confirmButtonStyle: 'info',
})) {
this.appState.accountMenu.setShow(true);
}
}
);
/** Show privileges manager if protection is not yet set up */
this.application.privilegesService!.actionHasPrivilegesConfigured(
ProtectedAction.ViewProtectedNotes
).then((configured) => {
if (!configured) {
this.application.presentPrivilegesManagementModal();
}
});
}
}
toggleNotePreview() {
@@ -756,7 +736,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
false,
true,
(mutator) => {
mutator.hidePreview = !this.note.hidePreview
mutator.hidePreview = !this.note.hidePreview;
}
);
}
@@ -775,7 +755,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
false,
true,
(mutator) => {
mutator.archived = !this.note.archived
mutator.archived = !this.note.archived;
},
/** If we are unarchiving, and we are in the archived tag, close the editor */
this.note.archived && this.appState.selectedTag?.isArchiveTag
@@ -884,7 +864,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
(mutator) => {
mutator.addItemAsRelationship(note);
}
)
);
}
this.application.sync();
this.reloadTags();
@@ -966,20 +946,18 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
}
reloadFont() {
const editor = document.getElementById(
ElementIds.NoteTextEditor
);
if (!editor) {
return;
}
const root = document.querySelector(':root') as HTMLElement;
const propertyName = '--sn-stylekit-editor-font-family';
if (this.state.monospaceFont) {
if (this.state.isDesktop) {
editor.style.fontFamily = Fonts.DesktopMonospaceFamily;
} else {
editor.style.fontFamily = Fonts.WebMonospaceFamily;
}
root.style.setProperty(
propertyName,
'var(--sn-stylekit-monospace-font)'
);
} else {
editor.style.fontFamily = Fonts.SansSerifFamily;
root.style.setProperty(
propertyName,
'var(--sn-stylekit-sans-serif-font)'
);
}
}
@@ -991,7 +969,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
);
await this.setState({
[key]: !currentValue
})
});
this.reloadFont();
if (key === PrefKey.EditorSpellcheck) {
@@ -1082,7 +1060,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
(mutator) => {
mutator.addItemAsRelationship(this.note);
}
)
);
}
}
}
@@ -1141,7 +1119,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
const mutator = m as ComponentMutator;
mutator.removeAssociatedItemId(note.uuid);
mutator.disassociateWithItem(note.uuid);
})
});
}
async associateComponentWithCurrentNote(component: SNComponent) {
@@ -1150,7 +1128,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
const mutator = m as ComponentMutator;
mutator.removeDisassociatedItemId(note.uuid);
mutator.associateWithItem(note.uuid);
})
});
}
registerKeyboardShortcuts() {

View File

@@ -8,15 +8,10 @@ class EditorGroupViewCtrl {
private application!: WebApplication
public editors: Editor[] = []
/* @ngInject */
constructor() {
}
$onInit() {
this.application.editorGroup.addChangeObserver(() => {
this.editors = this.application.editorGroup.editors;
})
});
}
}

View File

@@ -5,7 +5,6 @@ import { dateToLocalizedString, preventRefreshing } from '@/utils';
import {
ApplicationEvent,
SyncQueueStrategy,
ProtectedAction,
ContentType,
SNComponent,
SNTheme,
@@ -44,7 +43,7 @@ type DockShortcut = {
}
}
class FooterViewCtrl extends PureViewCtrl<{}, {
class FooterViewCtrl extends PureViewCtrl<unknown, {
outOfSync: boolean;
hasPasscode: boolean;
dataUpgradeAvailable: boolean;
@@ -63,7 +62,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
public arbitraryStatusMessage?: string
public user?: any
private offline = true
private showAccountMenu = false
public showAccountMenu = false
private didCheckForOffline = false
private queueExtReload = false
private reloadInProgress = false
@@ -76,7 +75,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
private observerRemovers: Array<() => void> = [];
private completedInitialSync = false;
private showingDownloadStatus = false;
private removeBetaWarningListener?: IReactionDisposer;
private autorunDisposer?: IReactionDisposer;
/* @ngInject */
constructor(
@@ -104,7 +103,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
this.rootScopeListener2 = undefined;
(this.closeAccountMenu as any) = undefined;
(this.toggleSyncResolutionMenu as any) = undefined;
this.removeBetaWarningListener?.();
this.autorunDisposer?.();
super.deinit();
}
@@ -116,8 +115,9 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
});
});
this.loadAccountSwitcherState();
this.removeBetaWarningListener = autorun(() => {
this.autorunDisposer = autorun(() => {
const showBetaWarning = this.appState.showBetaWarning;
this.showAccountMenu = this.appState.accountMenu.show;
this.setState({
showBetaWarning: showBetaWarning,
showDataUpgrade: !showBetaWarning
@@ -207,9 +207,9 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
case AppStateEvent.BeganBackupDownload:
statusService.setMessage("Saving local backup…");
break;
case AppStateEvent.EndedBackupDownload:
case AppStateEvent.EndedBackupDownload: {
const successMessage = "Successfully saved backup.";
const errorMessage = "Unable to save local backup."
const errorMessage = "Unable to save local backup.";
statusService.setMessage(data.success ? successMessage : errorMessage);
const twoSeconds = 2000;
@@ -222,6 +222,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
}
}, twoSeconds);
break;
}
}
}
@@ -255,7 +256,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
if (!this.didCheckForOffline) {
this.didCheckForOffline = true;
if (this.offline && this.application.getNoteCount() === 0) {
this.showAccountMenu = true;
this.appState.accountMenu.setShow(true);
}
}
this.syncUpdated();
@@ -297,7 +298,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
theme.package_info.dock_icon
);
}
)
);
this.observerRemovers.push(this.application.streamItems(
ContentType.Component,
@@ -437,7 +438,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
}
accountMenuPressed() {
this.showAccountMenu = !this.showAccountMenu;
this.appState.accountMenu.toggleShow();
this.closeAllRooms();
}
@@ -446,7 +447,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
}
closeAccountMenu() {
this.showAccountMenu = false;
this.appState.accountMenu.setShow(false);
}
lockApp() {
@@ -544,28 +545,9 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
}
async selectRoom(room: SNComponent) {
const run = () => {
this.$timeout(() => {
this.roomShowState[room.uuid] = !this.roomShowState[room.uuid];
});
};
if (!this.roomShowState[room.uuid]) {
const requiresPrivilege = await this.application.privilegesService!
.actionRequiresPrivilege(
ProtectedAction.ManageExtensions
);
if (requiresPrivilege) {
this.application.presentPrivilegesModal(
ProtectedAction.ManageExtensions,
run
);
} else {
run();
}
} else {
run();
}
this.$timeout(() => {
this.roomShowState[room.uuid] = !this.roomShowState[room.uuid];
});
}
displayBetaDialog() {
@@ -582,7 +564,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
if (this.application && this.application.authenticationInProgress()) {
return;
}
this.showAccountMenu = false;
this.appState.accountMenu.setShow(false);
}
}

View File

@@ -6,4 +6,4 @@ export { EditorView } from './editor/editor_view';
export { FooterView } from './footer/footer_view';
export { NotesView } from './notes/notes_view';
export { TagsView } from './tags/tags_view';
export { ChallengeModal } from './challenge_modal/challenge_modal'
export { ChallengeModal } from './challenge_modal/challenge_modal';

View File

@@ -5,44 +5,54 @@ export function notePassesFilter(
showArchived: boolean,
hidePinned: boolean,
filterText: string
) {
let canShowArchived = showArchived;
): boolean {
const canShowArchived = showArchived;
const canShowPinned = !hidePinned;
if (
(note.archived && !canShowArchived) ||
(note.pinned && !canShowPinned)
) {
if ((note.archived && !canShowArchived) || (note.pinned && !canShowPinned)) {
return false;
}
return noteMatchesQuery(note, filterText);
if (note.protected) {
const match = noteMatchesQuery(note, filterText);
/** Only match title to prevent leaking protected note text */
return match === Match.Title || match === Match.TitleAndText;
} else {
return noteMatchesQuery(note, filterText) !== Match.None;
}
}
function noteMatchesQuery(
note: SNNote,
query: string
) {
enum Match {
None = 0,
Title = 1,
Text = 2,
TitleAndText = Title + Text,
Uuid = 5,
}
function noteMatchesQuery(note: SNNote, query: string): Match {
if (query.length === 0) {
return true;
return Match.TitleAndText;
}
const title = note.safeTitle().toLowerCase();
const text = note.safeText().toLowerCase();
const lowercaseText = query.toLowerCase();
const words = lowercaseText.split(' ');
const quotedText = stringBetweenQuotes(lowercaseText);
if (quotedText) {
return title.includes(quotedText) || text.includes(quotedText);
return (
(title.includes(quotedText) ? Match.Title : Match.None) +
(text.includes(quotedText) ? Match.Text : Match.None)
);
}
if (stringIsUuid(lowercaseText)) {
return note.uuid === lowercaseText;
return note.uuid === lowercaseText ? Match.Uuid : Match.None;
}
const words = lowercaseText.split(" ");
const matchesTitle = words.every((word) => {
return title.indexOf(word) >= 0;
});
const matchesBody = words.every((word) => {
return text.indexOf(word) >= 0;
});
return matchesTitle || matchesBody;
return (matchesTitle ? Match.Title : 0) + (matchesBody ? Match.Text : 0);
}
function stringBetweenQuotes(text: string) {

View File

@@ -3,7 +3,7 @@
#notes-title-bar.section-title-bar
.padded
.section-title-bar-header
.title {{self.state.panelTitle}}
.sk-h2.font-semibold.title {{self.state.panelTitle}}
.sk-button.contrast.wide(
ng-click='self.createNewNote()',
title='Create a new note in the selected tag'
@@ -24,6 +24,10 @@
ng-click='self.clearFilterText();',
ng-show='self.state.noteFilter.text'
) ✕
no-account-warning(
application='self.application'
app-state='self.appState'
)
#notes-menu-bar.sn-component
.sk-app-bar.no-edges
.left
@@ -139,10 +143,12 @@
.default-preview(
ng-show='!note.preview_html && !note.preview_plain'
) {{note.text}}
.date.faded(ng-show='!self.state.hideDate')
span(ng-show="self.state.sortBy == 'userModifiedDate'")
.bottom-info.faded(ng-show='!self.state.hideDate || note.protected')
span(ng-if="note.protected")
| Protected{{self.state.hideDate ? '' : ' • '}}
span(ng-show="!self.state.hideDate && self.state.sortBy == 'userModifiedDate'")
| Modified {{note.updatedAtString || 'Now'}}
span(ng-show="self.state.sortBy != 'userModifiedDate'")
span(ng-show="!self.state.hideDate && self.state.sortBy != 'userModifiedDate'")
| {{note.createdAtString || 'Now'}}
.tags-string(ng-if='!self.state.hideTags && self.state.renderedNotesTags[$index]')
.faded {{self.state.renderedNotesTags[$index]}}

View File

@@ -59,7 +59,7 @@ const DEFAULT_LIST_NUM_NOTES = 20;
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
private panelPuppet?: PanelPuppet
private reloadNotesPromise?: any
@@ -410,7 +410,7 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
if (activeNote && activeNote.conflictOf) {
this.application!.changeAndSaveItem(activeNote.uuid, (mutator) => {
mutator.conflictOf = undefined;
})
});
}
if (this.isFiltering()) {
this.application!.getDesktopService().searchText(this.getState().noteFilter.text);
@@ -576,12 +576,6 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
class: 'warning'
});
}
if (note.protected) {
flags.push({
text: "Protected",
class: 'success'
});
}
if (note.locked) {
flags.push({
text: "Locked",
@@ -641,7 +635,7 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
selectNextNote() {
const displayableNotes = this.displayableNotes();
const currentIndex = displayableNotes.findIndex((candidate) => {
return candidate.uuid === this.activeEditorNote!.uuid
return candidate.uuid === this.activeEditorNote!.uuid;
});
if (currentIndex + 1 < displayableNotes.length) {
this.selectNote(displayableNotes[currentIndex + 1]);
@@ -798,7 +792,7 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
],
onKeyDown: () => {
const searchBar = this.getSearchBar();
if (searchBar) { searchBar.focus(); };
if (searchBar) { searchBar.focus(); }
}
});
}

View File

@@ -25,9 +25,9 @@
.tag-info
.title(ng-if="!tag.errorDecrypting") {{tag.title}}
.count(ng-show='tag.isAllTag') {{self.state.noteCounts[tag.uuid]}}
.danger.small-text.bold(ng-show='tag.conflictOf') Conflicted Copy
.danger.small-text.bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
.info.small-text.bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
.tags-title-section.section-title-bar
.section-title-bar-header
.sk-h3.title
@@ -52,9 +52,9 @@
spellcheck='false'
)
.count {{self.state.noteCounts[tag.uuid]}}
.danger.small-text.bold(ng-show='tag.conflictOf') Conflicted Copy
.danger.small-text.bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
.info.small-text.bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
.menu(ng-show='self.state.selectedTag == tag')
a.item(ng-click='self.selectedRenameTag(tag)' ng-show='!self.state.editingTag') Rename
a.item(ng-click='self.saveTag($event, tag)' ng-show='self.state.editingTag') Save

View File

@@ -36,7 +36,7 @@ type TagState = {
templateTag?: SNTag
}
class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
/** Passed through template */
readonly application!: WebApplication
@@ -136,7 +136,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
} else {
this.setState({
selectedTag: matchingTag
})
});
}
}
}
@@ -186,14 +186,14 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag)
.filter((note) => {
return !note.archived && !note.trashed;
})
});
noteCounts[tag.uuid] = notes.length;
}
} else {
const notes = this.application.referencesForItem(tag, ContentType.Note)
.filter((note) => {
return !note.archived && !note.trashed;
})
});
noteCounts[tag.uuid] = notes.length;
}
}
@@ -264,7 +264,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
if (tag.conflictOf) {
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
mutator.conflictOf = undefined;
})
});
}
this.application.getAppState().setSelectedTag(tag);
}
@@ -326,7 +326,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
"A tag with this name already exists."
);
return;
};
}
await this.application.changeAndSaveItem<TagMutator>(tag.uuid, (mutator) => {
mutator.title = newTitle;
});
@@ -350,7 +350,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
"A tag with this name already exists."
);
return;
};
}
const insertedTag = await this.application.insertItem(newTag);
const changedTag = await this.application.changeItem<TagMutator>(insertedTag.uuid, (m) => {
m.title = newTitle;