Merge branch 'release/3.5.15'

This commit is contained in:
Baptiste Grob
2021-01-13 12:11:16 +01:00
12 changed files with 123 additions and 78 deletions

View File

@@ -1,11 +1,16 @@
{ {
"extends": ["eslint:recommended", "semistandard", "prettier"], "extends": ["eslint:recommended", "prettier"],
"parser": "babel-eslint", "parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./app/assets/javascripts/tsconfig.json"
},
"rules": { "rules": {
"standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals "standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals
"no-throw-literal": 0, "no-throw-literal": 0,
// "no-console": "error", "no-console": "off",
"semi": 1 "semi": 1,
"camelcase": "warn",
"sort-imports": "off"
}, },
"env": { "env": {
"browser": true "browser": true

View File

@@ -59,10 +59,35 @@ import { StartApplication } from './startApplication';
import { Bridge } from './services/bridge'; import { Bridge } from './services/bridge';
import { SessionsModalDirective } from './directives/views/sessionsModal'; import { SessionsModalDirective } from './directives/views/sessionsModal';
function reloadHiddenFirefoxTab(): boolean {
/**
* For Firefox pinned tab issue:
* When a new browser session is started, and SN is in a pinned tab,
* SN exhibits strange behavior until the tab is reloaded.
*/
if (
document.hidden &&
navigator.userAgent.toLowerCase().includes('firefox') &&
!localStorage.getItem('reloading')
) {
localStorage.setItem('reloading', 'true');
location.reload();
return true;
} else {
localStorage.removeItem('reloading');
return false;
}
}
const startApplication: StartApplication = async function startApplication( const startApplication: StartApplication = async function startApplication(
defaultSyncServerHost: string, defaultSyncServerHost: string,
bridge: Bridge bridge: Bridge
) { ) {
if (reloadHiddenFirefoxTab()) {
return;
}
SNLog.onLog = console.log; SNLog.onLog = console.log;
startErrorReporting(); startErrorReporting();

View File

@@ -68,7 +68,6 @@ type AccountMenuState = {
errorReportingEnabled: boolean; errorReportingEnabled: boolean;
syncInProgress: boolean; syncInProgress: boolean;
syncError: string; syncError: string;
syncPercentage: string;
showSessions: boolean; showSessions: boolean;
} }
@@ -101,7 +100,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
}, },
mutable: {}, mutable: {},
showBetaWarning: false, showBetaWarning: false,
errorReportingEnabled: !storage.get(StorageKey.DisableErrorReporting), errorReportingEnabled: storage.get(StorageKey.DisableErrorReporting) === false,
showSessions: false, showSessions: false,
} as AccountMenuState; } as AccountMenuState;
} }
@@ -143,7 +142,6 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
this.setState({ this.setState({
syncInProgress: sync.inProgress, syncInProgress: sync.inProgress,
syncError: sync.errorMessage, syncError: sync.errorMessage,
syncPercentage: sync.humanReadablePercentage,
}); });
}) })
this.removeBetaWarningListener = autorun(() => { this.removeBetaWarningListener = autorun(() => {
@@ -636,7 +634,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
} else { } else {
storage.set(StorageKey.DisableErrorReporting, false); storage.set(StorageKey.DisableErrorReporting, false);
} }
if (!this.application.getSyncStatus().syncInProgress) { if (!this.state.syncInProgress) {
window.location.reload(); window.location.reload();
} }
} }

View File

