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:
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);
|
||||
258
app/assets/javascripts/components/SessionsModal.tsx
Normal file
258
app/assets/javascripts/components/SessionsModal.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import {
|
||||
SNApplication,
|
||||
SessionStrings,
|
||||
UuidString,
|
||||
isNullOrUndefined,
|
||||
RemoteSession,
|
||||
} from '@standardnotes/snjs';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useState, useEffect, useRef, useMemo } from 'preact/hooks';
|
||||
import { Dialog } from '@reach/dialog';
|
||||
import { Alert } from '@reach/alert';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@reach/alert-dialog';
|
||||
import { toDirective, useAutorun } from './utils';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
type Session = RemoteSession & {
|
||||
revoking?: true;
|
||||
};
|
||||
|
||||
function useSessions(
|
||||
application: SNApplication
|
||||
): [
|
||||
Session[],
|
||||
() => void,
|
||||
boolean,
|
||||
(uuid: UuidString) => Promise<void>,
|
||||
string
|
||||
] {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [lastRefreshDate, setLastRefreshDate] = useState(Date.now());
|
||||
const [refreshing, setRefreshing] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setRefreshing(true);
|
||||
const response = await application.getSessions();
|
||||
if ('error' in response) {
|
||||
if (response.error?.message) {
|
||||
setErrorMessage(response.error.message);
|
||||
} else {
|
||||
setErrorMessage('An unknown error occured while loading sessions.');
|
||||
}
|
||||
} else {
|
||||
const sessions = response as Session[];
|
||||
setSessions(sessions);
|
||||
setErrorMessage('');
|
||||
}
|
||||
setRefreshing(false);
|
||||
})();
|
||||
}, [application, lastRefreshDate]);
|
||||
|
||||
function refresh() {
|
||||
setLastRefreshDate(Date.now());
|
||||
}
|
||||
|
||||
async function revokeSession(uuid: UuidString) {
|
||||
const sessionsBeforeRevoke = sessions;
|
||||
|
||||
const responsePromise = 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 (isNullOrUndefined(response)) {
|
||||
setSessions(sessionsBeforeRevoke);
|
||||
} else if ('error' in response) {
|
||||
if (response.error?.message) {
|
||||
setErrorMessage(response.error?.message);
|
||||
} else {
|
||||
setErrorMessage('An unknown error occured while revoking the session.');
|
||||
}
|
||||
setSessions(sessionsBeforeRevoke);
|
||||
} else {
|
||||
setSessions(sessions.filter((session) => session.uuid !== uuid));
|
||||
}
|
||||
}
|
||||
|
||||
return [sessions, refresh, refreshing, revokeSession, errorMessage];
|
||||
}
|
||||
|
||||
const SessionsModal: FunctionComponent<{
|
||||
appState: AppState;
|
||||
application: SNApplication;
|
||||
}> = ({ appState, application }) => {
|
||||
const close = () => appState.closeSessionsModal();
|
||||
|
||||
const [
|
||||
sessions,
|
||||
refresh,
|
||||
refreshing,
|
||||
revokeSession,
|
||||
errorMessage,
|
||||
] = useSessions(application);
|
||||
|
||||
const [confirmRevokingSessionUuid, setRevokingSessionUuid] = useState('');
|
||||
const closeRevokeSessionAlert = () => setRevokingSessionUuid('');
|
||||
const cancelRevokeRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const formatter = useMemo(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog onDismiss={close} className="sessions-modal">
|
||||
<div className="sk-modal-content">
|
||||
<div class="sn-component">
|
||||
<div class="sk-panel">
|
||||
<div class="sk-panel-header">
|
||||
<div class="sk-panel-header-title">Active Sessions</div>
|
||||
<div className="buttons">
|
||||
<button
|
||||
class="sk-a close-button info"
|
||||
disabled={refreshing}
|
||||
onClick={refresh}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="sk-a close-button info" onClick={close}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sk-panel-content">
|
||||
{refreshing ? (
|
||||
<>
|
||||
<div class="sk-spinner small info"></div>
|
||||
<h2 className="sk-p sessions-modal-refreshing">
|
||||
Loading sessions
|
||||
</h2>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{errorMessage && (
|
||||
<Alert className="sk-p bold">{errorMessage}</Alert>
|
||||
)}
|
||||
{sessions.length > 0 && (
|
||||
<ul>
|
||||
{sessions.map((session) => (
|
||||
<li>
|
||||
<h2>{session.device_info}</h2>
|
||||
{session.current ? (
|
||||
<span className="info bold">Current session</span>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
Signed in on{' '}
|
||||
{formatter.format(session.updated_at)}
|
||||
</p>
|
||||
<button
|
||||
className="sk-button danger sk-label"
|
||||
disabled={session.revoking}
|
||||
onClick={() =>
|
||||
setRevokingSessionUuid(session.uuid)
|
||||
}
|
||||
>
|
||||
<span>Revoke</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
{confirmRevokingSessionUuid && (
|
||||
<AlertDialog
|
||||
onDismiss={() => {
|
||||
setRevokingSessionUuid('');
|
||||
}}
|
||||
leastDestructiveRef={cancelRevokeRef}
|
||||
>
|
||||
<div className="sk-modal-content">
|
||||
<div className="sn-component">
|
||||
<div className="sk-panel">
|
||||
<div className="sk-panel-content">
|
||||
<div className="sk-panel-section">
|
||||
<AlertDialogLabel className="sk-h3 sk-panel-section-title">
|
||||
{SessionStrings.RevokeTitle}
|
||||
</AlertDialogLabel>
|
||||
<AlertDialogDescription className="sk-panel-row">
|
||||
<p>{SessionStrings.RevokeText}</p>
|
||||
</AlertDialogDescription>
|
||||
<div className="sk-panel-row">
|
||||
<div className="sk-button-group">
|
||||
<button
|
||||
className="sk-button neutral sk-label"
|
||||
ref={cancelRevokeRef}
|
||||
onClick={closeRevokeSessionAlert}
|
||||
>
|
||||
<span>{SessionStrings.RevokeCancelButton}</span>
|
||||
</button>
|
||||
<button
|
||||
className="sk-button danger sk-label"
|
||||
onClick={() => {
|
||||
closeRevokeSessionAlert();
|
||||
revokeSession(confirmRevokingSessionUuid);
|
||||
}}
|
||||
>
|
||||
<span>{SessionStrings.RevokeConfirmButton}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Sessions: FunctionComponent<{
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
}> = ({ appState, application }) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
useAutorun(() => setShowModal(appState.isSessionsModalVisible));
|
||||
|
||||
if (showModal) {
|
||||
return <SessionsModal application={application} appState={appState} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
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: '=',
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user