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:
@@ -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"
|
||||
},
|
||||
|
||||
5
.github/workflows/dev.yml
vendored
5
.github/workflows/dev.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/pr.yml
vendored
9
.github/workflows/pr.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/prod.yml
vendored
5
.github/workflows/prod.yml
vendored
@@ -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
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@@ -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
|
||||
|
||||
|
||||
12
Gemfile.lock
12
Gemfile.lock
@@ -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)
|
||||
|
||||
@@ -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=""
|
||||
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=""
|
||||
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=""
|
||||
d="M384 224v-64h-160v-160h-64v160h-160v64h160v160h64v-160h160z" />
|
||||
<glyph glyph-name="ion-locked" unicode=""
|
||||
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 |
3
app/assets/icons/ic_close.svg
Normal file
3
app/assets/icons/ic_close.svg
Normal 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 |
3
app/assets/javascripts/@types/modules.ts
Normal file
3
app/assets/javascripts/@types/modules.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module '*.svg' {
|
||||
export default function SvgComponent(props: React.SVGProps<SVGSVGElement>): JSX.Element;
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
39
app/assets/javascripts/components/NoAccountWarning.tsx
Normal file
39
app/assets/javascripts/components/NoAccountWarning.tsx
Normal 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);
|
||||
@@ -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);
|
||||
56
app/assets/javascripts/components/utils.ts
Normal file
56
app/assets/javascripts/components/utils.ts
Normal 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: '=',
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
3
app/assets/javascripts/crypto.ts
Normal file
3
app/assets/javascripts/crypto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { SNWebCrypto } from "@standardnotes/sncrypto-web";
|
||||
|
||||
export const WebCrypto = new SNWebCrypto();
|
||||
@@ -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;
|
||||
|
||||
@@ -22,7 +22,7 @@ export function clickOutside($document: ng.IDocumentService) {
|
||||
$scope.$apply(attrs.clickOutside);
|
||||
didApplyClickOutside = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
attrs.clickOutside = undefined;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -7,7 +7,7 @@ const DEFAULT_CONTINUE_TITLE = "Continue";
|
||||
enum Steps {
|
||||
PasswordStep = 1,
|
||||
FinishStep = 2
|
||||
};
|
||||
}
|
||||
|
||||
type FormData = {
|
||||
currentPassword?: string,
|
||||
|
||||
@@ -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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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/*"],
|
||||
"@/*": ["./*"],
|
||||
|
||||
2
app/assets/javascripts/typings/pug.d.ts
vendored
2
app/assets/javascripts/typings/pug.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
declare module "*.pug" {
|
||||
import { compileTemplate } from 'pug'
|
||||
import { compileTemplate } from 'pug';
|
||||
const content: compileTemplate;
|
||||
export default content;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -68,7 +68,7 @@ export class EditorGroup {
|
||||
}
|
||||
return () => {
|
||||
removeFromArray(this.changeObservers, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private notifyObservers() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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)"
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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
|
||||
@@ -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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
409
app/assets/javascripts/views/challenge_modal/challenge_modal.tsx
Normal file
409
app/assets/javascripts/views/challenge_modal/challenge_modal.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
));
|
||||
}
|
||||
@@ -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()',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]}}
|
||||
|
||||
@@ -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(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ a {
|
||||
|
||||
p {
|
||||
overflow: auto;
|
||||
color: var(--sn-stylekit-paragraph-text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-ui-view {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.sessions-modal {
|
||||
min-width: 40vw;
|
||||
width: auto;
|
||||
|
||||
h2, ul, p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
16
app/assets/stylesheets/_sn.scss
Normal file
16
app/assets/stylesheets/_sn.scss
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,3 +11,4 @@
|
||||
@import "ionicons";
|
||||
@import "reach-sub";
|
||||
@import "sessions-modal";
|
||||
@import "sn";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
1
app/extensions/extensions-manager
Submodule
1
app/extensions/extensions-manager
Submodule
Submodule app/extensions/extensions-manager added at 206c1da0a4
62
package.json
62
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
1
vendor/extensions/extensions-manager
vendored
Submodule
1
vendor/extensions/extensions-manager
vendored
Submodule
Submodule vendor/extensions/extensions-manager added at 206c1da0a4
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user