Skip to content

Commit

Permalink
feat: (wip) sessions management
Browse files Browse the repository at this point in the history
  • Loading branch information
arielsvg committed Dec 16, 2020
1 parent da6858f commit 2c0f215
Show file tree
Hide file tree
Showing 20 changed files with 784 additions and 131 deletions.
7 changes: 5 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"angularjs-annotate"
"angularjs-annotate",
["@babel/plugin-transform-react-jsx", {
"pragma": "h",
"pragmaFrag": "Fragment"
}]
]
}
5 changes: 3 additions & 2 deletions app/assets/javascripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ import { trusted } from './filters';
import { isDev } from './utils';
import { BrowserBridge } from './services/browserBridge';
import { startErrorReporting } from './services/errorReporting';
import { alertDialog } from './services/alertService';
import { StartApplication } from './startApplication';
import { Bridge } from './services/bridge';
import { SessionsModalDirective } from './directives/views/sessionsModal';

const startApplication: StartApplication = async function startApplication(
defaultSyncServerHost: string,
Expand Down Expand Up @@ -122,7 +122,8 @@ const startApplication: StartApplication = async function startApplication(
)
.directive('revisionPreviewModal', () => new RevisionPreviewModal())
.directive('historyMenu', () => new HistoryMenu())
.directive('syncResolutionMenu', () => new SyncResolutionMenu());
.directive('syncResolutionMenu', () => new SyncResolutionMenu())
.directive('sessionsModal', SessionsModalDirective);

// Filters
angular.module('app').filter('trusted', ['$sce', trusted]);
Expand Down
7 changes: 7 additions & 0 deletions app/assets/javascripts/directives/views/accountMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type AccountMenuState = {
syncInProgress: boolean;
syncError: string;
syncPercentage: string;
showSessions: boolean;
}

class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
Expand Down Expand Up @@ -101,6 +102,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
mutable: {},
showBetaWarning: false,
errorReportingEnabled: !storage.get(StorageKey.DisableErrorReporting),
showSessions: this.appState.enableUnfinishedFeatures,
} as AccountMenuState;
}

Expand Down Expand Up @@ -320,6 +322,11 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword);
}

openSessionsModal() {
this.close();
this.appState.openSessionsModal();
}

async openPrivilegesModal() {
const run = () => {
this.application!.presentPrivilegesManagementModal();
Expand Down
251 changes: 251 additions & 0 deletions app/assets/javascripts/directives/views/sessionsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { AppState } from '@/ui_models/app_state';
import { PureViewCtrl } from '@/views';
import { SNApplication, RemoteSession, UuidString } from '@standardnotes/snjs';
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
import { render, FunctionComponent } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import { Dialog } from '@reach/dialog';
import { Alert } from '@reach/alert';
import {
AlertDialog,
AlertDialogDescription,
AlertDialogLabel,
} from '@reach/alert-dialog';

function useAutorun(view: (r: IReactionPublic) => any, opts?: IAutorunOptions) {
useEffect(() => autorun(view, opts), []);
}

function useSessions(
application: SNApplication
): [
RemoteSession[],
() => void,
boolean,
(uuid: UuidString) => Promise<void>,
string
] {
const [sessions, setSessions] = useState<RemoteSession[]>([]);
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 RemoteSession[];
setSessions(sessions);
setErrorMessage('');
}
setRefreshing(false);
})();
}, [lastRefreshDate]);

function refresh() {
setLastRefreshDate(Date.now());
}

async function revokeSession(uuid: UuidString) {
let sessionsBeforeRevoke = sessions;
setSessions(sessions.filter((session) => session.uuid !== uuid));
const response = await application.revokeSession(uuid);
if ('error' in response) {
if (response.error?.message) {
setErrorMessage(response.error?.message);
} else {
setErrorMessage('An unknown error occured while revoking the session.');
}
setSessions(sessionsBeforeRevoke);
}
}

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 [revokingSessionUuid, setRevokingSessionUuid] = useState('');
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',
});

return (
<>
<Dialog onDismiss={close}>
<div className="sk-modal-content sessions-modal">
<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"
onClick={() =>
setRevokingSessionUuid(session.uuid)
}
>
<span>Revoke</span>
</button>
</>
)}
</li>
))}
</ul>
)}
</>
)}
</div>
</div>
</div>
</div>
</Dialog>
{revokingSessionUuid && (
<AlertDialog 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">
Revoke this session?
</AlertDialogLabel>
<AlertDialogDescription className="sk-panel-row">
<p>
The associated app will not be able to sync unless you
sign in again.
</p>
</AlertDialogDescription>
<div className="sk-panel-row">
<div className="sk-button-group">
<button
className="sk-button neutral sk-label"
ref={cancelRevokeRef}
onClick={closeRevokeSessionAlert}
>
<span>Cancel</span>
</button>
<button
className="sk-button danger sk-label"
onClick={() => {
closeRevokeSessionAlert();
revokeSession(revokingSessionUuid);
}}
>
<span>Revoke</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AlertDialog>
)}
</>
);
};

const Sessions: FunctionComponent<{
appState: AppState;
application: SNApplication;
}> = ({ appState, application }) => {
const [showModal, setShowModal] = useState(false);
useAutorun(() => setShowModal(appState.isSessionsModalVisible));

if (showModal) {
return <SessionsModal application={application} appState={appState} />;
} else {
return null;
}
};

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: '=',
},
};
}
2 changes: 2 additions & 0 deletions app/assets/javascripts/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//= require_tree ./app

// css
import '@reach/dialog/styles.css';
import 'sn-stylekit/dist/stylekit.css';
import '../stylesheets/index.css.scss';
// import '../stylesheets/_reach-sub.scss';

// Vendor
import 'angular';
Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/services/archiveManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class ArchiveManager {
scriptTag.async = false;
const headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag);
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
scriptTag.onload = () => {
this.zip.workerScriptsPath = 'assets/zip/';
resolve();
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"newLine": "lf",
"declarationDir": "../../../dist/@types",
"baseUrl": ".",
"jsx": "react-jsx",
"jsxImportSource": "preact",
"paths": {
"%/*": ["../templates/*"],
"@/*": ["./*"],
Expand Down
Loading

0 comments on commit 2c0f215

Please sign in to comment.