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

@@ -1,6 +1,13 @@
{
"extends": ["eslint:recommended", "prettier"],
"root": true,
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:react-hooks/recommended"
],
"plugins": ["@typescript-eslint", "react"],
"parserOptions": {
"project": "./app/assets/javascripts/tsconfig.json"
},

View File

@@ -8,7 +8,7 @@ jobs:
tsc:
name: Check types
name: Check types & lint
runs-on: ubuntu-latest
@@ -22,6 +22,9 @@ jobs:
- name: Typescript
run: yarn tsc
- name: ESLint
run: yarn lint --quiet
deploy:
runs-on: ubuntu-latest

View File

@@ -7,12 +7,21 @@ on:
- main
jobs:
tsc:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: yarn install --pure-lockfile
- name: Typescript
run: yarn tsc
- name: ESLint
run: yarn lint --quiet

View File

@@ -8,7 +8,7 @@ jobs:
tsc:
name: Check types
name: Check types & lint
runs-on: ubuntu-latest
@@ -23,6 +23,9 @@ jobs:
- name: Typescript
run: yarn tsc
- name: ESLint
run: yarn lint --quiet
deploy:
runs-on: ubuntu-latest

View File

@@ -1,10 +1,5 @@
FROM ruby:2.7.1-alpine3.12
ARG UID=1000
ARG GID=1000
RUN addgroup -S webapp -g $GID && adduser -D -S webapp -G webapp -u $UID
RUN apk add --update --no-cache \
alpine-sdk \
nodejs-current \
@@ -16,19 +11,15 @@ RUN apk add --update --no-cache \
WORKDIR /app/
RUN chown -R $UID:$GID .
COPY package.json yarn.lock Gemfile Gemfile.lock /app/
USER webapp
COPY --chown=$UID:$GID package.json yarn.lock Gemfile Gemfile.lock /app/
COPY --chown=$UID:$GID vendor /app/vendor
COPY vendor /app/vendor
RUN yarn install --pure-lockfile
RUN gem install bundler && bundle install
COPY --chown=$UID:$GID . /app/
COPY . /app/
RUN yarn bundle

View File