@@ -1,6 +1,11 @@
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { PureViewCtrl } from '@/views'; import { PureViewCtrl } from '@/views';
import { SNApplication, RemoteSession, UuidString } from '@standardnotes/snjs'; import {
SNApplication,
RemoteSession,
SessionStrings,
UuidString,
} from '@standardnotes/snjs';
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx'; import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
import { render, FunctionComponent } from 'preact'; import { render, FunctionComponent } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks'; import { useState, useEffect, useRef } from 'preact/hooks';
@@ -16,16 +21,20 @@ function useAutorun(view: (r: IReactionPublic) => any, opts?: IAutorunOptions) {
useEffect(() => autorun(view, opts), []); useEffect(() => autorun(view, opts), []);
} }
type Session = RemoteSession & {
revoking?: true;
};
function useSessions( function useSessions(
application: SNApplication application: SNApplication
): [ ): [
RemoteSession[], Session[],
() => void, () => void,
boolean, boolean,
(uuid: UuidString) => Promise<void>, (uuid: UuidString) => Promise<void>,
string string
] { ] {
const [sessions, setSessions] = useState<RemoteSession[]>([]); const [sessions, setSessions] = useState<Session[]>([]);
const [lastRefreshDate, setLastRefreshDate] = useState(Date.now()); const [lastRefreshDate, setLastRefreshDate] = useState(Date.now());
const [refreshing, setRefreshing] = useState(true); const [refreshing, setRefreshing] = useState(true);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
@@ -41,7 +50,7 @@ function useSessions(
setErrorMessage('An unknown error occured while loading sessions.'); setErrorMessage('An unknown error occured while loading sessions.');
} }
} else { } else {
const sessions = response as RemoteSession[]; const sessions = response as Session[];
setSessions(sessions); setSessions(sessions);
setErrorMessage(''); setErrorMessage('');
} }
@@ -54,9 +63,21 @@ function useSessions(
} }
async function revokeSession(uuid: UuidString) { async function revokeSession(uuid: UuidString) {
const responsePromise = application.revokeSession(uuid);
let sessionsBeforeRevoke = sessions; let sessionsBeforeRevoke = sessions;
setSessions(sessions.filter((session) => session.uuid !== uuid));
const response = await application.revokeSession(uuid); const sessionsDuringRevoke = sessions.slice();
const toRemoveIndex = sessions.findIndex(
(session) => session.uuid === uuid
);
sessionsDuringRevoke[toRemoveIndex] = {
...sessionsDuringRevoke[toRemoveIndex],
revoking: true,
};
setSessions(sessionsDuringRevoke);
const response = await responsePromise;
if ('error' in response) { if ('error' in response) {
if (response.error?.message) { if (response.error?.message) {
setErrorMessage(response.error?.message); setErrorMessage(response.error?.message);
@@ -64,6 +85,8 @@ function useSessions(
setErrorMessage('An unknown error occured while revoking the session.'); setErrorMessage('An unknown error occured while revoking the session.');
} }
setSessions(sessionsBeforeRevoke); setSessions(sessionsBeforeRevoke);
} else {
setSessions(sessions.filter((session) => session.uuid !== uuid));
} }
} }
@@ -84,7 +107,7 @@ const SessionsModal: FunctionComponent<{
errorMessage, errorMessage,
] = useSessions(application); ] = useSessions(application);
const [revokingSessionUuid, setRevokingSessionUuid] = useState(''); const [confirmRevokingSessionUuid, setRevokingSessionUuid] = useState('');
const closeRevokeSessionAlert = () => setRevokingSessionUuid(''); const closeRevokeSessionAlert = () => setRevokingSessionUuid('');
const cancelRevokeRef = useRef<HTMLButtonElement>(); const cancelRevokeRef = useRef<HTMLButtonElement>();
@@ -146,6 +169,7 @@ const SessionsModal: FunctionComponent<{
</p> </p>
<button <button
className="sk-button danger sk-label" className="sk-button danger sk-label"
disabled={session.revoking}
onClick={() => onClick={() =>
setRevokingSessionUuid(session.uuid) setRevokingSessionUuid(session.uuid)
} }
@@ -165,7 +189,7 @@ const SessionsModal: FunctionComponent<{
</div> </div>
</div> </div>
</Dialog> </Dialog>
{revokingSessionUuid && ( {confirmRevokingSessionUuid && (
<AlertDialog leastDestructiveRef={cancelRevokeRef}> <AlertDialog leastDestructiveRef={cancelRevokeRef}>
<div className="sk-modal-content"> <div className="sk-modal-content">
<div className="sn-component"> <div className="sn-component">
@@ -173,14 +197,10 @@ const SessionsModal: FunctionComponent<{
<div className="sk-panel-content"> <div className="sk-panel-content">
<div className="sk-panel-section"> <div className="sk-panel-section">
<AlertDialogLabel className="sk-h3 sk-panel-section-title"> <AlertDialogLabel className="sk-h3 sk-panel-section-title">
Revoke this session? {SessionStrings.RevokeTitle}
</AlertDialogLabel> </AlertDialogLabel>
<AlertDialogDescription className="sk-panel-row"> <AlertDialogDescription className="sk-panel-row">
<p> <p>{SessionStrings.RevokeText}</p>
The associated app will be signed out and all data
removed from the device when it is next launched. You
can sign back in on that device at any time.
</p>
</AlertDialogDescription> </AlertDialogDescription>
<div className="sk-panel-row"> <div className="sk-panel-row">
<div className="sk-button-group"> <div className="sk-button-group">
@@ -189,16 +209,16 @@ const SessionsModal: FunctionComponent<{
ref={cancelRevokeRef} ref={cancelRevokeRef}
onClick={closeRevokeSessionAlert} onClick={closeRevokeSessionAlert}
> >
<span>Cancel</span> <span>{SessionStrings.RevokeCancelButton}</span>
</button> </button>
<button <button
className="sk-button danger sk-label" className="sk-button danger sk-label"
onClick={() => { onClick={() => {
closeRevokeSessionAlert(); closeRevokeSessionAlert();
revokeSession(revokingSessionUuid); revokeSession(confirmRevokingSessionUuid);
}} }}
> >
<span>Revoke</span> <span>{SessionStrings.RevokeConfirmButton}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote } from '@standardnotes/snjs'; import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote, BackupFile } from '@standardnotes/snjs';
function zippableTxtName(name: string, suffix = ""): string { function zippableTxtName(name: string, suffix = ""): string {
const sanitizedName = name const sanitizedName = name
@@ -22,22 +22,27 @@ export class ArchiveManager {
} }
public async downloadBackup(encrypted: boolean) { public async downloadBackup(encrypted: boolean) {
const items = this.application.allItems();
const run = async () => { const run = async () => {
// download in Standard Notes format
const intent = encrypted const intent = encrypted
? EncryptionIntent.FileEncrypted ? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted; : EncryptionIntent.FileDecrypted;
const data = await this.application.createBackupFile(intent);
if (!data) {
return;
}
const blobData = new Blob(
[JSON.stringify(data, null, 2)],
{ type: 'text/json' }
);
if (encrypted) { if (encrypted) {
const data = await this.itemsData(items, intent);
this.downloadData( this.downloadData(
data!, blobData,
`Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt` `Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt`
); );
} else { } else {
/** download as zipped plain text files */ /** download as zipped plain text files */
this.downloadZippedItems(items); this.downloadZippedDecryptedItems(data);
} }
}; };
@@ -65,15 +70,6 @@ export class ArchiveManager {
return string; return string;
} }
private async itemsData(items: SNItem[], intent: EncryptionIntent) {
const data = await this.application.createBackupFile(items, intent);
if (!data) {
return undefined;
}
const blobData = new Blob([data], { type: 'text/json' });
return blobData;
}
private get zip() { private get zip() {
return (window as any).zip; return (window as any).zip;
} }
@@ -95,17 +91,19 @@ export class ArchiveManager {
}); });
} }
private async downloadZippedItems( private async downloadZippedDecryptedItems(
items: SNItem[] data: BackupFile
) { ) {
await this.loadZip(); await this.loadZip();
const items = data.items;
this.zip.createWriter( this.zip.createWriter(
new this.zip.BlobWriter('application/zip'), new this.zip.BlobWriter('application/zip'),
async (zipWriter: any) => { async (zipWriter: any) => {
const data = await this.application.createBackupFile(items, EncryptionIntent.FileDecrypted);
await new Promise((resolve) => { await new Promise((resolve) => {
const blob = new Blob([data!], { type: 'text/plain' }); const blob = new Blob(
[JSON.stringify(data, null, 2)],
{ type: 'text/plain' }
);
const fileName = zippableTxtName( const fileName = zippableTxtName(
'Standard Notes Backup and Import File.txt' 'Standard Notes Backup and Import File.txt'
); );

View File

@@ -9,8 +9,8 @@ import { Bridge } from './bridge';
type UpdateObserverCallback = (component: SNComponent) => void type UpdateObserverCallback = (component: SNComponent) => void
type ComponentActivationCallback = (payload: PurePayload) => void type ComponentActivationCallback = (payload: PurePayload) => void
type ComponentActivationObserver = { type ComponentActivationObserver = {
id: string, id: string;
callback: ComponentActivationCallback callback: ComponentActivationCallback;
} }
export class DesktopManager extends ApplicationService { export class DesktopManager extends ApplicationService {
@@ -19,8 +19,9 @@ export class DesktopManager extends ApplicationService {
$timeout: ng.ITimeoutService $timeout: ng.ITimeoutService
componentActivationObservers: ComponentActivationObserver[] = [] componentActivationObservers: ComponentActivationObserver[] = []
updateObservers: { updateObservers: {
callback: UpdateObserverCallback callback: UpdateObserverCallback;
}[] = [] }[] = [];
isDesktop = isDesktopApplication(); isDesktop = isDesktopApplication();
dataLoaded = false dataLoaded = false
@@ -156,7 +157,7 @@ export class DesktopManager extends ApplicationService {
undefined undefined
); );
} }
}) });
this.$timeout(() => { this.$timeout(() => {
for (const observer of this.updateObservers) { for (const observer of this.updateObservers) {
@@ -187,12 +188,11 @@ export class DesktopManager extends ApplicationService {
}); });
} }
desktop_requestBackupFile() { async desktop_requestBackupFile() {
return this.application!.createBackupFile( const data = this.application!.createBackupFile(EncryptionIntent.FileEncrypted);
undefined, if (data) {
undefined, return JSON.stringify(data, null, 2);
true }
);
} }
desktop_didBeginBackup() { desktop_didBeginBackup() {

View File

@@ -1,4 +1,4 @@
import { SNLog } from '@standardnotes/snjs'; import { isNullOrUndefined, SNLog } from '@standardnotes/snjs';
import { isDesktopApplication, isDev } from '@/utils'; import { isDesktopApplication, isDev } from '@/utils';
import { storage, StorageKey } from './localStorage'; import { storage, StorageKey } from './localStorage';
import Bugsnag from '@bugsnag/js'; import Bugsnag from '@bugsnag/js';
@@ -6,6 +6,7 @@ import Bugsnag from '@bugsnag/js';
declare const __VERSION__: string; declare const __VERSION__: string;
declare global { declare global {
interface Window { interface Window {
// eslint-disable-next-line camelcase
_bugsnag_api_key?: string; _bugsnag_api_key?: string;
} }
} }
@@ -21,8 +22,15 @@ function redactFilePath(line: string): string {
} }
export function startErrorReporting() { export function startErrorReporting() {
const disableErrorReporting = storage.get(StorageKey.DisableErrorReporting);
if ( if (
storage.get(StorageKey.DisableErrorReporting) || /**
* Error reporting used to be opt-out, but is now opt-in, so
* treat the absence of an error reporting preference as an indication
* to disable error reporting.
*/
isNullOrUndefined(disableErrorReporting) ||
disableErrorReporting ||
!window._bugsnag_api_key !window._bugsnag_api_key
) { ) {
SNLog.onError = console.error; SNLog.onError = console.error;

View File

@@ -15,9 +15,9 @@ import {
} from '@/strings'; } from '@/strings';
type InputValue = { type InputValue = {
prompt: ChallengePrompt prompt: ChallengePrompt;
value: string value: string;
invalid: boolean invalid: boolean;
} }
type Values = Record<number, InputValue> type Values = Record<number, InputValue>

View File

@@ -25,6 +25,7 @@
position: relative; position: relative;
overflow: unset; overflow: unset;
flex-basis: 0; flex-basis: 0;
max-width: 600px;
} }
[data-reach-dialog-content] .sk-modal-content, [data-reach-dialog-content] .sk-modal-content,

View File

@@ -150,16 +150,6 @@
.sk-panel-column .sk-panel-column
.sk-h1.sk-bold.wrap {{self.state.user.email}} .sk-h1.sk-bold.wrap {{self.state.user.email}}
.sk-subtitle.subtle.normal {{self.state.server}} .sk-subtitle.subtle.normal {{self.state.server}}
.sk-horizontal-group(
delay='1000',
delay-hide='true',
show='self.state.syncInProgress'
)
.sk-spinner.small.info
.sk-sublabel
| Syncing
span(ng-if='self.state.syncPercentage')
| ({{self.state.syncPercentage}})
.sk-panel-row .sk-panel-row
a.sk-a.info.sk-panel-row.condensed( a.sk-a.info.sk-panel-row.condensed(
ng-click="self.openPasswordWizard()" ng-click="self.openPasswordWizard()"

View File

@@ -1,6 +1,6 @@
{ {
"name": "standard-notes-web", "name": "standard-notes-web",
"version": "3.5.14", "version": "3.5.15",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -71,7 +71,7 @@
"@reach/alert-dialog": "^0.12.1", "@reach/alert-dialog": "^0.12.1",
"@reach/dialog": "^0.12.1", "@reach/dialog": "^0.12.1",
"@standardnotes/sncrypto-web": "^1.2.9", "@standardnotes/sncrypto-web": "^1.2.9",
"@standardnotes/snjs": "^2.0.32", "@standardnotes/snjs": "^2.0.38",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"mobx": "^6.0.4", "mobx": "^6.0.4",
"preact": "^10.5.7" "preact": "^10.5.7"

View File

@@ -1045,10 +1045,10 @@
"@standardnotes/sncrypto-common" "^1.2.7" "@standardnotes/sncrypto-common" "^1.2.7"
libsodium-wrappers "^0.7.8" libsodium-wrappers "^0.7.8"
"@standardnotes/snjs@^2.0.32": "@standardnotes/snjs@^2.0.38":
version "2.0.32" version "2.0.38"
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.0.32.tgz#7742e3cb08ee560b5b7cf06e526ebc19ff2a81ba" resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.0.38.tgz#3edbdc6c04926f2f4940aea8ebb6c7007f6d4235"
integrity sha512-SnbOqGwx5H59HjFSzBKT/1Wh2zAAcx+QSQeYDyKjZKNYN0g8JvirzL0FpoLTxa15BRTl2mG073eIyFsEtx8LFA== integrity sha512-cr+jdtDOyrwKlad/SRCy/D/cIsEQdShEsuazSIajpvXhwo9ixkj/wv/o0r3GrHug6CgXLOQRgzuZ9eZvi0PM9g==
dependencies: dependencies:
"@standardnotes/sncrypto-common" "^1.2.9" "@standardnotes/sncrypto-common" "^1.2.9"