feat: New account menu and text input with icon & toggle (#665)

* feat: Add new icons

* Revert "feat: Add new icons"

This reverts commit 0acb403fe846dbb2e48fd22de35c3568c3cb4453.

* feat: Add new icons for account menu

* feat: Add new Icons

* feat: Add "currentPane" state to prefs view

* feat: Update account menu to new design

* feat: Add input component with icon & toggle

* fix: sync icon & function

* fix: Fix eye icon

* feat: Create re-usable checkbox

feat: Add "merge local" option

* feat: Allow using className on IconButton

* feat: Add disabled state on input

feat: Make toggle circle

* refactor: Move checkbox to components

* feat: Handle invalid email/password error

* feat: Implement new design for Create Account

* feat: Implement new account menu design

* feat: Add disabled option to IconButton

* feat: Set account menu pane from other component

* feat: Add 2fa account menu pane

feat: Add lock icon

* feat: Remove unnecessary 2FA menu pane

feat: Reset current menu pane on clickOutside

* feat: Change "Log in" to "Sign in"

* feat: Remove sync from footer

* feat: Change "Login" to "Sign in"

feat: Add spinner to "Syncing..."

refactor: Use then-catch-finally for sync

* feat: Use common enableCustomServer state

* feat: Animate account menu closing

* fix: Reset menu pane only after it's closed

* feat: Add keyDown handler to InputWithIcon

* feat: Handle Enter press in inputs

* Update app/assets/javascripts/components/InputWithIcon.tsx

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>

* Update app/assets/javascripts/components/InputWithIcon.tsx

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>

* refactor: Use server state from AccountMenuState

* Update app/assets/javascripts/components/AccountMenu/CreateAccount.tsx

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>

* Update app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>

* feat: Use common AdvancedOptions

* feat: Add "eye-off" icon and toggle state

* feat: Allow undefined values

* refactor: Remove enableCustomServer state

* feat: Persist server option state

* feat: Add bottom-100 and cursor-auto util classes

refactor: Use bottom-100 and cursor-auto classes

* refactor: Invert ternary operator

* refactor: Remove unused imports

* refactor: Use toggled as prop instead of state

* refactor: Change "Log in/out" to "Sign in/out"

* refactor: Change "Login" to "Sign in"

* refactor: Remove hardcoded width/height

* refactor: Use success class

* feat: Remove hardcoded width & height from svg

* fix: Fix chevron-down icon

Co-authored-by: Antonella Sgarlatta <antsgar@gmail.com>
Co-authored-by: Antonella Sgarlatta <antonella@standardnotes.org>
This commit is contained in:
Aman Harwara
2021-10-08 21:48:31 +05:30
committed by GitHub
parent 7b6c99d188
commit f1122f292e
51 changed files with 1566 additions and 407 deletions

View File

@@ -0,0 +1,73 @@
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { Checkbox } from '../Checkbox';
import { Icon } from '../Icon';
import { InputWithIcon } from '../InputWithIcon';
type Props = {
application: WebApplication;
appState: AppState;
disabled?: boolean;
};
export const AdvancedOptions: FunctionComponent<Props> = observer(
({ appState, application, disabled = false, children }) => {
const { server, setServer, enableServerOption, setEnableServerOption } =
appState.accountMenu;
const [showAdvanced, setShowAdvanced] = useState(false);
const handleServerOptionChange = (e: Event) => {
if (e.target instanceof HTMLInputElement) {
setEnableServerOption(e.target.checked);
}
};
const handleSyncServerChange = (e: Event) => {
if (e.target instanceof HTMLInputElement) {
setServer(e.target.value);
application.setCustomHost(e.target.value);
}
};
const toggleShowAdvanced = () => {
setShowAdvanced(!showAdvanced);
};
return (
<>
<button
className="sn-dropdown-item font-bold"
onClick={toggleShowAdvanced}
>
<div className="flex items-center">
Advanced options
<Icon type="chevron-down" className="color-grey-1 ml-1" />
</div>
</button>
{showAdvanced ? (
<div className="px-3 my-2">
{children}
<Checkbox
name="custom-sync-server"
label="Custom sync server"
checked={enableServerOption}
onChange={handleServerOptionChange}
disabled={disabled}
/>
<InputWithIcon
inputType="text"
icon="server"
placeholder="https://api.standardnotes.com"
value={server}
onChange={handleSyncServerChange}
disabled={!enableServerOption && !disabled}
/>
</div>
) : null}
</>
);
}
);

View File

@@ -3,7 +3,7 @@ import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
STRING_GENERATING_LOGIN_KEYS,
STRING_GENERATING_REGISTER_KEYS,
STRING_NON_MATCHING_PASSWORDS
STRING_NON_MATCHING_PASSWORDS,
} from '@/strings';
import { JSXInternal } from 'preact/src/jsx';
import TargetedEvent = JSXInternal.TargetedEvent;
@@ -17,13 +17,9 @@ import { AppState } from '@/ui_models/app_state';
type Props = {
application: WebApplication;
appState: AppState;
}
const Authentication = observer(({
application,
appState,
}: Props) => {
};
const Authentication = observer(({ application, appState }: Props) => {
const [showAdvanced, setShowAdvanced] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [email, setEmail] = useState('');
@@ -39,12 +35,12 @@ const Authentication = observer(({
const {
server,
notesAndTagsCount,
showLogin,
showSignIn,
showRegister,
setShowLogin,
setShowSignIn,
setShowRegister,
setServer,
closeAccountMenu
closeAccountMenu,
} = appState.accountMenu;
const user = application.getUser();
@@ -58,11 +54,11 @@ const Authentication = observer(({
// Reset password and confirmation fields when hiding the form
useEffect(() => {
if (!showLogin && !showRegister) {
if (!showSignIn && !showRegister) {
setPassword('');
setPasswordConfirmation('');
}
}, [showLogin, showRegister]);
}, [showSignIn, showRegister]);
const handleHostInputChange = (event: TargetedEvent<HTMLInputElement>) => {
const { value } = event.target as HTMLInputElement;
@@ -75,7 +71,7 @@ const Authentication = observer(({
const passwordConfirmationInputRef = useRef<HTMLInputElement>();
const handleSignInClick = () => {
setShowLogin(true);
setShowSignIn(true);
setIsEmailFocused(true);
};
@@ -90,7 +86,7 @@ const Authentication = observer(({
passwordConfirmationInputRef.current?.blur();
};
const login = async () => {
const signin = async () => {
setStatus(STRING_GENERATING_LOGIN_KEYS);
setIsAuthenticating(true);
@@ -105,13 +101,13 @@ const Authentication = observer(({
if (!error) {
setIsAuthenticating(false);
setPassword('');
setShowLogin(false);
setShowSignIn(false);
closeAccountMenu();
return;
}
setShowLogin(true);
setShowSignIn(true);
setStatus(undefined);
setPassword('');
@@ -150,10 +146,11 @@ const Authentication = observer(({
}
};
const handleAuthFormSubmit = (event:
TargetedEvent<HTMLFormElement> |
TargetedMouseEvent<HTMLButtonElement> |
TargetedKeyboardEvent<HTMLButtonElement>
const handleAuthFormSubmit = (
event:
| TargetedEvent<HTMLFormElement>
| TargetedMouseEvent<HTMLButtonElement>
| TargetedKeyboardEvent<HTMLButtonElement>
) => {
event.preventDefault();
@@ -163,8 +160,8 @@ const Authentication = observer(({
blurAuthFields();
if (showLogin) {
login();
if (showSignIn) {
signin();
} else {
register();
}
@@ -186,19 +183,23 @@ const Authentication = observer(({
setEmail(value);
};
const handlePasswordConfirmationChange = (event: TargetedEvent<HTMLInputElement>) => {
const handlePasswordConfirmationChange = (
event: TargetedEvent<HTMLInputElement>
) => {
const { value } = event.target as HTMLInputElement;
setPasswordConfirmation(value);
};
const handleMergeLocalData = async (event: TargetedEvent<HTMLInputElement>) => {
const handleMergeLocalData = async (
event: TargetedEvent<HTMLInputElement>
) => {
const { checked } = event.target as HTMLInputElement;
setShouldMergeLocal(checked);
if (!checked) {
const confirmResult = await confirmDialog({
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
confirmButtonStyle: 'danger'
confirmButtonStyle: 'danger',
});
setShouldMergeLocal(!confirmResult);
}
@@ -206,10 +207,12 @@ const Authentication = observer(({
return (
<>
{!user && !showLogin && !showRegister && (
{!user && !showSignIn && !showRegister && (
<div className="sk-panel-section sk-panel-hero">
<div className="sk-panel-row">
<div className="sk-h1">Sign in or register to enable sync and end-to-end encryption.</div>
<div className="sk-h1">
Sign in or register to enable sync and end-to-end encryption.
</div>
</div>
<div className="flex my-1">
<button
@@ -226,17 +229,21 @@ const Authentication = observer(({
</button>
</div>
<div className="sk-panel-row sk-p">
Standard Notes is free on every platform, and comes
standard with sync and encryption.
Standard Notes is free on every platform, and comes standard with
sync and encryption.
</div>
</div>
)}
{(showLogin || showRegister) && (
{(showSignIn || showRegister) && (
<div className="sk-panel-section">
<div className="sk-panel-section-title">
{showLogin ? 'Sign In' : 'Register'}
{showSignIn ? 'Sign In' : 'Register'}
</div>
<form className="sk-panel-form" onSubmit={handleAuthFormSubmit} noValidate>
<form
className="sk-panel-form"
onSubmit={handleAuthFormSubmit}
noValidate
>
<div className="sk-panel-section">
<input
className="sk-input contrast"
@@ -261,26 +268,28 @@ const Authentication = observer(({
onKeyDown={handleKeyPressKeyDown}
ref={passwordInputRef}
/>
{showRegister &&
<input
className="sk-input contrast"
name="password_conf"
type="password"
placeholder="Confirm Password"
required
onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown}
value={passwordConfirmation}
onChange={handlePasswordConfirmationChange}
ref={passwordConfirmationInputRef}
/>}
{showRegister && (
<input
className="sk-input contrast"
name="password_conf"
type="password"
placeholder="Confirm Password"
required
onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown}
value={passwordConfirmation}
onChange={handlePasswordConfirmationChange}
ref={passwordConfirmationInputRef}
/>
)}
<div className="sk-panel-row" />
<button
type="button"
className="sk-a info font-bold text-left p-0 cursor-pointer hover:underline mr-1 ml-1"
onClick={() => {
setShowAdvanced(!showAdvanced);
}}>
}}
>
Advanced Options
</button>
</div>
@@ -301,24 +310,28 @@ const Authentication = observer(({
required
/>
</div>
{showLogin && (
{showSignIn && (
<label className="sk-label padded-row sk-panel-row justify-left">
<div className="sk-horizontal-group tight cursor-pointer">
<input
className="sk-input"
type="checkbox"
checked={isStrictSignIn}
onChange={() => setIsStrictSignIn(prevState => !prevState)}
onChange={() =>
setIsStrictSignIn((prevState) => !prevState)
}
/>
<p className="sk-p">Use strict sign in</p>
<span>
<a className="info"
href="https://standardnotes.com/help/security" rel="noopener"
target="_blank"
>
(Learn more)
</a>
</span>
<a
className="info"
href="https://standardnotes.com/help/security"
rel="noopener"
target="_blank"
>
(Learn more)
</a>
</span>
</div>
</label>
)}
@@ -327,9 +340,12 @@ const Authentication = observer(({
)}
{!isAuthenticating && (
<div className="sk-panel-section form-submit">
<button className="sn-button info text-base py-3 text-center" type="submit"
disabled={isAuthenticating}>
{showLogin ? 'Sign In' : 'Register'}
<button
className="sn-button info text-base py-3 text-center"
type="submit"
disabled={isAuthenticating}
>
{showSignIn ? 'Sign In' : 'Register'}
</button>
</div>
)}
@@ -337,9 +353,9 @@ const Authentication = observer(({
<div className="sk-notification neutral">
<div className="sk-notification-title">No Password Reset.</div>
<div className="sk-notification-text">
Because your notes are encrypted using your password,
Standard Notes does not have a password reset option.
You cannot forget your password.
Because your notes are encrypted using your password, Standard
Notes does not have a password reset option. You cannot forget
your password.
</div>
</div>
)}
@@ -358,7 +374,7 @@ const Authentication = observer(({
<input
type="checkbox"
checked={!isEphemeral}
onChange={() => setIsEphemeral(prevState => !prevState)}
onChange={() => setIsEphemeral((prevState) => !prevState)}
/>
<p className="sk-p">Stay signed in</p>
</div>
@@ -371,7 +387,9 @@ const Authentication = observer(({
checked={shouldMergeLocal}
onChange={handleMergeLocalData}
/>
<p className="sk-p">Merge local data ({notesAndTagsCount}) notes and tags</p>
<p className="sk-p">
Merge local data ({notesAndTagsCount}) notes and tags
</p>
</div>
</label>
)}
@@ -379,7 +397,8 @@ const Authentication = observer(({
)}
</form>
</div>
)}</>
)}
</>
);
});

View File

@@ -0,0 +1,172 @@
import { STRING_NON_MATCHING_PASSWORDS } from '@/strings';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { StateUpdater, useRef, useState } from 'preact/hooks';
import { AccountMenuPane } from '.';
import { Button } from '../Button';
import { Checkbox } from '../Checkbox';
import { IconButton } from '../IconButton';
import { InputWithIcon } from '../InputWithIcon';
import { AdvancedOptions } from './AdvancedOptions';
type Props = {
appState: AppState;
application: WebApplication;
setMenuPane: (pane: AccountMenuPane) => void;
email: string;
password: string;
setPassword: StateUpdater<string>;
};
export const ConfirmPassword: FunctionComponent<Props> = observer(
({ application, appState, setMenuPane, email, password, setPassword }) => {
const { notesAndTagsCount } = appState.accountMenu;
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isRegistering, setIsRegistering] = useState(false);
const [isEphemeral, setIsEphemeral] = useState(false);
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
const passwordInputRef = useRef<HTMLInputElement>();
const handlePasswordChange = (e: Event) => {
if (e.target instanceof HTMLInputElement) {
setConfirmPassword(e.target.value);
}
};
const handleEphemeralChange = () => {
setIsEphemeral(!isEphemeral);
};
const handleShouldMergeChange = () => {
setShouldMergeLocal(!shouldMergeLocal);
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
handleConfirmFormSubmit(e);
}
};
const handleConfirmFormSubmit = (e: Event) => {
e.preventDefault();
if (!password) {
passwordInputRef?.current.focus();
return;
}
if (password === confirmPassword) {
setIsRegistering(true);
application
.register(email, password, isEphemeral, shouldMergeLocal)
.then((res) => {
if (res.error) {
throw new Error(res.error.message);
}
appState.accountMenu.closeAccountMenu();
appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu);
})
.catch((err) => {
console.error(err);
application.alertService.alert(err).finally(() => {
setPassword('');
handleGoBack();
});
})
.finally(() => {
setIsRegistering(false);
});
} else {
application.alertService
.alert(STRING_NON_MATCHING_PASSWORDS)
.finally(() => {
setConfirmPassword('');
passwordInputRef?.current.focus();
});
}
};
const handleGoBack = () => {
setMenuPane(AccountMenuPane.Register);
};
return (
<>
<div className="flex items-center px-3 mt-1 mb-3">
<IconButton
icon="arrow-left"
title="Go back"
className="flex mr-2 color-neutral"
onClick={handleGoBack}
focusable={true}
disabled={isRegistering}
/>
<div className="sn-account-menu-headline">Confirm password</div>
</div>
<div className="px-3 mb-3 text-sm">
Because your notes are encrypted using your password,{' '}
<span className="color-dark-red">
Standard Notes does not have a password reset option
</span>
. If you forget your password, you will permanently lose access to
your data.
</div>
<form onSubmit={handleConfirmFormSubmit} className="px-3 mb-1">
<InputWithIcon
className="mb-2"
icon="password"
inputType={showPassword ? 'text' : 'password'}
placeholder="Confirm password"
value={confirmPassword}
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
toggle={{
toggleOnIcon: 'eye-off',
toggleOffIcon: 'eye',
title: 'Show password',
toggled: showPassword,
onClick: setShowPassword,
}}
ref={passwordInputRef}
disabled={isRegistering}
/>
<Button
className="btn-w-full mt-1 mb-3"
label={
isRegistering ? 'Creating account...' : 'Create account & sign in'
}
type="primary"
onClick={handleConfirmFormSubmit}
disabled={isRegistering}
/>
<Checkbox
name="is-ephemeral"
label="Stay signed in"
checked={!isEphemeral}
onChange={handleEphemeralChange}
disabled={isRegistering}
/>
{notesAndTagsCount > 0 ? (
<Checkbox
name="should-merge-local"
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
checked={shouldMergeLocal}
onChange={handleShouldMergeChange}
disabled={isRegistering}
/>
) : null}
</form>
<div className="h-1px my-2 bg-border"></div>
<AdvancedOptions
appState={appState}
application={application}
disabled={isRegistering}
/>
</>
);
}
);

View File

@@ -0,0 +1,137 @@
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks';
import { AccountMenuPane } from '.';
import { Button } from '../Button';
import { IconButton } from '../IconButton';
import { InputWithIcon } from '../InputWithIcon';
import { AdvancedOptions } from './AdvancedOptions';
type Props = {
appState: AppState;
application: WebApplication;
setMenuPane: (pane: AccountMenuPane) => void;
email: string;
setEmail: StateUpdater<string>;
password: string;
setPassword: StateUpdater<string>;
};
export const CreateAccount: FunctionComponent<Props> = observer(
({
appState,
application,
setMenuPane,
email,
setEmail,
password,
setPassword,
}) => {
const [showPassword, setShowPassword] = useState(false);
const emailInputRef = useRef<HTMLInputElement>();
const passwordInputRef = useRef<HTMLInputElement>();
useEffect(() => {
if (emailInputRef.current) {
emailInputRef.current.focus();
}
}, []);
const handleEmailChange = (e: Event) => {
if (e.target instanceof HTMLInputElement) {
setEmail(e.target.value);
}
};
const handlePasswordChange = (e: Event) => {
if (e.target instanceof HTMLInputElement) {
setPassword(e.target.value);
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
handleRegisterFormSubmit(e);
}
};
const handleRegisterFormSubmit = (e: Event) => {
e.preventDefault();
if (!email || email.length === 0) {
emailInputRef?.current.focus();
return;
}
if (!password || password.length === 0) {
passwordInputRef?.current.focus();
return;
}
setEmail(email);
setPassword(password);
setMenuPane(AccountMenuPane.ConfirmPassword);
};
const handleClose = () => {
setMenuPane(AccountMenuPane.GeneralMenu);
setEmail('');
setPassword('');
};
return (
<>
<div className="flex items-center px-3 mt-1 mb-3">
<IconButton
icon="arrow-left"
title="Go back"
className="flex mr-2 color-neutral"
onClick={handleClose}
focusable={true}
/>
<div className="sn-account-menu-headline">Create account</div>
</div>
<form onSubmit={handleRegisterFormSubmit} className="px-3 mb-1">
<InputWithIcon
className="mb-2"
icon="email"
inputType="email"
placeholder="Email"
value={email}
onChange={handleEmailChange}
onKeyDown={handleKeyDown}
ref={emailInputRef}
/>
<InputWithIcon
className="mb-2"
icon="password"
inputType={showPassword ? 'text' : 'password'}
placeholder="Password"
value={password}
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
toggle={{
toggleOnIcon: 'eye-off',
toggleOffIcon: 'eye',
title: 'Show password',
toggled: showPassword,
onClick: setShowPassword,
}}
ref={passwordInputRef}
/>
<Button
className="btn-w-full mt-1"
label="Next"
type="primary"
onClick={handleRegisterFormSubmit}
/>
</form>
<div className="h-1px my-2 bg-border"></div>
<AdvancedOptions application={application} appState={appState} />
</>
);
}
);

View File

@@ -6,25 +6,26 @@ import { observer } from 'mobx-react-lite';
type Props = {
application: WebApplication;
appState: AppState;
}
};
const Footer = observer(({
application,
appState,
}: Props) => {
const Footer = observer(({ application, appState }: Props) => {
const {
showLogin,
showSignIn,
showRegister,
setShowLogin,
setShowSignIn,
setShowRegister,
setSigningOut
setSigningOut,
} = appState.accountMenu;
const user = application.getUser();
const { showBetaWarning, disableBetaWarning: disableAppStateBetaWarning } = appState;
const { showBetaWarning, disableBetaWarning: disableAppStateBetaWarning } =
appState;
const [appVersion] = useState(() => `v${((window as any).electronAppVersion || application.bridge.appVersion)}`);
const [appVersion] = useState(
() =>
`v${(window as any).electronAppVersion || application.bridge.appVersion}`
);
const disableBetaWarning = () => {
disableAppStateBetaWarning();
@@ -35,7 +36,7 @@ const Footer = observer(({
};
const hidePasswordForm = () => {
setShowLogin(false);
setShowSignIn(false);
setShowRegister(false);
};
@@ -46,16 +47,20 @@ const Footer = observer(({
<span>{appVersion}</span>
{showBetaWarning && (
<span>
<span> (</span>
<a className="sk-a" onClick={disableBetaWarning}>Hide beta warning</a>
<span>)</span>
</span>
<span> (</span>
<a className="sk-a" onClick={disableBetaWarning}>
Hide beta warning
</a>
<span>)</span>
</span>
)}
</div>
{(showLogin || showRegister) && (
<a className="sk-a right" onClick={hidePasswordForm}>Cancel</a>
{(showSignIn || showRegister) && (
<a className="sk-a right" onClick={hidePasswordForm}>
Cancel
</a>
)}
{!showLogin && !showRegister && (
{!showSignIn && !showRegister && (
<a className="sk-a right danger capitalize" onClick={signOut}>
{user ? 'Sign out' : 'Clear session data'}
</a>

View File

@@ -0,0 +1,166 @@
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { Icon } from '../Icon';
import { formatLastSyncDate } from '@/preferences/panes/account/Sync';
import { SyncQueueStrategy } from '@standardnotes/snjs';
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
import { useState } from 'preact/hooks';
import { AccountMenuPane } from '.';
import { FunctionComponent } from 'preact';
type Props = {
appState: AppState;
application: WebApplication;
setMenuPane: (pane: AccountMenuPane) => void;
closeMenu: () => void;
};
const iconClassName = 'color-grey-1 mr-2';
export const GeneralAccountMenu: FunctionComponent<Props> = observer(
({ application, appState, setMenuPane, closeMenu }) => {
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
const [lastSyncDate, setLastSyncDate] = useState(
formatLastSyncDate(application.getLastSyncDate() as Date)
);
const doSynchronization = async () => {
setIsSyncingInProgress(true);
application
.sync({
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
checkIntegrity: true,
})
.then((res) => {
if (res && res.error) {
throw new Error();
} else {
setLastSyncDate(
formatLastSyncDate(application.getLastSyncDate() as Date)
);
}
})
.catch(() => {
application.alertService.alert(STRING_GENERIC_SYNC_ERROR);
})
.finally(() => {
setIsSyncingInProgress(false);
});
};
const user = application.getUser();
return (
<>
<div className="flex items-center justify-between px-3 mt-1 mb-3">
<div className="sn-account-menu-headline">Account</div>
<div className="flex cursor-pointer" onClick={closeMenu}>
<Icon type="close" className="color-grey-1" />
</div>
</div>
{user ? (
<>
<div className="px-3 mb-2 color-foreground text-sm">
<div>You're signed in as:</div>
<div className="font-bold">{user.email}</div>
</div>
<div className="flex items-center justify-between px-3 mb-2">
{isSyncingInProgress ? (
<div className="flex items-center color-info font-semibold">
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
Syncing...
</div>
) : (
<div className="flex items-center success font-semibold">
<Icon type="check-circle" className="mr-2" />
Last synced: {lastSyncDate}
</div>
)}
<div
className="flex cursor-pointer color-grey-1"
onClick={doSynchronization}
>
<Icon type="sync" />
</div>
</div>
</>
) : (
<>
<div className="px-3 mb-1">
<div className="mb-3 color-foreground">
Youre offline. Sign in to sync your notes and preferences
across all your devices and enable end-to-end encryption.
</div>
<div className="flex items-center color-grey-1">
<Icon type="cloud-off" className="mr-2" />
<span className="font-semibold">Offline</span>
</div>
</div>
</>
)}
<div className="h-1px my-2 bg-border"></div>
{user ? (
<button
className="sn-dropdown-item"
onClick={() => {
appState.accountMenu.closeAccountMenu();
appState.preferences.setCurrentPane('account');
appState.preferences.openPreferences();
}}
>
<Icon type="user" className={iconClassName} />
Account settings
</button>
) : (
<>
<button
className="sn-dropdown-item"
onClick={() => {
setMenuPane(AccountMenuPane.Register);
}}
>
<Icon type="user" className={iconClassName} />
Create free account
</button>
<button
className="sn-dropdown-item"
onClick={() => {
setMenuPane(AccountMenuPane.SignIn);
}}
>
<Icon type="signIn" className={iconClassName} />
Sign in
</button>
</>
)}
<button
className="sn-dropdown-item"
onClick={() => {
appState.accountMenu.closeAccountMenu();
appState.preferences.setCurrentPane('help-feedback');
appState.preferences.openPreferences();
}}
>
<Icon type="help" className={iconClassName} />
Help &amp; feedback
</button>
{user ? (
<>
<div className="h-1px my-2 bg-border"></div>
<button
className="sn-dropdown-item"
onClick={() => {
appState.accountMenu.setSigningOut(true);
}}
>
<Icon type="signOut" className={iconClassName} />
Sign out and clear local data
</button>
</>
) : null}
</>
);
}
);

View File

@@ -0,0 +1,227 @@
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import { AccountMenuPane } from '.';
import { Button } from '../Button';
import { Checkbox } from '../Checkbox';
import { Icon } from '../Icon';
import { IconButton } from '../IconButton';
import { InputWithIcon } from '../InputWithIcon';
import { AdvancedOptions } from './AdvancedOptions';
type Props = {
appState: AppState;
application: WebApplication;
setMenuPane: (pane: AccountMenuPane) => void;
};
export const SignInPane: FunctionComponent<Props> = observer(
({ application, appState, setMenuPane }) => {
const { notesAndTagsCount } = appState.accountMenu;
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isInvalid, setIsInvalid] = useState(false);
const [isEphemeral, setIsEphemeral] = useState(false);
const [isStrictSignin, setIsStrictSignin] = useState(false);
const [isSigningIn, setIsSigningIn] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
const emailInputRef = useRef<HTMLInputElement>();
const passwordInputRef = useRef<HTMLInputElement>();
useEffect(() => {
if (emailInputRef?.current) {
emailInputRef.current.focus();
}
}, []);
const resetInvalid = () => {
if (isInvalid) {
setIsInvalid(false);
}
};
const handleEmailChange = (e: Event) => {
if (e.target instanceof HTMLInputElement) {
setEmail(e.target.value);
}
};
const handlePasswordChange = (e: Event) => {
if (isInvalid) {
setIsInvalid(false);
}
if (e.target instanceof HTMLInputElement) {
setPassword(e.target.value);
}
};
const handleEphemeralChange = () => {
setIsEphemeral(!isEphemeral);
};
const handleStrictSigninChange = () => {
setIsStrictSignin(!isStrictSignin);
};
const handleShouldMergeChange = () => {
setShouldMergeLocal(!shouldMergeLocal);
};
const signIn = () => {
setIsSigningIn(true);
emailInputRef?.current.blur();
passwordInputRef?.current.blur();
application
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
.then((res) => {
if (res.error) {
throw new Error(res.error.message);
}
appState.accountMenu.closeAccountMenu();
})
.catch((err) => {
console.error(err);
if (err.toString().includes('Invalid email or password')) {
setIsInvalid(true);
} else {
application.alertService.alert(err);
}
setPassword('');
passwordInputRef?.current.blur();
})
.finally(() => {
setIsSigningIn(false);
});
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
handleSignInFormSubmit(e);
}
};
const handleSignInFormSubmit = (e: Event) => {
e.preventDefault();
if (!email || email.length === 0) {
emailInputRef?.current.focus();
return;
}
if (!password || password.length === 0) {
passwordInputRef?.current.focus();
return;
}
signIn();
};
return (
<>
<div className="flex items-center px-3 mt-1 mb-3">
<IconButton
icon="arrow-left"
title="Go back"
className="flex mr-2 color-neutral"
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
focusable={true}
disabled={isSigningIn}
/>
<div className="sn-account-menu-headline">Sign in</div>
</div>
<form onSubmit={handleSignInFormSubmit}>
<div className="px-3 mb-1">
<InputWithIcon
className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`}
icon="email"
inputType="email"
placeholder="Email"
value={email}
onChange={handleEmailChange}
onFocus={resetInvalid}
onKeyDown={handleKeyDown}
disabled={isSigningIn}
ref={emailInputRef}
/>
<InputWithIcon
className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`}
icon="password"
inputType={showPassword ? 'text' : 'password'}
placeholder="Password"
value={password}
onChange={handlePasswordChange}
onFocus={resetInvalid}
onKeyDown={handleKeyDown}
disabled={isSigningIn}
toggle={{
toggleOnIcon: 'eye-off',
toggleOffIcon: 'eye',
title: 'Show password',
toggled: showPassword,
onClick: setShowPassword,
}}
ref={passwordInputRef}
/>
{isInvalid ? (
<div className="color-dark-red my-2">
Invalid email or password.
</div>
) : null}
<Button
className="btn-w-full mt-1 mb-3"
label={isSigningIn ? 'Signing in...' : 'Sign in'}
type="primary"
onClick={handleSignInFormSubmit}
disabled={isSigningIn}
/>
<Checkbox
name="is-ephemeral"
label="Stay signed in"
checked={!isEphemeral}
disabled={isSigningIn}
onChange={handleEphemeralChange}
/>
{notesAndTagsCount > 0 ? (
<Checkbox
name="should-merge-local"
label={`Merge local data (${notesAndTagsCount} notes and tags)`}
checked={shouldMergeLocal}
disabled={isSigningIn}
onChange={handleShouldMergeChange}
/>
) : null}
</div>
</form>
<div className="h-1px my-2 bg-border"></div>
<AdvancedOptions
appState={appState}
application={application}
disabled={isSigningIn}
>
<div className="flex justify-between items-center mb-1">
<Checkbox
name="use-strict-signin"
label="Use strict sign-in"
checked={isStrictSignin}
disabled={isSigningIn}
onChange={handleStrictSigninChange}
/>
<a
href="https://standardnotes.com/help/security"
target="_blank"
rel="noopener noreferrer"
title="Learn more"
>
<Icon type="info" className="color-neutral" />
</a>
</div>
</AdvancedOptions>
</>
);
}
);

View File

@@ -2,72 +2,115 @@ import { observer } from 'mobx-react-lite';
import { toDirective } from '@/components/utils';
import { AppState } from '@/ui_models/app_state';
import { WebApplication } from '@/ui_models/application';
import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
import Authentication from '@/components/AccountMenu/Authentication';
import Footer from '@/components/AccountMenu/Footer';
import User from '@/components/AccountMenu/User';
import { useEffect } from 'preact/hooks';
import { useState } from 'preact/hooks';
import { GeneralAccountMenu } from './GeneralAccountMenu';
import { FunctionComponent } from 'preact';
import { SignInPane } from './SignIn';
import { CreateAccount } from './CreateAccount';
import { ConfirmSignoutContainer } from '../ConfirmSignoutModal';
import { ConfirmPassword } from './ConfirmPassword';
export enum AccountMenuPane {
GeneralMenu,
SignIn,
Register,
ConfirmPassword,
}
type Props = {
appState: AppState;
application: WebApplication;
};
const AccountMenu = observer(({ application, appState }: Props) => {
const {
show: showAccountMenu,
showLogin,
showRegister,
setShowLogin,
setShowRegister,
closeAccountMenu
} = appState.accountMenu;
type PaneSelectorProps = Props & {
menuPane: AccountMenuPane;
setMenuPane: (pane: AccountMenuPane) => void;
closeMenu: () => void;
};
const user = application.getUser();
const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
({ application, appState, menuPane, setMenuPane, closeMenu }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
useEffect(() => {
// Reset "Login" and "Registration" sections state when hiding account menu,
// so the next time account menu is opened these sections are closed
if (!showAccountMenu) {
setShowLogin(false);
setShowRegister(false);
}
}, [setShowLogin, setShowRegister, showAccountMenu]);
return (
<div className="sn-component">
<div id="account-panel" className="sk-panel">
<div className="sk-panel-header">
<div className="sk-panel-header-title">Account</div>
<a className="sk-a info close-button" onClick={closeAccountMenu}>Close</a>
</div>
<div className="sk-panel-content">
<Authentication
application={application}
switch (menuPane) {
case AccountMenuPane.GeneralMenu:
return (
<GeneralAccountMenu
appState={appState}
application={application}
setMenuPane={setMenuPane}
closeMenu={closeMenu}
/>
);
case AccountMenuPane.SignIn:
return (
<SignInPane
appState={appState}
application={application}
setMenuPane={setMenuPane}
/>
);
case AccountMenuPane.Register:
return (
<CreateAccount
appState={appState}
application={application}
setMenuPane={setMenuPane}
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
/>
);
case AccountMenuPane.ConfirmPassword:
return (
<ConfirmPassword
appState={appState}
application={application}
setMenuPane={setMenuPane}
email={email}
password={password}
setPassword={setPassword}
/>
);
}
}
);
const AccountMenu: FunctionComponent<Props> = observer(
({ application, appState }) => {
const {
currentPane,
setCurrentPane,
shouldAnimateCloseMenu,
closeAccountMenu,
} = appState.accountMenu;
return (
<div className="sn-component">
<div
className={`sn-account-menu sn-dropdown ${
shouldAnimateCloseMenu
? 'slide-up-animation'
: 'sn-dropdown--animated'
} min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto absolute`}
>
<MenuPaneSelector
appState={appState}
application={application}
menuPane={currentPane}
setMenuPane={setCurrentPane}
closeMenu={closeAccountMenu}
/>
{!showLogin && !showRegister && user && (
<div>
<User
application={application}
appState={appState}
/>
</div>
)}
</div>
<ConfirmSignoutContainer
application={application}
appState={appState}
/>
<Footer
application={application}
appState={appState}
/>
</div>
</div>
);
});
export const AccountMenuDirective = toDirective<Props>(
AccountMenu
);
}
);
export const AccountMenuDirective = toDirective<Props>(AccountMenu);

View File

@@ -0,0 +1,32 @@
import { FunctionComponent } from 'preact';
type CheckboxProps = {
name: string;
checked: boolean;
onChange: (e: Event) => void;
disabled?: boolean;
label: string;
};
export const Checkbox: FunctionComponent<CheckboxProps> = ({
name,
checked,
onChange,
disabled,
label,
}) => {
return (
<label htmlFor={name} className="flex items-center fit-content mb-2">
<input
className="mr-2"
type="checkbox"
name={name}
id={name}
checked={checked}
onChange={onChange}
disabled={disabled}
/>
{label}
</label>
);
};

View File

@@ -36,11 +36,35 @@ import InfoIcon from '../../icons/ic-info.svg';
import CheckIcon from '../../icons/ic-check.svg';
import CheckBoldIcon from '../../icons/ic-check-bold.svg';
import AccountCircleIcon from '../../icons/ic-account-circle.svg';
import CloudOffIcon from '../../icons/ic-cloud-off.svg';
import SignInIcon from '../../icons/ic-signin.svg';
import SignOutIcon from '../../icons/ic-signout.svg';
import CheckCircleIcon from '../../icons/ic-check-circle.svg';
import SyncIcon from '../../icons/ic-sync.svg';
import ArrowLeftIcon from '../../icons/ic-arrow-left.svg';
import ChevronDownIcon from '../../icons/ic-chevron-down.svg';
import EmailIcon from '../../icons/ic-email.svg';
import ServerIcon from '../../icons/ic-server.svg';
import EyeIcon from '../../icons/ic-eye.svg';
import EyeOffIcon from '../../icons/ic-eye-off.svg';
import LockIcon from '../../icons/ic-lock.svg';
import { toDirective } from './utils';
import { FunctionalComponent } from 'preact';
const ICONS = {
lock: LockIcon,
eye: EyeIcon,
'eye-off': EyeOffIcon,
server: ServerIcon,
email: EmailIcon,
'chevron-down': ChevronDownIcon,
'arrow-left': ArrowLeftIcon,
sync: SyncIcon,
'check-circle': CheckCircleIcon,
signIn: SignInIcon,
signOut: SignOutIcon,
'cloud-off': CloudOffIcon,
'pencil-off': PencilOffIcon,
'plain-text': PlainTextIcon,
'rich-text': RichTextIcon,

View File

@@ -19,6 +19,8 @@ interface Props {
title: string;
focusable: boolean;
disabled?: boolean;
}
/**
@@ -31,6 +33,8 @@ export const IconButton: FunctionComponent<Props> = ({
icon,
title,
focusable,
iconClassName = '',
disabled = false,
}) => {
const click = (e: MouseEvent) => {
e.preventDefault();
@@ -42,8 +46,9 @@ export const IconButton: FunctionComponent<Props> = ({
title={title}
className={`no-border cursor-pointer bg-transparent flex flex-row items-center hover:brightness-130 p-0 ${focusableClass} ${className}`}
onClick={click}
disabled={disabled}
>
<Icon type={icon} />
<Icon type={icon} className={iconClassName} />
</button>
);
};

View File

@@ -0,0 +1,89 @@
import { FunctionComponent, Ref } from 'preact';
import { JSXInternal } from 'preact/src/jsx';
import { forwardRef } from 'preact/compat';
import { Icon, IconType } from './Icon';
import { IconButton } from './IconButton';
type ToggleProps = {
toggleOnIcon: IconType;
toggleOffIcon: IconType;
title: string;
toggled: boolean;
onClick: (toggled: boolean) => void;
};
type Props = {
icon: IconType;
inputType: 'text' | 'email' | 'password';
className?: string;
iconClassName?: string;
value: string | undefined;
onChange: JSXInternal.GenericEventHandler<HTMLInputElement>;
onFocus?: JSXInternal.GenericEventHandler<HTMLInputElement>;
onKeyDown?: JSXInternal.KeyboardEventHandler<HTMLInputElement>;
disabled?: boolean;
placeholder: string;
toggle?: ToggleProps;
};
const DISABLED_CLASSNAME = 'bg-grey-5 cursor-not-allowed';
export const InputWithIcon: FunctionComponent<Props> = forwardRef(
(
{
icon,
inputType,
className,
iconClassName,
value,
onChange,
onFocus,
onKeyDown,
disabled,
toggle,
placeholder,
},
ref: Ref<HTMLInputElement>
) => {
const handleToggle = () => {
if (toggle) toggle.onClick(!toggle?.toggled);
};
return (
<div
className={`flex items-stretch position-relative bg-default border-1 border-solid border-neutral rounded focus-within:ring-info overflow-hidden ${
disabled ? DISABLED_CLASSNAME : ''
} ${className}`}
>
<div className="flex px-2 py-1.5">
<Icon type={icon} className={`color-grey-1 ${iconClassName}`} />
</div>
<input
type={inputType}
onFocus={onFocus}
onChange={onChange}
onKeyDown={onKeyDown}
value={value}
className={`pr-2 w-full border-0 focus:shadow-none ${
disabled ? DISABLED_CLASSNAME : ''
}`}
disabled={disabled}
placeholder={placeholder}
ref={ref}
/>
{toggle ? (
<div className="flex items-center justify-center px-2">
<IconButton
className="w-5 h-5 justify-center sk-circle hover:bg-grey-4"
icon={toggle.toggled ? toggle.toggleOnIcon : toggle.toggleOffIcon}
iconClassName="sn-icon--small"
title={toggle.title}
onClick={handleToggle}
focusable={true}
/>
</div>
) : null}
</div>
);
}
);

View File

@@ -14,19 +14,18 @@ type Props = {
appState: AppState;
};
export const OtherSessionsLogoutContainer = observer((props: Props) => {
if (!props.appState.accountMenu.otherSessionsLogOut) {
export const OtherSessionsSignOutContainer = observer((props: Props) => {
if (!props.appState.accountMenu.otherSessionsSignOut) {
return null;
}
return <ConfirmOtherSessionsLogout {...props} />;
return <ConfirmOtherSessionsSignOut {...props} />;
});
const ConfirmOtherSessionsLogout = observer(
const ConfirmOtherSessionsSignOut = observer(
({ application, appState }: Props) => {
const cancelRef = useRef<HTMLButtonElement>();
function closeDialog() {
appState.accountMenu.setOtherSessionsLogout(false);
appState.accountMenu.setOtherSessionsSignOut(false);
}
return (
@@ -41,9 +40,10 @@ const ConfirmOtherSessionsLogout = observer(
</AlertDialogLabel>
<AlertDialogDescription className="sk-panel-row">
<p className="color-foreground">
This action will sign out all other devices signed into your account,
and remove your data from those devices when they next regain connection
to the internet. You may sign back in on those devices at any time.
This action will sign out all other devices signed into
your account, and remove your data from those devices when
they next regain connection to the internet. You may sign
back in on those devices at any time.
</p>
</AlertDialogDescription>
<div className="flex my-1 mt-4">
@@ -60,9 +60,9 @@ const ConfirmOtherSessionsLogout = observer(
application.revokeAllOtherSessions();
closeDialog();
application.alertService.alert(
"You have successfully revoked your sessions from other devices.",
'You have successfully revoked your sessions from other devices.',
undefined,
"Finish"
'Finish'
);
}}
>

View File

@@ -1,48 +1,52 @@
import { WebApplication } from '@/ui_models/application';
import { PasswordWizardScope, PasswordWizardType, WebDirective } from './../../types';
import {
PasswordWizardScope,
PasswordWizardType,
WebDirective,
} from './../../types';
import template from '%/directives/password-wizard.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
const DEFAULT_CONTINUE_TITLE = "Continue";
const DEFAULT_CONTINUE_TITLE = 'Continue';
enum Steps {
PasswordStep = 1,
FinishStep = 2
FinishStep = 2,
}
type FormData = {
currentPassword?: string,
newPassword?: string,
newPasswordConfirmation?: string,
status?: string
}
currentPassword?: string;
newPassword?: string;
newPasswordConfirmation?: string;
status?: string;
};
type State = {
lockContinue: boolean
formData: FormData,
continueTitle: string,
step: Steps,
title: string,
showSpinner: boolean
processing: boolean
}
lockContinue: boolean;
formData: FormData;
continueTitle: string;
step: Steps;
title: string;
showSpinner: boolean;
processing: boolean;
};
type Props = {
type: PasswordWizardType,
changePassword: boolean,
securityUpdate: boolean
}
type: PasswordWizardType;
changePassword: boolean;
securityUpdate: boolean;
};
class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordWizardScope {
$element: JQLite
application!: WebApplication
type!: PasswordWizardType
isContinuing = false
class PasswordWizardCtrl
extends PureViewCtrl<Props, State>
implements PasswordWizardScope
{
$element: JQLite;
application!: WebApplication;
type!: PasswordWizardType;
isContinuing = false;
/* @ngInject */
constructor(
$element: JQLite,
$timeout: ng.ITimeoutService,
) {
constructor($element: JQLite, $timeout: ng.ITimeoutService) {
super($timeout);
this.$element = $element;
this.registerWindowUnloadStopper();
@@ -53,13 +57,13 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
this.initProps({
type: this.type,
changePassword: this.type === PasswordWizardType.ChangePassword,
securityUpdate: this.type === PasswordWizardType.AccountUpgrade
securityUpdate: this.type === PasswordWizardType.AccountUpgrade,
});
this.setState({
formData: {},
continueTitle: DEFAULT_CONTINUE_TITLE,
step: Steps.PasswordStep,
title: this.props.changePassword ? 'Change Password' : 'Account Update'
title: this.props.changePassword ? 'Change Password' : 'Account Update',
});
}
@@ -78,7 +82,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
resetContinueState() {
this.setState({
showSpinner: false,
continueTitle: DEFAULT_CONTINUE_TITLE
continueTitle: DEFAULT_CONTINUE_TITLE,
});
this.isContinuing = false;
}
@@ -95,7 +99,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
this.isContinuing = true;
await this.setState({
showSpinner: true,
continueTitle: "Generating Keys..."
continueTitle: 'Generating Keys...',
});
const valid = await this.validateCurrentPassword();
if (!valid) {
@@ -110,8 +114,8 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
this.isContinuing = false;
this.setState({
showSpinner: false,
continueTitle: "Finish",
step: Steps.FinishStep
continueTitle: 'Finish',
step: Steps.FinishStep,
});
}
@@ -119,43 +123,43 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
return this.setState({
formData: {
...this.state.formData,
...formData
}
...formData,
},
});
}
async validateCurrentPassword() {
const currentPassword = this.state.formData.currentPassword;
const newPass = this.props.securityUpdate ? currentPassword : this.state.formData.newPassword;
const newPass = this.props.securityUpdate
? currentPassword
: this.state.formData.newPassword;
if (!currentPassword || currentPassword.length === 0) {
this.application.alertService!.alert(
"Please enter your current password."
'Please enter your current password.'
);
return false;
}
if (this.props.changePassword) {
if (!newPass || newPass.length === 0) {
this.application.alertService!.alert(
"Please enter a new password."
);
this.application.alertService!.alert('Please enter a new password.');
return false;
}
if (newPass !== this.state.formData.newPasswordConfirmation) {
this.application.alertService!.alert(
"Your new password does not match its confirmation."
'Your new password does not match its confirmation.'
);
this.setFormDataState({
status: undefined
status: undefined,
});
return false;
}
}
if (!this.application.getUser()?.email) {
this.application.alertService!.alert(
"We don't have your email stored. Please log out then log back in to fix this issue."
"We don't have your email stored. Please sign out then log back in to fix this issue."
);
this.setFormDataState({
status: undefined
status: undefined,
});
return false;
}
@@ -166,7 +170,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
);
if (!success) {
this.application.alertService!.alert(
"The current password you entered is not correct. Please try again."
'The current password you entered is not correct. Please try again.'
);
}
return success;
@@ -176,10 +180,10 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
await this.application.downloadBackup();
await this.setState({
lockContinue: true,
processing: true
processing: true,
});
await this.setFormDataState({
status: "Processing encryption keys…"
status: 'Processing encryption keys…',
});
const newPassword = this.props.securityUpdate
? this.state.formData.currentPassword
@@ -195,16 +199,16 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
});
if (!success) {
this.setFormDataState({
status: "Unable to process your password. Please try again."
status: 'Unable to process your password. Please try again.',
});
} else {
this.setState({
formData: {
...this.state.formData,
status: this.props.changePassword
? "Successfully changed password."
: "Successfully performed account update."
}
? 'Successfully changed password.'
: 'Successfully performed account update.',
},
});
}
return success;
@@ -213,7 +217,7 @@ class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordW
dismiss() {
if (this.state.lockContinue) {
this.application.alertService!.alert(
"Cannot close window until pending tasks are complete."
'Cannot close window until pending tasks are complete.'
);
} else {
const elem = this.$element;
@@ -234,7 +238,7 @@ export class PasswordWizard extends WebDirective {
this.bindToController = true;
this.scope = {
type: '=',
application: '='
application: '=',
};
}
}

View File

@@ -1,14 +1,20 @@
import { RoundIconButton } from '@/components/RoundIconButton';
import { TitleBar, Title } from '@/components/TitleBar';
import { FunctionComponent } from 'preact';
import { AccountPreferences, HelpAndFeedback, Listed, General, Security } from './panes';
import {
AccountPreferences,
HelpAndFeedback,
Listed,
General,
Security,
} from './panes';
import { observer } from 'mobx-react-lite';
import { PreferencesMenu } from './PreferencesMenu';
import { PreferencesMenuView } from './PreferencesMenuView';
import { WebApplication } from '@/ui_models/application';
import { MfaProps } from './panes/two-factor-auth/MfaProps';
import { AppState } from '@/ui_models/app_state';
import { useEffect } from 'preact/hooks';
import { useEffect, useMemo } from 'preact/hooks';
import { Extensions } from './panes/Extensions';
interface PreferencesProps extends MfaProps {
@@ -22,7 +28,9 @@ const PaneSelector: FunctionComponent<
> = observer((props) => {
switch (props.menu.selectedPaneId) {
case 'general':
return <General appState={props.appState} application={props.application} />
return (
<General appState={props.appState} application={props.application} />
);
case 'account':
return (
<AccountPreferences
@@ -67,20 +75,22 @@ const PreferencesCanvas: FunctionComponent<
export const PreferencesView: FunctionComponent<PreferencesProps> = observer(
(props) => {
const menu = useMemo(() => new PreferencesMenu(), []);
useEffect(() => {
menu.selectPane(props.appState.preferences.currentPane);
const removeEscKeyObserver = props.application.io.addKeyObserver({
key: 'Escape',
onKeyDown: (event) => {
event.preventDefault();
props.closePreferences();
}
},
});
return () => {
removeEscKeyObserver();
};
}, [props]);
const menu = new PreferencesMenu();
}, [props, menu]);
return (
<div className="h-full w-full absolute top-left-0 flex flex-col bg-contrast z-index-preferences">
<TitleBar className="items-center justify-between">

View File

@@ -2,7 +2,7 @@ import {
Sync,
SubscriptionWrapper,
Credentials,
LogOutWrapper,
SignOutWrapper,
Authentication,
} from '@/preferences/panes/account';
import { PreferencesPane } from '@/preferences/components';
@@ -23,7 +23,7 @@ export const AccountPreferences = observer(
return (
<PreferencesPane>
<Authentication application={application} appState={appState} />
<LogOutWrapper application={application} appState={appState} />
<SignOutWrapper application={application} appState={appState} />
</PreferencesPane>
);
}
@@ -33,7 +33,7 @@ export const AccountPreferences = observer(
<Credentials application={application} />
<Sync application={application} />
<SubscriptionWrapper application={application} />
<LogOutWrapper application={application} appState={appState} />
<SignOutWrapper application={application} appState={appState} />
</PreferencesPane>
);
}

View File

@@ -1,41 +1,65 @@
import { Button } from "@/components/Button";
import { PreferencesGroup, PreferencesSegment, Subtitle, Text, Title } from "@/preferences/components";
import { WebApplication } from "@/ui_models/application";
import { AppState } from "@/ui_models/app_state";
import { observer } from "mobx-react-lite";
import { FunctionComponent } from "preact";
import { AccountMenuPane } from '@/components/AccountMenu';
import { Button } from '@/components/Button';
import {
PreferencesGroup,
PreferencesSegment,
Subtitle,
Text,
Title,
} from '@/preferences/components';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
export const Authentication: FunctionComponent<{ application: WebApplication, appState: AppState }> =
observer(({ appState }) => {
export const Authentication: FunctionComponent<{
application: WebApplication;
appState: AppState;
}> = observer(({ appState }) => {
const clickSignIn = () => {
appState.preferences.closePreferences();
appState.accountMenu.setCurrentPane(AccountMenuPane.SignIn);
appState.accountMenu.setShow(true);
};
const clickSignIn = () => {
appState.preferences.closePreferences();
appState.accountMenu.setShowLogin(true);
appState.accountMenu.setShow(true);
};
const clickRegister = () => {
appState.preferences.closePreferences();
appState.accountMenu.setCurrentPane(AccountMenuPane.Register);
appState.accountMenu.setShow(true);
};
const clickRegister = () => {
appState.preferences.closePreferences();
appState.accountMenu.setShowRegister(true);
appState.accountMenu.setShow(true);
};
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-col items-center px-12">
<Title>You're not signed in</Title>
<Subtitle className="text-center">Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.</Subtitle>
<div className="min-h-3" />
<div className="flex flex-row w-full">
<Button type="primary" onClick={clickSignIn} label="Sign in" className="flex-grow" />
<div className="min-w-3" />
<Button type="primary" onClick={clickRegister} label="Register" className="flex-grow" />
</div>
<div className="min-h-3" />
<Text className="text-center">Standard Notes is free on every platform, and comes standard with sync and encryption.</Text>
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-col items-center px-12">
<Title>You're not signed in</Title>
<Subtitle className="text-center">
Sign in to sync your notes and preferences across all your devices
and enable end-to-end encryption.
</Subtitle>
<div className="min-h-3" />
<div className="flex flex-row w-full">
<Button
type="primary"
onClick={clickSignIn}
label="Sign in"
className="flex-grow"
/>
<div className="min-w-3" />
<Button
type="primary"
onClick={clickRegister}
label="Register"
className="flex-grow"
/>
</div>
</PreferencesSegment>
</PreferencesGroup>
);
});
<div className="min-h-3" />
<Text className="text-center">
Standard Notes is free on every platform, and comes standard with
sync and encryption.
</Text>
</div>
</PreferencesSegment>
</PreferencesGroup>
);
});

View File

@@ -1,6 +1,6 @@
import { Button } from '@/components/Button';
import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
import { OtherSessionsLogoutContainer } from '@/components/OtherSessionsLogout';
import { OtherSessionsSignOutContainer } from '@/components/OtherSessionsSignOut';
import {
PreferencesGroup,
PreferencesSegment,
@@ -13,30 +13,33 @@ import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
const LogOutView: FunctionComponent<{
const SignOutView: FunctionComponent<{
application: WebApplication;
appState: AppState;
}> = observer(({ application, appState }) => {
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Log out</Title>
<Title>Sign out</Title>
<div className="min-h-2" />
<Subtitle>Other devices</Subtitle>
<Text>Want to log out on all devices except this one?</Text>
<Text>Want to sign out on all devices except this one?</Text>
<div className="min-h-3" />
<div className="flex flex-row">
<Button
className="mr-3"
type="normal"
label="Log out other sessions"
label="Sign out other sessions"
onClick={() => {
appState.accountMenu.setOtherSessionsLogout(true);
appState.accountMenu.setOtherSessionsSignOut(true);
}}
/>
<Button type="normal" label="Manage sessions" onClick={() => appState.openSessionsModal()} />
<Button
type="normal"
label="Manage sessions"
onClick={() => appState.openSessionsModal()}
/>
</div>
</PreferencesSegment>
<PreferencesSegment>
@@ -45,20 +48,19 @@ const LogOutView: FunctionComponent<{
<div className="min-h-3" />
<Button
type="danger"
label="Log out and clear local data"
label="Sign out and clear local data"
onClick={() => {
appState.accountMenu.setSigningOut(true);
}}
/>
</PreferencesSegment>
</PreferencesGroup>
<OtherSessionsLogoutContainer appState={appState} application={application} />
<ConfirmSignoutContainer
<OtherSessionsSignOutContainer
appState={appState}
application={application}
/>
<ConfirmSignoutContainer appState={appState} application={application} />
</>
);
});
@@ -85,19 +87,19 @@ const ClearSessionDataView: FunctionComponent<{
</PreferencesSegment>
</PreferencesGroup>
<ConfirmSignoutContainer
appState={appState}
application={application}
/>
</>);
<ConfirmSignoutContainer appState={appState} application={application} />
</>
);
});
export const LogOutWrapper: FunctionComponent<{
export const SignOutWrapper: FunctionComponent<{
application: WebApplication;
appState: AppState;
}> = observer(({ application, appState }) => {
const isLoggedIn = application.getUser() != undefined;
if (!isLoggedIn) return <ClearSessionDataView appState={appState} application={application} />;
return <LogOutView appState={appState} application={application} />;
if (!isLoggedIn)
return (
<ClearSessionDataView appState={appState} application={application} />
);
return <SignOutView appState={appState} application={application} />;
});

View File

@@ -1,4 +1,9 @@
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/preferences/components';
import {
PreferencesGroup,
PreferencesSegment,
Text,
Title,
} from '@/preferences/components';
import { Button } from '@/components/Button';
import { SyncQueueStrategy } from '@node_modules/@standardnotes/snjs';
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
@@ -12,48 +17,54 @@ type Props = {
application: WebApplication;
};
export const Sync: FunctionComponent<Props> = observer(({ application }: Props) => {
const formatLastSyncDate = (lastUpdatedDate: Date) => {
return dateToLocalizedString(lastUpdatedDate);
};
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
return dateToLocalizedString(lastUpdatedDate);
};
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.getLastSyncDate() as Date));
export const Sync: FunctionComponent<Props> = observer(
({ application }: Props) => {
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
const [lastSyncDate, setLastSyncDate] = useState(
formatLastSyncDate(application.getLastSyncDate() as Date)
);
const doSynchronization = async () => {
setIsSyncingInProgress(true);
const doSynchronization = async () => {
setIsSyncingInProgress(true);
const response = await application.sync({
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
checkIntegrity: true
});
setIsSyncingInProgress(false);
if (response && response.error) {
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
} else {
setLastSyncDate(formatLastSyncDate(application.getLastSyncDate() as Date));
}
};
const response = await application.sync({
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
checkIntegrity: true,
});
setIsSyncingInProgress(false);
if (response && response.error) {
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
} else {
setLastSyncDate(
formatLastSyncDate(application.getLastSyncDate() as Date)
);
}
};
return (
<PreferencesGroup>
<PreferencesSegment>
<div className='flex flex-row items-center'>
<div className='flex-grow flex flex-col'>
<Title>Sync</Title>
<Text>
Last synced <span className='font-bold'>on {lastSyncDate}</span>
</Text>
<Button
className='min-w-20 mt-3'
type='normal'
label='Sync now'
disabled={isSyncingInProgress}
onClick={doSynchronization}
/>
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex-grow flex flex-col">
<Title>Sync</Title>
<Text>
Last synced <span className="font-bold">on {lastSyncDate}</span>
</Text>
<Button
className="min-w-20 mt-3"
type="normal"
label="Sync now"
disabled={isSyncingInProgress}
onClick={doSynchronization}
/>
</div>
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>
);
});
</PreferencesSegment>
</PreferencesGroup>
);
}
);

View File

@@ -3,7 +3,7 @@ import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
ModalDialogLabel,
} from '@/components/shared/ModalDialog';
import { Button } from '@/components/Button';
import { FunctionalComponent } from 'preact';
@@ -15,29 +15,31 @@ import { useBeforeUnload } from '@/hooks/useBeforeUnload';
enum SubmitButtonTitles {
Default = 'Continue',
GeneratingKeys = 'Generating Keys...',
Finish = 'Finish'
Finish = 'Finish',
}
enum Steps {
InitialStep,
FinishStep
FinishStep,
}
type Props = {
onCloseDialog: () => void;
application: WebApplication;
}
};
export const ChangePassword: FunctionalComponent<Props> = ({
onCloseDialog,
application
application,
}) => {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('');
const [isContinuing, setIsContinuing] = useState(false);
const [lockContinue, setLockContinue] = useState(false);
const [submitButtonTitle, setSubmitButtonTitle] = useState(SubmitButtonTitles.Default);
const [submitButtonTitle, setSubmitButtonTitle] = useState(
SubmitButtonTitles.Default
);
const [currentStep, setCurrentStep] = useState(Steps.InitialStep);
useBeforeUnload();
@@ -46,16 +48,12 @@ export const ChangePassword: FunctionalComponent<Props> = ({
const validateCurrentPassword = async () => {
if (!currentPassword || currentPassword.length === 0) {
applicationAlertService.alert(
'Please enter your current password.'
);
applicationAlertService.alert('Please enter your current password.');
return false;
}
if (!newPassword || newPassword.length === 0) {
applicationAlertService.alert(
'Please enter a new password.'
);
applicationAlertService.alert('Please enter a new password.');
return false;
}
if (newPassword !== newPasswordConfirmation) {
@@ -67,7 +65,7 @@ export const ChangePassword: FunctionalComponent<Props> = ({
if (!application.getUser()?.email) {
applicationAlertService.alert(
'We don\'t have your email stored. Please log out then log back in to fix this issue.'
"We don't have your email stored. Please sign out then sign back in to fix this issue."
);
return false;
}
@@ -172,15 +170,15 @@ export const ChangePassword: FunctionalComponent<Props> = ({
<ModalDialogButtons>
{currentStep === Steps.InitialStep && (
<Button
className='min-w-20'
type='normal'
label='Cancel'
className="min-w-20"
type="normal"
label="Cancel"
onClick={handleDialogClose}
/>
)}
<Button
className='min-w-20'
type='primary'
className="min-w-20"
type="primary"
label={submitButtonTitle}
onClick={handleSubmit}
/>

View File

@@ -1,5 +1,5 @@
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
export { Sync } from './Sync';
export { Credentials } from './Credentials';
export { LogOutWrapper } from './LogOutView';
export { SignOutWrapper } from './SignOutView';
export { Authentication } from './Authentication';

View File

@@ -1,22 +1,36 @@
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import {
action,
computed,
makeObservable,
observable,
runInAction,
} from 'mobx';
import { ApplicationEvent, ContentType } from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application';
import { SNItem } from '@standardnotes/snjs/dist/@types/models/core/item';
import { AccountMenuPane } from '@/components/AccountMenu';
type StructuredItemsCount =
{ notes: number, tags: number, deleted: number, archived: number };
type StructuredItemsCount = {
notes: number;
tags: number;
deleted: number;
archived: number;
};
export class AccountMenuState {
show = false;
signingOut = false;
otherSessionsLogOut = false;
otherSessionsSignOut = false;
server: string | undefined = undefined;
enableServerOption = false;
notesAndTags: SNItem[] = [];
isEncryptionEnabled = false;
encryptionStatusString = '';
isBackupEncrypted = false;
showLogin = false;
showSignIn = false;
showRegister = false;
shouldAnimateCloseMenu = false;
currentPane = AccountMenuPane.GeneralMenu;
constructor(
private application: WebApplication,
@@ -25,14 +39,17 @@ export class AccountMenuState {
makeObservable(this, {
show: observable,
signingOut: observable,
otherSessionsLogOut: observable,
otherSessionsSignOut: observable,
server: observable,
enableServerOption: observable,
notesAndTags: observable,
isEncryptionEnabled: observable,
encryptionStatusString: observable,
isBackupEncrypted: observable,
showLogin: observable,
showSignIn: observable,
showRegister: observable,
currentPane: observable,
shouldAnimateCloseMenu: observable,
setShow: action,
toggleShow: action,
@@ -40,9 +57,11 @@ export class AccountMenuState {
setIsEncryptionEnabled: action,
setEncryptionStatusString: action,
setIsBackupEncrypted: action,
setOtherSessionsLogout: action,
setOtherSessionsSignOut: action,
setCurrentPane: action,
setEnableServerOption: action,
notesAndTagsCount: computed
notesAndTagsCount: computed,
});
this.addAppLaunchedEventObserver();
@@ -61,14 +80,14 @@ export class AccountMenuState {
streamNotesAndTags = (): void => {
this.appEventListeners.push(
this.application.streamItems(
[ContentType.Note, ContentType.Tag],
() => {
runInAction(() => {
this.notesAndTags = this.application.getItems([ContentType.Note, ContentType.Tag]);
});
}
)
this.application.streamItems([ContentType.Note, ContentType.Tag], () => {
runInAction(() => {
this.notesAndTags = this.application.getItems([
ContentType.Note,
ContentType.Tag,
]);
});
})
);
};
@@ -77,7 +96,12 @@ export class AccountMenuState {
};
closeAccountMenu = (): void => {
this.setShow(false);
this.shouldAnimateCloseMenu = true;
setTimeout(() => {
this.setShow(false);
this.shouldAnimateCloseMenu = false;
this.setCurrentPane(AccountMenuPane.GeneralMenu);
}, 150);
};
setSigningOut = (signingOut: boolean): void => {
@@ -88,6 +112,10 @@ export class AccountMenuState {
this.server = server;
};
setEnableServerOption = (enableServerOption: boolean): void => {
this.enableServerOption = enableServerOption;
};
setIsEncryptionEnabled = (isEncryptionEnabled: boolean): void => {
this.isEncryptionEnabled = isEncryptionEnabled;
};
@@ -100,8 +128,8 @@ export class AccountMenuState {
this.isBackupEncrypted = isBackupEncrypted;
};
setShowLogin = (showLogin: boolean): void => {
this.showLogin = showLogin;
setShowSignIn = (showSignIn: boolean): void => {
this.showSignIn = showSignIn;
};
setShowRegister = (showRegister: boolean): void => {
@@ -112,16 +140,25 @@ export class AccountMenuState {
this.show = !this.show;
};
setOtherSessionsLogout = (otherSessionsLogOut: boolean): void => {
this.otherSessionsLogOut = otherSessionsLogOut;
}
setOtherSessionsSignOut = (otherSessionsSignOut: boolean): void => {
this.otherSessionsSignOut = otherSessionsSignOut;
};
setCurrentPane = (pane: AccountMenuPane): void => {
this.currentPane = pane;
};
get notesAndTagsCount(): number {
return this.notesAndTags.length;
}
get structuredNotesAndTagsCount(): StructuredItemsCount {
const count: StructuredItemsCount = { notes: 0, archived: 0, deleted: 0, tags: 0 };
const count: StructuredItemsCount = {
notes: 0,
archived: 0,
deleted: 0,
tags: 0,
};
for (const item of this.notesAndTags) {
if (item.archived) {
count.archived++;
@@ -138,7 +175,6 @@ export class AccountMenuState {
if (item.content_type === ContentType.Tag) {
count.tags++;
}
}
return count;
}

View File

@@ -1,23 +1,32 @@
import { PreferenceId } from '@/preferences/PreferencesMenu';
import { action, computed, makeObservable, observable } from 'mobx';
export class PreferencesState {
private _open = false;
currentPane: PreferenceId = 'general';
constructor() {
makeObservable<PreferencesState, '_open'>(this, {
_open: observable,
currentPane: observable,
openPreferences: action,
closePreferences: action,
setCurrentPane: action,
isOpen: computed,
});
}
setCurrentPane = (prefId: PreferenceId): void => {
this.currentPane = prefId;
};
openPreferences = (): void => {
this._open = true;
};
closePreferences = (): void => {
this._open = false;
this.currentPane = 'general';
};
get isOpen() {

View File

@@ -69,14 +69,9 @@
ng-if='ctrl.newUpdateAvailable == true'
)
span.info.sk-label New update available.
.sk-app-bar-item.no-pointer(
ng-if='ctrl.lastSyncDate && !ctrl.isRefreshing'
)
.sk-label.subtle(ng-if='!ctrl.offline')
| Last refreshed {{ctrl.lastSyncDate}}
.sk-app-bar-item(
ng-click='ctrl.toggleSyncResolutionMenu()',
ng-if='(ctrl.state.outOfSync && !ctrl.isRefreshing) || ctrl.showSyncResolution'
ng-if='(ctrl.state.outOfSync) || ctrl.showSyncResolution'
)
.sk-label.warning(ng-if='ctrl.state.outOfSync') Potentially Out of Sync
sync-resolution-menu(
@@ -85,12 +80,8 @@
ng-if='ctrl.showSyncResolution',
application='ctrl.application'
)
.sk-app-bar-item(ng-if='ctrl.lastSyncDate && ctrl.isRefreshing')
.sk-spinner.small
.sk-app-bar-item(ng-if='ctrl.offline')
.sk-label Offline
.sk-app-bar-item(ng-click='ctrl.refreshData()' ng-if='!ctrl.offline')
.sk-label Refresh
.sk-app-bar-item.border(ng-if='ctrl.state.dockShortcuts.length > 0')
.sk-app-bar-item.dock-shortcut(ng-repeat='shortcut in ctrl.state.dockShortcuts')
.sk-app-bar-item-column(

View File

@@ -1,10 +1,9 @@
import { RootScopeMessages } from './../../messages';
import { ApplicationGroup } from '@/ui_models/application_group';
import { WebDirective } from '@/types';
import { dateToLocalizedString, preventRefreshing } from '@/utils';
import { preventRefreshing } from '@/utils';
import {
ApplicationEvent,
SyncQueueStrategy,
ContentType,
SNComponent,
SNTheme,
@@ -14,7 +13,6 @@ import {
import template from './footer-view.pug';
import { AppStateEvent, EventSource } from '@/ui_models/app_state';
import {
STRING_GENERIC_SYNC_ERROR,
STRING_NEW_UPDATE_READY,
STRING_CONFIRM_APP_QUIT_DURING_UPGRADE,
STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT,
@@ -23,6 +21,7 @@ import {
} from '@/strings';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { alertDialog, confirmDialog } from '@/services/alertService';
import { AccountMenuPane } from '@/components/AccountMenu';
/**
* Disable before production release.
@@ -69,8 +68,6 @@ class FooterViewCtrl extends PureViewCtrl<
private queueExtReload = false;
private reloadInProgress = false;
public hasError = false;
public isRefreshing = false;
public lastSyncDate?: string;
public newUpdateAvailable = false;
public dockShortcuts: DockShortcut[] = [];
public roomShowState: Partial<Record<string, boolean>> = {};
@@ -267,7 +264,6 @@ class FooterViewCtrl extends PureViewCtrl<
this.appState.accountMenu.setShow(true);
}
}
this.syncUpdated();
this.findErrors();
this.updateOfflineStatus();
break;
@@ -463,37 +459,13 @@ class FooterViewCtrl extends PureViewCtrl<
closeAccountMenu() {
this.appState.accountMenu.setShow(false);
this.appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu);
}
lockApp() {
this.application.lock();
}
refreshData() {
this.isRefreshing = true;
this.application
.sync({
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
checkIntegrity: true,
})
.then((response) => {
this.$timeout(() => {
this.isRefreshing = false;
}, 200);
if (response && response.error) {
this.application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
} else {
this.syncUpdated();
}
});
}
syncUpdated() {
this.lastSyncDate = dateToLocalizedString(
this.application.getLastSyncDate()!
);
}
onNewUpdateAvailable() {
this.newUpdateAvailable = true;
}
@@ -581,7 +553,7 @@ class FooterViewCtrl extends PureViewCtrl<
if (this.application && this.application.authenticationInProgress()) {
return;
}
this.appState.accountMenu.setShow(false);
this.appState.accountMenu.closeAccountMenu();
}
clickPreferences() {