@@ -98,7 +98,7 @@ GEM
mini_mime (>= 0.1.1)
method_source (0.9.2)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
mini_portile2 (2.5.0)
minitest (5.14.0)
msgpack (1.3.3)
msgpack (1.3.3-x64-mingw32)
@@ -106,14 +106,16 @@ GEM
net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0)
nio4r (2.5.2)
nokogiri (1.10.8)
mini_portile2 (~> 2.4.0)
nokogiri (1.10.8-x64-mingw32)
mini_portile2 (~> 2.4.0)
nokogiri (1.11.1)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogiri (1.11.1-x64-mingw32)
racc (~> 1.4)
non-stupid-digest-assets (1.0.9)
sprockets (>= 2.0)
puma (4.3.5)
nio4r (~> 2.0)
racc (1.5.2)
rack (2.2.3)
rack-cors (1.1.1)
rack (>= 2.0.0)

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2019-4-11: Created with FontForge (http://fontforge.org)
-->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20190318 at Thu Apr 11 10:47:26 2019
By Mo Bitar
Copyright (c) 2019, Mo Bitar
</metadata>
<defs>
<font id="Ionicons" horiz-adv-x="384" >
<font-face
font-family="Ionicons"
font-weight="400"
font-stretch="normal"
units-per-em="512"
panose-1="2 0 5 9 0 0 0 0 0 0"
ascent="448"
descent="-64"
bbox="0 -32 384 416"
underline-thickness="25.6"
underline-position="-51.2"
unicode-range="U+F200-F266"
/>
<missing-glyph />
<glyph glyph-name="ion-arrow-return-right" unicode="&#xf266;"
d="M384 256l-128 -96v64h-192v-128h248c4 0 8 -4 8 -8v-48c0 -4 -4 -8 -8 -8h-304c-4 0 -8 4 -8 8v240c0 4 4 8 8 8h248v64z" />
<glyph glyph-name="ion-arrow-return-left" unicode="&#xf265;"
d="M128 352v-64h248c4 0 8 -4 8 -8v-240c0 -4 -4 -8 -8 -8h-304c-4 0 -8 4 -8 8v48c0 4 4 8 8 8h248v128h-192v-64l-128 96z" />
<glyph glyph-name="ion-plus" unicode="&#xf218;"
d="M384 224v-64h-160v-160h-64v160h-160v64h160v160h64v-160h160z" />
<glyph glyph-name="ion-locked" unicode="&#xf200;"
d="M22 -32c-12 0 -22 10 -22 22v212c0 12 10 22 22 22h3h19v31c0 42 17 87 43 115s64 46 105 46v0v0c41 0 79 -18 105 -46s43 -73 43 -115v-31h22c12 0 22 -10 22 -22v-212c0 -12 -10 -22 -22 -22h-340zM97 255v-31h17h155h18v31c0 27 -10 61 -28 80v0v1
c-18 19 -42 29 -67 29v0v0c-25 0 -49 -10 -67 -29v-1v0c-18 -19 -28 -53 -28 -80z" />
</font>
</defs></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.2459 5.92925C15.5704 5.60478 15.5704 5.07872 15.2459 4.75425C14.9214 4.42978 14.3954 4.42978 14.0709 4.75425L10.0001 8.82508L5.92925 4.75425C5.60478 4.42978 5.07872 4.42978 4.75425 4.75425C4.42978 5.07872 4.42978 5.60478 4.75425 5.92925L8.82508 10.0001L4.75425 14.0709C4.42978 14.3954 4.42978 14.9214 4.75425 15.2459C5.07872 15.5704 5.60478 15.5704 5.92925 15.2459L10.0001 11.1751L14.0709 15.2459C14.3954 15.5704 14.9214 15.5704 15.2459 15.2459C15.5704 14.9214 15.5704 14.3954 15.2459 14.0709L11.1751 10.0001L15.2459 5.92925Z" />
</svg>

After

Width:  |  Height:  |  Size: 646 B

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;

View File

@@ -64,6 +64,9 @@ $heading-height: 75px;
&:disabled {
color: var(--sn-stylekit-editor-foreground-color);
}
&:focus {
box-shadow: none;
}
}
}
@@ -117,6 +120,7 @@ $heading-height: 75px;
&:focus {
outline: 0;
box-shadow: none;
}
}
}
@@ -140,7 +144,6 @@ $heading-height: 75px;
}
.editable {
font-family: monospace;
overflow-y: scroll;
width: 100%;
background-color: var(--sn-stylekit-editor-background-color);
@@ -182,3 +185,7 @@ $heading-height: 75px;
}
}
}
#note-text-editor:focus {
box-shadow: none;
}

View File

@@ -16,8 +16,7 @@
src: url('../fonts/ionicons.eot?v=2.0.0');
src: url('../fonts/ionicons.eot?v=2.0.1#iefix') format('embedded-opentype'),
url('../fonts/ionicons.ttf?v=2.0.1') format('truetype'),
url('../fonts/ionicons.woff?v=2.0.1') format('woff'),
url('../fonts/ionicons.svg?v=2.0.1#Ionicons') format('svg');
url('../fonts/ionicons.woff?v=2.0.1') format('woff');
font-weight: normal;
font-style: normal;
}

View File

