Merge branch 'release/10.3.0'

This commit is contained in:
Mo Bitar
2021-12-10 11:59:20 -06:00
8 changed files with 268 additions and 227 deletions

View File

@@ -70,16 +70,19 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
<div className="my-0.5 font-bold">{user.email}</div> <div className="my-0.5 font-bold">{user.email}</div>
<span className="color-neutral">{application.getHost()}</span> <span className="color-neutral">{application.getHost()}</span>
</div> </div>
<div className="flex items-center justify-between px-3 mb-2"> <div className="flex items-start justify-between px-3 mb-2">
{isSyncingInProgress ? ( {isSyncingInProgress ? (
<div className="flex items-center color-info font-semibold"> <div className="flex items-center color-info font-semibold">
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div> <div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
Syncing... Syncing...
</div> </div>
) : ( ) : (
<div className="flex items-center success font-semibold"> <div className="flex items-start">
<Icon type="check-circle" className="mr-2" /> <Icon type="check-circle" className="mr-2 success" />
Last synced: {lastSyncDate} <div>
<div class="font-semibold success">Last synced:</div>
<div class="color-text">{lastSyncDate}</div>
</div>
</div> </div>
)} )}
<div <div

View File

@@ -34,7 +34,7 @@ export const SignInPane: FunctionComponent<Props> = observer(
useEffect(() => { useEffect(() => {
if (emailInputRef?.current) { if (emailInputRef?.current) {
emailInputRef.current!.focus(); emailInputRef.current?.focus();
} }
}, []); }, []);
@@ -73,8 +73,8 @@ export const SignInPane: FunctionComponent<Props> = observer(
const signIn = () => { const signIn = () => {
setIsSigningIn(true); setIsSigningIn(true);
emailInputRef?.current!.blur(); emailInputRef?.current?.blur();
passwordInputRef?.current!.blur(); passwordInputRef?.current?.blur();
application application
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal) .signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
@@ -92,7 +92,7 @@ export const SignInPane: FunctionComponent<Props> = observer(
application.alertService.alert(err); application.alertService.alert(err);
} }
setPassword(''); setPassword('');
passwordInputRef?.current!.blur(); passwordInputRef?.current?.blur();
}) })
.finally(() => { .finally(() => {
setIsSigningIn(false); setIsSigningIn(false);
@@ -109,12 +109,12 @@ export const SignInPane: FunctionComponent<Props> = observer(
e.preventDefault(); e.preventDefault();
if (!email || email.length === 0) { if (!email || email.length === 0) {
emailInputRef?.current!.focus(); emailInputRef?.current?.focus();
return; return;
} }
if (!password || password.length === 0) { if (!password || password.length === 0) {
passwordInputRef?.current!.focus(); passwordInputRef?.current?.focus();
return; return;
} }
@@ -134,69 +134,67 @@ export const SignInPane: FunctionComponent<Props> = observer(
/> />
<div className="sn-account-menu-headline">Sign in</div> <div className="sn-account-menu-headline">Sign in</div>
</div> </div>
<form onSubmit={handleSignInFormSubmit}> <div className="px-3 mb-1">
<div className="px-3 mb-1"> <InputWithIcon
<InputWithIcon className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`}
className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`} icon="email"
icon="email" inputType="email"
inputType="email" placeholder="Email"
placeholder="Email" value={email}
value={email} onChange={handleEmailChange}
onChange={handleEmailChange} onFocus={resetInvalid}
onFocus={resetInvalid} onKeyDown={handleKeyDown}
onKeyDown={handleKeyDown} disabled={isSigningIn}
disabled={isSigningIn} ref={emailInputRef}
ref={emailInputRef} />
/> <InputWithIcon
<InputWithIcon className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`}
className={`mb-2 ${isInvalid ? 'border-dark-red' : null}`} icon="password"
icon="password" inputType={showPassword ? 'text' : 'password'}
inputType={showPassword ? 'text' : 'password'} placeholder="Password"
placeholder="Password" value={password}
value={password} onChange={handlePasswordChange}
onChange={handlePasswordChange} onFocus={resetInvalid}
onFocus={resetInvalid} onKeyDown={handleKeyDown}
onKeyDown={handleKeyDown} disabled={isSigningIn}
disabled={isSigningIn} toggle={{
toggle={{ toggleOnIcon: 'eye-off',
toggleOnIcon: 'eye-off', toggleOffIcon: 'eye',
toggleOffIcon: 'eye', title: 'Show password',
title: 'Show password', toggled: showPassword,
toggled: showPassword, onClick: setShowPassword,
onClick: setShowPassword, }}
}} ref={passwordInputRef}
ref={passwordInputRef} />
/> {isInvalid ? (
{isInvalid ? ( <div className="color-dark-red my-2">
<div className="color-dark-red my-2"> Invalid email or password.
Invalid email or password. </div>
</div> ) : null}
) : null} <Button
<Button className="btn-w-full mt-1 mb-3"
className="btn-w-full mt-1 mb-3" label={isSigningIn ? 'Signing in...' : 'Sign in'}
label={isSigningIn ? 'Signing in...' : 'Sign in'} type="primary"
type="primary" onClick={handleSignInFormSubmit}
onClick={handleSignInFormSubmit} disabled={isSigningIn}
disabled={isSigningIn} />
/> <Checkbox
name="is-ephemeral"
label="Stay signed in"
checked={!isEphemeral}
disabled={isSigningIn}
onChange={handleEphemeralChange}
/>
{notesAndTagsCount > 0 ? (
<Checkbox <Checkbox
name="is-ephemeral" name="should-merge-local"
label="Stay signed in" label={`Merge local data (${notesAndTagsCount} notes and tags)`}
checked={!isEphemeral} checked={shouldMergeLocal}
disabled={isSigningIn} disabled={isSigningIn}
onChange={handleEphemeralChange} onChange={handleShouldMergeChange}
/> />
{notesAndTagsCount > 0 ? ( ) : null}
<Checkbox </div>
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> <div className="h-1px my-2 bg-border"></div>
<AdvancedOptions <AdvancedOptions
appState={appState} appState={appState}

View File

@@ -2,11 +2,11 @@ import { WebApplication } from '@/ui_models/application';
import { CollectionSort, PrefKey } from '@standardnotes/snjs'; import { CollectionSort, PrefKey } from '@standardnotes/snjs';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact'; import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks'; import { useRef, useState } from 'preact/hooks';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { Menu } from './menu/Menu'; import { Menu } from './menu/Menu';
import { MenuItem, MenuItemSeparator, MenuItemType } from './menu/MenuItem'; import { MenuItem, MenuItemSeparator, MenuItemType } from './menu/MenuItem';
import { toDirective } from './utils'; import { toDirective, useCloseOnClickOutside } from './utils';
type Props = { type Props = {
application: WebApplication; application: WebApplication;
@@ -108,8 +108,16 @@ flex flex-col py-2 bottom-0 left-2 absolute';
application.setPreference(PrefKey.NotesHideProtected, !hideProtected); application.setPreference(PrefKey.NotesHideProtected, !hideProtected);
}; };
const menuRef = useRef<HTMLDivElement>(null);
useCloseOnClickOutside(menuRef as any, (open: boolean) => {
if (!open) {
setShowMenuFalse();
}
});
return ( return (
<div className={menuClassName}> <div ref={menuRef} className={menuClassName}>
<Menu a11yLabel="Sort by" closeMenu={setShowMenuFalse}> <Menu a11yLabel="Sort by" closeMenu={setShowMenuFalse}>
<div className="px-3 my-1 text-xs font-semibold color-text uppercase"> <div className="px-3 my-1 text-xs font-semibold color-text uppercase">
Sort by Sort by

View File

@@ -76,55 +76,67 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
}, [focusModeEnabled]); }, [focusModeEnabled]);
const reloadThemes = useCallback(() => { const reloadThemes = useCallback(() => {
application.streamItems(ContentType.Theme, () => { const themes = application.getDisplayableItems(
const themes = application.getDisplayableItems( ContentType.Theme
ContentType.Theme ) as SNTheme[];
) as SNTheme[]; setThemes(
setThemes( themes.sort((a, b) => {
themes.sort((a, b) => { const aIsLayerable = a.isLayerable();
const aIsLayerable = a.isLayerable(); const bIsLayerable = b.isLayerable();
const bIsLayerable = b.isLayerable();
if (aIsLayerable && !bIsLayerable) { if (aIsLayerable && !bIsLayerable) {
return 1; return 1;
} else if (!aIsLayerable && bIsLayerable) { } else if (!aIsLayerable && bIsLayerable) {
return -1; return -1;
} else { } else {
return a.package_info.name.toLowerCase() < return a.package_info.name.toLowerCase() <
b.package_info.name.toLowerCase() b.package_info.name.toLowerCase()
? -1 ? -1
: 1; : 1;
} }
}) })
); );
setDefaultThemeOn( setDefaultThemeOn(
!themes.find((theme) => theme.active && !theme.isLayerable()) !themes.find((theme) => theme.active && !theme.isLayerable())
); );
});
}, [application]); }, [application]);
const reloadToggleableComponents = useCallback(() => { const reloadToggleableComponents = useCallback(() => {
application.streamItems(ContentType.Component, () => { const toggleableComponents = (
const toggleableComponents = ( application.getDisplayableItems(ContentType.Component) as SNComponent[]
application.getDisplayableItems( ).filter((component) =>
ContentType.Component [ComponentArea.EditorStack, ComponentArea.TagsList].includes(
) as SNComponent[] component.area
).filter((component) => )
[ComponentArea.EditorStack, ComponentArea.TagsList].includes( );
component.area setToggleableComponents(toggleableComponents);
)
);
setToggleableComponents(toggleableComponents);
});
}, [application]); }, [application]);
useEffect(() => { useEffect(() => {
reloadThemes(); const cleanupItemStream = application.streamItems(
}, [reloadThemes]); ContentType.Theme,
() => {
reloadThemes();
}
);
return () => {
cleanupItemStream();
};
}, [application, reloadThemes]);
useEffect(() => { useEffect(() => {
reloadToggleableComponents(); const cleanupItemStream = application.streamItems(
}, [reloadToggleableComponents]); ContentType.Component,
() => {
reloadToggleableComponents();
}
);
return () => {
cleanupItemStream();
};
}, [application, reloadToggleableComponents]);
useEffect(() => { useEffect(() => {
if (themesMenuOpen) { if (themesMenuOpen) {
@@ -274,10 +286,9 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
</Disclosure> </Disclosure>
) : null} ) : null}
{toggleableComponents.map((component) => ( {toggleableComponents.map((component) => (
<Switch <button
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none" className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
checked={component.active} onClick={() => {
onChange={() => {
toggleComponent(component); toggleComponent(component);
}} }}
> >
@@ -285,7 +296,8 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
<Icon type="window" className="color-neutral mr-2" /> <Icon type="window" className="color-neutral mr-2" />
{component.name} {component.name}
</div> </div>
</Switch> <Switch checked={component.active} className="px-0" />
</button>
))} ))}
<FocusModeSwitch <FocusModeSwitch
application={application} application={application}

View File

@@ -17,112 +17,124 @@ interface IProps {
appState: AppState; appState: AppState;
} }
export const OfflineSubscription: FunctionalComponent<IProps> = observer(({ application, appState }) => { export const OfflineSubscription: FunctionalComponent<IProps> = observer(
const [activationCode, setActivationCode] = useState(''); ({ application, appState }) => {
const [isSuccessfullyActivated, setIsSuccessfullyActivated] = useState(false); const [activationCode, setActivationCode] = useState('');
const [isSuccessfullyRemoved, setIsSuccessfullyRemoved] = useState(false); const [isSuccessfullyActivated, setIsSuccessfullyActivated] =
const [hasUserPreviouslyStoredCode, setHasUserPreviouslyStoredCode] = useState(false); useState(false);
const [isSuccessfullyRemoved, setIsSuccessfullyRemoved] = useState(false);
const [hasUserPreviouslyStoredCode, setHasUserPreviouslyStoredCode] =
useState(false);
useEffect(() => { useEffect(() => {
if (application.hasOfflineRepo()) { if (application.hasOfflineRepo()) {
setHasUserPreviouslyStoredCode(true); setHasUserPreviouslyStoredCode(true);
}
}, [application]);
const shouldShowOfflineSubscription = () => {
return !application.hasAccount() || application.isThirdPartyHostUsed();
};
const handleSubscriptionCodeSubmit = async (
event: TargetedEvent<HTMLFormElement, Event>
) => {
event.preventDefault();
const result = await application.setOfflineFeaturesCode(activationCode);
if (result?.error) {
await application.alertService.alert(result.error);
} else {
setIsSuccessfullyActivated(true);
setHasUserPreviouslyStoredCode(true);
setIsSuccessfullyRemoved(false);
}
};
const handleRemoveOfflineKey = async () => {
await application.deleteOfflineFeatureRepo();
setIsSuccessfullyActivated(false);
setHasUserPreviouslyStoredCode(false);
setActivationCode('');
setIsSuccessfullyRemoved(true);
};
const handleRemoveClick = async () => {
application.alertService
.confirm(
STRING_REMOVE_OFFLINE_KEY_CONFIRMATION,
'Remove offline key?',
'Remove Offline Key',
ButtonType.Danger,
'Cancel'
)
.then(async (shouldRemove: boolean) => {
if (shouldRemove) {
await handleRemoveOfflineKey();
}
})
.catch((err: string) => {
application.alertService.alert(err);
});
};
if (!shouldShowOfflineSubscription()) {
return null;
} }
}, [application]);
const shouldShowOfflineSubscription = () => { return (
return !application.hasAccount() || application.isCustomServerHostUsed(); <>
}; <div className="flex items-center justify-between">
<div className="flex flex-col mt-3 w-full">
const handleSubscriptionCodeSubmit = async (event: TargetedEvent<HTMLFormElement, Event>) => { <Subtitle>
event.preventDefault(); {!hasUserPreviouslyStoredCode && 'Activate'} Offline Subscription
</Subtitle>
const result = await application.setOfflineFeaturesCode(activationCode); <form onSubmit={handleSubscriptionCodeSubmit}>
<div className={'mt-2'}>
if (result?.error) { {!hasUserPreviouslyStoredCode && (
await application.alertService.alert(result.error); <DecoratedInput
} else { onChange={(code) => setActivationCode(code)}
setIsSuccessfullyActivated(true); placeholder={'Offline Subscription Code'}
setHasUserPreviouslyStoredCode(true); text={activationCode}
setIsSuccessfullyRemoved(false); disabled={isSuccessfullyActivated}
} className={'mb-3'}
}; />
)}
const handleRemoveOfflineKey = async () => { </div>
await application.deleteOfflineFeatureRepo(); {(isSuccessfullyActivated || isSuccessfullyRemoved) && (
<div className={'mt-3 mb-3 info'}>
setIsSuccessfullyActivated(false); Your offline subscription code has been successfully{' '}
setHasUserPreviouslyStoredCode(false); {isSuccessfullyActivated ? 'activated' : 'removed'}.
setActivationCode(''); </div>
setIsSuccessfullyRemoved(true); )}
}; {hasUserPreviouslyStoredCode && (
<Button
const handleRemoveClick = async () => { type="danger"
application.alertService.confirm( label="Remove offline key"
STRING_REMOVE_OFFLINE_KEY_CONFIRMATION, onClick={() => {
'Remove offline key?', handleRemoveClick();
'Remove Offline Key', }}
ButtonType.Danger,
'Cancel'
)
.then(async (shouldRemove: boolean) => {
if (shouldRemove) {
await handleRemoveOfflineKey();
}
})
.catch((err: string) => {
application.alertService.alert(err);
});
};
if (!shouldShowOfflineSubscription()) {
return null;
}
return (
<>
<div className='flex items-center justify-between'>
<div className='flex flex-col mt-3 w-full'>
<Subtitle>{!hasUserPreviouslyStoredCode && 'Activate'} Offline Subscription</Subtitle>
<form onSubmit={handleSubscriptionCodeSubmit}>
<div className={'mt-2'}>
{!hasUserPreviouslyStoredCode && (
<DecoratedInput
onChange={(code) => setActivationCode(code)}
placeholder={'Offline Subscription Code'}
text={activationCode}
disabled={isSuccessfullyActivated}
className={'mb-3'}
/> />
)} )}
</div> {!hasUserPreviouslyStoredCode && !isSuccessfullyActivated && (
{(isSuccessfullyActivated || isSuccessfullyRemoved) && ( <Button
<div className={'mt-3 mb-3 info'}> label={'Submit'}
Your offline subscription code has been successfully {isSuccessfullyActivated ? 'activated' : 'removed'}. type="primary"
</div> disabled={activationCode === ''}
)} onClick={(event) =>
{hasUserPreviouslyStoredCode && ( handleSubscriptionCodeSubmit(
<Button event as TargetedEvent<HTMLFormElement>
type='danger' )
label='Remove offline key' }
onClick={() => { />
handleRemoveClick(); )}
}} </form>
/> </div>
)}
{!hasUserPreviouslyStoredCode && !isSuccessfullyActivated && (
<Button
label={'Submit'}
type='primary'
disabled={activationCode === ''}
onClick={(event) =>
handleSubscriptionCodeSubmit(event as TargetedEvent<HTMLFormElement>)
}
/>
)}
</form>
</div> </div>
</div> <HorizontalSeparator classes="mt-8 mb-5" />
<HorizontalSeparator classes='mt-8 mb-5' /> </>
</> );
); }
}); );

View File

@@ -256,6 +256,10 @@
margin-right: 0.75rem; margin-right: 0.75rem;
} }
.mr-4 {
margin-right: 1rem;
}
.mr-12 { .mr-12 {
margin-right: 3rem; margin-right: 3rem;
} }
@@ -283,6 +287,10 @@
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.max-w-1\/2 {
max-width: 50%;
}
.max-w-3\/4 { .max-w-3\/4 {
max-width: 75%; max-width: 75%;
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "standard-notes-web", "name": "standard-notes-web",
"version": "3.9.8", "version": "3.9.10",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -85,7 +85,7 @@
"@reach/listbox": "^0.16.2", "@reach/listbox": "^0.16.2",
"@standardnotes/features": "1.10.2", "@standardnotes/features": "1.10.2",
"@standardnotes/sncrypto-web": "1.5.3", "@standardnotes/sncrypto-web": "1.5.3",
"@standardnotes/snjs": "2.20.1", "@standardnotes/snjs": "2.20.3",
"mobx": "^6.3.5", "mobx": "^6.3.5",
"mobx-react-lite": "^3.2.2", "mobx-react-lite": "^3.2.2",
"preact": "^10.5.15", "preact": "^10.5.15",

View File

@@ -2614,10 +2614,10 @@
buffer "^6.0.3" buffer "^6.0.3"
libsodium-wrappers "^0.7.9" libsodium-wrappers "^0.7.9"
"@standardnotes/snjs@2.20.1": "@standardnotes/snjs@2.20.3":
version "2.20.1" version "2.20.3"
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.20.1.tgz#4813adbfd16a1c373357bd4c7ece3085abf142b4" resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.20.3.tgz#11fe962dfb017be459e856b9fbc6311c5046a0b0"
integrity sha512-wJILt7YerLFaZTKoIZaCkqSuvpVWoVKqCVjP0KNHrMknnbgMHZygHqrb9sr57JL1AcNCkAULEFS/kev2GYTG3Q== integrity sha512-FHog3p3SuMvTXsEl76UenbjY8PS6VL/b6MjaC9whJR5ZwsvmLwE0G16dWZ+z+izm+CsRaHMWU8R2cy7RG7HjZg==
dependencies: dependencies:
"@standardnotes/auth" "^3.8.1" "@standardnotes/auth" "^3.8.1"
"@standardnotes/common" "^1.2.1" "@standardnotes/common" "^1.2.1"