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

@@ -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';