@@ -94,6 +94,8 @@ a {
p {
overflow: auto;
color: var(--sn-stylekit-paragraph-text-color);
margin: 0;
}
.main-ui-view {

View File

@@ -42,55 +42,6 @@
}
}
#privileges-modal {
min-width: 400px;
max-width: 700px;
.sk-panel-header {
position: relative;
}
.close-button {
cursor: pointer;
position: absolute;
padding: 1.1rem 2rem;
right: 0;
}
table {
margin-bottom: 12px;
width: 100%;
overflow: auto;
border-collapse: collapse;
border-spacing: 0px;
border-color: var(--sn-stylekit-contrast-border-color);
background-color: var(--sn-stylekit-background-color);
color: var(--sn-stylekit-contrast-foreground-color);
th,
td {
padding: 6px 13px;
border: 1px solid var(--sn-stylekit-contrast-border-color);
}
tr:nth-child(2n) {
background-color: var(--sn-stylekit-contrast-background-color);
}
}
th {
text-align: center;
font-weight: normal;
}
.priv-header {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
#item-preview-modal {
> .sk-modal-content {
width: 800px;

View File

@@ -30,13 +30,14 @@
#notes-title-bar {
padding-top: 16px;
font-weight: normal;
font-size: var(--sn-stylekit-font-size-h1);
.section-title-bar-header .title {
font-size: var(--sn-stylekit-font-size-h3);
font-weight: 600;
width: calc(90% - 45px);
}
p {
font-size: var(--sn-stylekit-font-size-p);
}
}
#notes-menu-bar {
@@ -67,12 +68,6 @@
border-color: transparent;
width: 100%;
position: relative;
&:focus {
outline: 0;
border-color: var(--sn-stylekit-info-color);
border-width: 1px;
}
}
#search-clear-button {
@@ -86,7 +81,8 @@
line-height: 17px;
text-align: center;
position: absolute;
top: 8px;
top: 50%;
transform: translateY(-50%);
right: 8px;
transition: background-color 0.15s linear;
@@ -130,7 +126,7 @@
text-overflow: ellipsis;
}
> .date {
> .bottom-info {
font-size: 12px;
margin-top: 4px;
}

View File

@@ -20,11 +20,13 @@
}
[data-reach-dialog-content] {
width: auto;
padding: 0;
margin: 0;
position: relative;
overflow: unset;
flex-basis: 0;
min-width: 400px;
max-width: 600px;
}

View File

