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:
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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>
|
||||
)}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
137
app/assets/javascripts/components/AccountMenu/CreateAccount.tsx
Normal file
137
app/assets/javascripts/components/AccountMenu/CreateAccount.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
You’re 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 & 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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
227
app/assets/javascripts/components/AccountMenu/SignIn.tsx
Normal file
227
app/assets/javascripts/components/AccountMenu/SignIn.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
32
app/assets/javascripts/components/Checkbox.tsx
Normal file
32
app/assets/javascripts/components/Checkbox.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
89
app/assets/javascripts/components/InputWithIcon.tsx
Normal file
89
app/assets/javascripts/components/InputWithIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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'
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -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: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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} />;
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user