@@ -1,4 +1,7 @@
.sessions-modal {
min-width: 40vw;
width: auto;
h2, ul, p {
margin: 0;
padding: 0;

View File

@@ -0,0 +1,16 @@
/* Generic UI controls that have yet to be extracted into Stylekit */
.sn-btn {
@extend .border-0;
@extend .bg-main;
@extend .cursor-pointer;
@extend .capitalize;
@extend .font-bold;
@extend .py-2;
@extend .px-3;
@extend .rounded;
@extend .text-info-contrast;
@extend .text-sm;
@extend .hover\:brightness-130;
}

View File

@@ -49,6 +49,7 @@
.sk-panel {
.sk-panel-header {
.close-button {
border-radius: var(--sn-stylekit-general-border-radius);
&:hover {
text-decoration: none;
}
@@ -88,5 +89,15 @@ button.sk-a {
}
*:focus {
outline: solid var(--sn-stylekit-info-color) 2px;
outline: none;
box-shadow: 0 0 0 2px var(--sn-stylekit-info-color);
}
input:focus {
box-shadow: 0 0 0 1px var(--sn-stylekit-info-color);
}
.sk-button:focus, button:focus {
box-shadow: 0 0 0 2px var(--sn-stylekit-background-color),
0 0 0 4px var(--sn-stylekit-info-color);
}

View File

@@ -91,6 +91,7 @@
&:focus {
outline: 0;
box-shadow: 0;
}
pointer-events: none;

View File

@@ -57,35 +57,6 @@ $screen-md-max: ($screen-lg-min - 1) !default;
}
}
.selectable {
user-select: text !important;
cursor: text;
}
.mt-5 {
margin-top: 5px !important;
}
.mt-10 {
margin-top: 10px !important;
}
.mr-5 {
margin-right: 5px !important;
}
.mr-8 {
margin-right: 8px !important;
}
.faded {
opacity: 0.5;
}
.center-align {
text-align: center !important;
}
.block {
display: block !important;
}
@@ -94,19 +65,6 @@ $screen-md-max: ($screen-lg-min - 1) !default;
display: inline-block;
}
.wrap {
word-wrap: break-word;
word-break: break-all;
}
.medium-padding {
padding: 10px !important;
}
.bold {
font-weight: bold !important;
}
.normal {
font-weight: normal !important;
}
@@ -118,3 +76,151 @@ $screen-md-max: ($screen-lg-min - 1) !default;
.medium-text {
font-size: 14px !important;
}
.faded {
opacity: 0.5;
}
.flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.self-start {
align-self: flex-start;
}
.justify-self-start {
justify-self: flex-start;
}
.m-0 {
margin: 0;
}
.mt-1 {
margin-top: .25rem;
}
.mt-3 {
margin-top: .75rem;
}
.mt-5 {
margin-top: 1.25rem;
}
.p-0 {
padding: 0rem;
}
.p-5 {
padding: 1.25rem;
}
.px-3 {
padding-left: .75rem;
padding-right: .75rem;
}
.py-2 {
padding-top: .5rem;
padding-bottom: .5rem;
}
.border-0 {
border-width: 0px;
}
.rounded {
border-radius: var(--sn-stylekit-general-border-radius);
}
.rounded-md {
border-radius: 0.375rem;
}
.bg-main {
background-color: var(--sn-stylekit-info-color);
}
.bg-transparent {
background-color: transparent;
}
.capitalize {
text-transform: capitalize;
}
.col-start-1 {
grid-column-start: 1;
}
.col-start-2 {
grid-column-start: 2;
}
.col-end-3 {
grid-column-end: 3;
}
.color-neutral {
color: var(--sn-stylekit-neutral-color)
}
.hover\:color-info:hover {
color: var(--sn-stylekit-info-color)
}
.hover\:brightness-130:hover {
filter: brightness(130%);
}
.cursor-pointer {
cursor: pointer;
}
.fill-current {
fill: currentColor;
}
.font-editor {
font-family: var(--sn-stylekit-editor-font-family);
}
.font-semibold {
font-weight: 600 !important;
}
.font-bold {
font-weight: 700 !important;
}
.grid {
display: grid;
}
.grid-template-cols-1fr {
grid-template-columns: 1fr;
}
.relative {
position: relative;
}
.row-start-1 {
grid-row-start: 1;
}
.selectable {
user-select: text !important;
cursor: text;
}
.shadow-sm {
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.04), 0px 1px 4px 0px rgba(0, 0, 0, 0.12);
}
.text-sm {
font-size: var(--sn-stylekit-font-size-h5);;
}
.text-info-contrast {
color: var(--sn-stylekit-info-contrast-color);
}
.wrap {
word-wrap: break-word;
word-break: break-all;
}

View File

@@ -11,3 +11,4 @@
@import "ionicons";
@import "reach-sub";
@import "sessions-modal";
@import "sn";

View File

@@ -71,7 +71,7 @@
.sk-notification-title.sk-panel-row.padded-row Advanced Options
.bordered-row.padded-row
label.sk-label Sync Server Domain
input.sk-input.mt-5.sk-base(
input.sk-input.sk-base(
name='server',
ng-model='self.state.formData.url',
ng-change='self.onHostInputChange()'
@@ -156,18 +156,26 @@
) Change Password
a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openSessionsModal()"
ng-if="self.state.showSessions"
) Manage Sessions
a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openPrivilegesModal('')",
ng-show='self.state.user'
) Manage Privileges
.sk-panel-section
.sk-panel-section-title Encryption
.sk-panel-section-subtitle.info(ng-if='self.state.encryptionEnabled')
| {{self.encryptionStatusForNotes()}}
p.sk-p
| {{self.state.encryptionStatusString}}
.sk-panel-section(ng-if="self.hasProtections()")
.sk-panel-section-title Protections
.sk-panel-section-subtitle.info(ng-if="self.state.protectionsDisabledUntil")
| Protections are disabled until {{self.state.protectionsDisabledUntil}}
.sk-panel-section-subtitle.info(ng-if="!self.state.protectionsDisabledUntil")
| Protections are enabled
p.sk-p
| Actions like viewing protected notes, exporting decrypted backups,
| or revoking an active session, require additional authentication
| like entering your account password or application passcode.
.sk-panel-row(ng-if="self.state.protectionsDisabledUntil")
button.sk-button.info(ng-click="self.enableProtections()")
span.sk-label.capitalize Enable protections
.sk-panel-section
.sk-panel-section-title Passcode Lock
div(ng-if='!self.state.hasPasscode')
@@ -180,6 +188,8 @@
p.sk-p
| Add a passcode to lock the application and
| encrypt on-device key storage.
p(ng-if='self.state.keyStorageInfo')
| {{self.state.keyStorageInfo}}
div(ng-if='!self.state.canAddPasscode')
p.sk-p
| Adding a passcode is not supported in temporary sessions. Please sign
@@ -208,8 +218,7 @@
ng-click='self.state.formData.showPasscodeForm = false'
) Cancel
div(ng-if='self.state.hasPasscode && !self.state.formData.showPasscodeForm')
.sk-p
| Passcode lock is enabled.
.sk-panel-section-subtitle.info Passcode lock is enabled
.sk-notification.contrast
.sk-notification-title Options
.sk-notification-text
@@ -226,10 +235,6 @@
| {{option.label}}
.sk-p The autolock timer begins when the window or tab loses focus.
.sk-panel-row
a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openPrivilegesModal('')",
ng-show='!self.state.user'
) Manage Privileges
a.sk-a.info.sk-panel-row.condensed(
ng-click='self.changePasscodePressed()'
) Change Passcode
@@ -274,33 +279,22 @@
span(ng-if='self.isDesktopApplication()')
| Backups are automatically created on desktop and can be managed
| via the "Backups" top-level menu.
#import-password-request(ng-if='self.state.importData.requestPassword')
form.sk-panel-form.stretch(ng-submit='self.submitImportPassword()')
p Enter the account password associated with the import file.
input.sk-input.contrast.mt-5(
autofocus='true',
ng-model='self.state.importData.password',
placeholder='Enter File Account Password',
type='password'
)
.sk-button-group.stretch.sk-panel-row.form-submit
button.sk-button.info(type='submit')
.sk-label Decrypt & Import
p
| Importing from backup will not overwrite existing data,
| but instead create a duplicate of any differing data.
p
| If you'd like to import only a selection of items instead of
| the whole file, please use the Batch Manager extension.
.sk-panel-row
.sk-spinner.small.info(ng-if='self.state.importData.loading')
.sk-panel-section
.sk-panel-section-title Error Reporting
.sk-panel-section-subtitle.info
| Automatic error reporting is {{ self.state.errorReportingEnabled ? 'enabled.' : 'disabled.' }}
| Automatic error reporting is {{ self.state.errorReportingEnabled ? 'enabled' : 'disabled' }}
p.sk-p
| Help us improve Standard Notes by automatically submitting
| anonymized error reports.
p.sk-p.selectable(ng-if="self.state.errorReportingId")
| Your random identifier is
strong {{ self.state.errorReportingId }}
p.sk-p(ng-if="self.state.errorReportingId")
| Disabling error reporting will remove that identifier from your
| local storage, and a new identifier will be created should you
| decide to enable error reporting again in the future.
.sk-panel-row
button(ng-click="self.toggleErrorReportingEnabled()").sk-button.info
span.sk-label {{ self.state.errorReportingEnabled ? 'Disable' : 'Enable'}} Error Reporting

View File

@@ -14,12 +14,12 @@
| {{ctrl.permissionsString}}
.sk-panel-row
p.sk-p
| Extensions use an offline messaging system to communicate. Learn more at
| Extensions use an offline messaging system to communicate. Learn more at
a.sk-a.info(
href='https://standardnotes.org/permissions',
rel='noopener',
href='https://standardnotes.org/permissions',
rel='noopener',
target='_blank'
) https://standardnotes.org/permissions.
.sk-panel-footer
.sk-button.info.big.block.bold(ng-click='ctrl.accept()')
.sk-button.info.big.block.font-bold(ng-click='ctrl.accept()')
.sk-label Continue

View File

@@ -1,37 +0,0 @@
.sk-modal-background(ng-click="ctrl.cancel()")
#privileges-modal.sk-modal-content
.sn-component
.sk-panel
.sk-panel-header
.sk-panel-header-title Authentication Required
a.close-button.info(ng-click="ctrl.cancel()") Cancel
.sk-panel-content
.sk-panel-section
div(ng-repeat="credential in ctrl.requiredCredentials")
.sk-p.sk-bold.sk-panel-row
strong {{ctrl.promptForCredential(credential)}}
.sk-panel-row
input.sk-input.contrast(
ng-model="ctrl.authParameters[credential]"
should-focus="$index == 0"
sn-autofocus="true"
sn-enter="ctrl.submit()"
type="password"
)
.sk-panel-row
label.sk-label.danger(
ng-if="ctrl.isCredentialInFailureState(credential)"
) Invalid authentication. Please try again.
.sk-panel-row
.sk-panel-row
.sk-horizontal-group
.sk-p.sk-bold Remember For
a.sk-a.info(
ng-repeat="option in ctrl.sessionLengthOptions"
ng-class="{'boxed' : option.value == ctrl.selectedSessionLength}"
ng-click="ctrl.selectSessionLength(option.value)"
)
| {{option.label}}
.sk-panel-footer.extra-padding
.sk-button.info.big.block.bold(ng-click="ctrl.submit()")
.sk-label Submit

View File

@@ -1,51 +0,0 @@
.sk-modal-background(ng-click='ctrl.cancel()')
#privileges-modal.sk-modal-content
.sn-component
.sk-panel
.sk-panel-header
.sk-panel-header-title Manage Privileges
a.sk-a.close-button.info(ng-click='ctrl.cancel()') Done
.sk-panel-content
.sk-panel-section
table.sk-table
thead
tr
th
th(ng-repeat='cred in ctrl.availableCredentials')
.priv-header
strong {{ctrl.credentialDisplayInfo[cred].label}}
.sk-p.font-small(
ng-show='!ctrl.credentialDisplayInfo[cred].availability',
style='margin-top: 2px'
) Not Configured
tbody
tr(ng-repeat='action in ctrl.availableActions')
td
.sk-p {{ctrl.displayInfoForAction(action)}}
th(ng-repeat='credential in ctrl.availableCredentials')
input(
ng-checked='ctrl.isCredentialRequiredForAction(action, credential)',
ng-click='ctrl.checkboxValueChanged(action, credential)',
ng-disabled='!ctrl.credentialDisplayInfo[credential].availability',
type='checkbox'
)
.sk-panel-section(ng-if='ctrl.sessionExpirey && !ctrl.sessionExpired')
.sk-p.sk-panel-row
| You will not be asked to authenticate until {{ctrl.sessionExpirey}}.
a.sk-a.sk-panel-row.info(ng-click='ctrl.clearSession()') Clear Session
.sk-panel-footer
.sk-h2.sk-bold About Privileges
.sk-panel-section.no-bottom-pad
.sk-panel-row
.text-content
.sk-p
| Privileges represent interface level authentication for accessing
| certain items and features. Note that when your application is unlocked,
| your data exists in temporary memory in an unencrypted state.
| Privileges are meant to protect against unwanted access in the event of
| an unlocked application, but do not affect data encryption state.
p.sk-p
| Privileges sync across your other devices; however, note that if you
| require an "Application Passcode" privilege, and another device does not have
| an application passcode set up, the application passcode requirement will be ignored
| on that device.

View File

@@ -1,6 +1,6 @@
{
"name": "standard-notes-web",
"version": "3.5.19",
"version": "3.6.0-beta01",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
@@ -8,7 +8,7 @@
},
"scripts": {
"setup": "yarn submodules && yarn install",
"start": "webpack-dev-server --progress --config webpack.dev.js",
"start": "webpack-dev-server --config webpack.dev.js",
"watch": "webpack -w --config webpack.dev.js",
"watch:desktop": "webpack -w --config webpack.dev.js --env.platform='desktop'",
"bundle": "webpack --config webpack.prod.js && yarn tsc",
@@ -16,64 +16,60 @@
"bundle:desktop:beta": "webpack --config webpack.prod.js --env.platform='desktop' --env.public_beta='true'",
"build": "bundle install && yarn install --pure-lockfile && bundle exec rails assets:precompile && yarn bundle",
"submodules": "git submodule update --init --force",
"lint": "eslint --fix app/assets/javascripts/**/*.js",
"lint": "eslint --fix app/assets/javascripts",
"tsc": "tsc --project app/assets/javascripts/tsconfig.json"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/plugin-transform-react-jsx": "^7.12.10",
"@babel/preset-env": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/core": "^7.12.16",
"@babel/plugin-transform-react-jsx": "^7.12.16",
"@babel/preset-env": "^7.12.16",
"@babel/preset-typescript": "^7.12.16",
"@svgr/webpack": "^5.5.0",
"@types/angular": "^1.8.0",
"@types/chai": "^4.2.11",
"@types/lodash": "^4.14.149",
"@types/mocha": "^7.0.2",
"@types/lodash": "^4.14.168",
"@types/pug": "^2.0.4",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^2.23.0",
"@typescript-eslint/parser": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"angular": "^1.8.2",
"apply-loader": "^2.0.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-plugin-angularjs-annotate": "^0.10.0",
"chai": "^4.2.0",
"connect": "^3.7.0",
"css-loader": "^3.4.2",
"dotenv": "^8.2.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint": "^7.18.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"file-loader": "^5.1.0",
"html-webpack-plugin": "^4.3.0",
"lodash": "^4.17.19",
"lodash": "^4.17.20",
"mini-css-extract-plugin": "^0.9.0",
"mocha": "^7.1.0",
"ng-cache-loader": "0.0.26",
"node-sass": "^4.14.1",
"pug": "^2.0.4",
"pug-loader": "^2.4.0",
"sass-loader": "^8.0.2",
"serve-static": "^1.14.1",
"sn-stylekit": "2.1.0",
"ts-loader": "^8.0.12",
"typescript": "^4.1.3",
"sn-stylekit": "^2.2.0",
"ts-loader": "^8.0.17",
"typescript": "^4.1.5",
"typescript-eslint": "0.0.1-alpha.0",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^4.2.2"
},
"dependencies": {
"@bugsnag/js": "^7.5.1",
"@reach/alert": "^0.12.1",
"@reach/alert-dialog": "^0.12.1",
"@reach/dialog": "^0.12.1",
"@standardnotes/sncrypto-web": "^1.2.9",
"@standardnotes/snjs": "^2.0.38",
"babel-loader": "^8.2.2",
"mobx": "^6.0.4",
"preact": "^10.5.7"
"@bugsnag/js": "^7.6.0",
"@reach/alert": "^0.13.0",
"@reach/alert-dialog": "^0.13.0",
"@reach/dialog": "^0.13.0",
"@standardnotes/sncrypto-web": "^1.2.10",
"@standardnotes/snjs": "^2.0.61",
"mobx": "^6.1.6",
"preact": "^10.5.12"
}
}

View File

@@ -71,7 +71,11 @@ module.exports = (
],
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
test: /\.svg$/,
use: ['@svgr/webpack'],
},
{
test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'file-loader',

2574
yarn.lock

File diff suppressed because it is too large Load Diff