chore: prettier files

This commit is contained in:
Mo
2022-05-03 10:51:40 -05:00
parent f5a90060ea
commit 43d94fbcbf
127 changed files with 1365 additions and 2428 deletions

View File

@@ -8,10 +8,7 @@ import { findDOMNode, unmountComponentAtNode } from 'preact/compat'
export type PureComponentState = Partial<Record<string, any>> export type PureComponentState = Partial<Record<string, any>>
export type PureComponentProps = Partial<Record<string, any>> export type PureComponentProps = Partial<Record<string, any>>
export abstract class PureComponent< export abstract class PureComponent<P = PureComponentProps, S = PureComponentState> extends Component<P, S> {
P = PureComponentProps,
S = PureComponentState,
> extends Component<P, S> {
private unsubApp!: () => void private unsubApp!: () => void
private unsubState!: () => void private unsubState!: () => void
private reactionDisposers: IReactionDisposer[] = [] private reactionDisposers: IReactionDisposer[] = []

View File

@@ -107,10 +107,8 @@ export const ConfirmPassword: FunctionComponent<Props> = observer(
</div> </div>
<div className="px-3 mb-3 text-sm"> <div className="px-3 mb-3 text-sm">
Because your notes are encrypted using your password,{' '} Because your notes are encrypted using your password,{' '}
<span className="color-dark-red"> <span className="color-dark-red">Standard Notes does not have a password reset option</span>. If you forget
Standard Notes does not have a password reset option your password, you will permanently lose access to your data.
</span>
. If you forget your password, you will permanently lose access to your data.
</div> </div>
<form onSubmit={handleConfirmFormSubmit} className="px-3 mb-1"> <form onSubmit={handleConfirmFormSubmit} className="px-3 mb-1">
<DecoratedPasswordInput <DecoratedPasswordInput

View File

@@ -111,19 +111,10 @@ export const CreateAccount: FunctionComponent<Props> = observer(
ref={passwordInputRef} ref={passwordInputRef}
value={password} value={password}
/> />
<Button <Button className="btn-w-full mt-1" label="Next" variant="primary" onClick={handleRegisterFormSubmit} />
className="btn-w-full mt-1"
label="Next"
variant="primary"
onClick={handleRegisterFormSubmit}
/>
</form> </form>
<div className="h-1px my-2 bg-border"></div> <div className="h-1px my-2 bg-border"></div>
<AdvancedOptions <AdvancedOptions application={application} appState={appState} onVaultChange={onVaultChange} />
application={application}
appState={appState}
onVaultChange={onVaultChange}
/>
</> </>
) )
}, },

View File

@@ -26,9 +26,7 @@ const iconClassName = 'color-neutral mr-2'
export const GeneralAccountMenu: FunctionComponent<Props> = observer( export const GeneralAccountMenu: FunctionComponent<Props> = observer(
({ application, appState, setMenuPane, closeMenu, mainApplicationGroup }) => { ({ application, appState, setMenuPane, closeMenu, mainApplicationGroup }) => {
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false) const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
const [lastSyncDate, setLastSyncDate] = useState( const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
formatLastSyncDate(application.sync.getLastSyncDate() as Date),
)
const doSynchronization = async () => { const doSynchronization = async () => {
setIsSyncingInProgress(true) setIsSyncingInProgress(true)
@@ -97,8 +95,8 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
<> <>
<div className="px-3 mb-1"> <div className="px-3 mb-1">
<div className="mb-3 color-foreground"> <div className="mb-3 color-foreground">
Youre offline. Sign in to sync your notes and preferences across all your devices Youre offline. Sign in to sync your notes and preferences across all your devices and enable end-to-end
and enable end-to-end encryption. encryption.
</div> </div>
<div className="flex items-center color-grey-1"> <div className="flex items-center color-grey-1">
<Icon type="cloud-off" className="mr-2" /> <Icon type="cloud-off" className="mr-2" />
@@ -114,10 +112,7 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
initialFocus={!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX} initialFocus={!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX}
> >
<MenuItemSeparator /> <MenuItemSeparator />
<WorkspaceSwitcherOption <WorkspaceSwitcherOption mainApplicationGroup={mainApplicationGroup} appState={appState} />
mainApplicationGroup={mainApplicationGroup}
appState={appState}
/>
<MenuItemSeparator /> <MenuItemSeparator />
{user ? ( {user ? (
<MenuItem <MenuItem

View File

@@ -19,188 +19,186 @@ type Props = {
setMenuPane: (pane: AccountMenuPane) => void setMenuPane: (pane: AccountMenuPane) => void
} }
export const SignInPane: FunctionComponent<Props> = observer( export const SignInPane: FunctionComponent<Props> = observer(({ application, appState, setMenuPane }) => {
({ application, appState, setMenuPane }) => { const { notesAndTagsCount } = appState.accountMenu
const { notesAndTagsCount } = appState.accountMenu const [email, setEmail] = useState('')
const [email, setEmail] = useState('') const [password, setPassword] = useState('')
const [password, setPassword] = useState('') const [error, setError] = useState('')
const [error, setError] = useState('') const [isEphemeral, setIsEphemeral] = useState(false)
const [isEphemeral, setIsEphemeral] = useState(false)
const [isStrictSignin, setIsStrictSignin] = useState(false) const [isStrictSignin, setIsStrictSignin] = useState(false)
const [isSigningIn, setIsSigningIn] = useState(false) const [isSigningIn, setIsSigningIn] = useState(false)
const [shouldMergeLocal, setShouldMergeLocal] = useState(true) const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
const [isVault, setIsVault] = useState(false) const [isVault, setIsVault] = useState(false)
const emailInputRef = useRef<HTMLInputElement>(null) const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null) const passwordInputRef = useRef<HTMLInputElement>(null)
useEffect(() => { useEffect(() => {
if (emailInputRef?.current) { if (emailInputRef?.current) {
emailInputRef.current?.focus() emailInputRef.current?.focus()
}
if (isDev && window.devAccountEmail) {
setEmail(window.devAccountEmail)
setPassword(window.devAccountPassword as string)
}
}, [])
const resetInvalid = () => {
if (error.length) {
setError('')
}
} }
if (isDev && window.devAccountEmail) {
const handleEmailChange = (text: string) => { setEmail(window.devAccountEmail)
setEmail(text) setPassword(window.devAccountPassword as string)
} }
}, [])
const handlePasswordChange = (text: string) => { const resetInvalid = () => {
if (error.length) { if (error.length) {
setError('') setError('')
}
setPassword(text)
} }
}
const handleEphemeralChange = () => { const handleEmailChange = (text: string) => {
setIsEphemeral(!isEphemeral) setEmail(text)
}
const handlePasswordChange = (text: string) => {
if (error.length) {
setError('')
} }
setPassword(text)
}
const handleStrictSigninChange = () => { const handleEphemeralChange = () => {
setIsStrictSignin(!isStrictSignin) setIsEphemeral(!isEphemeral)
} }
const handleShouldMergeChange = () => { const handleStrictSigninChange = () => {
setShouldMergeLocal(!shouldMergeLocal) setIsStrictSignin(!isStrictSignin)
} }
const signIn = () => { const handleShouldMergeChange = () => {
setIsSigningIn(true) setShouldMergeLocal(!shouldMergeLocal)
emailInputRef?.current?.blur() }
passwordInputRef?.current?.blur()
application const signIn = () => {
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal) setIsSigningIn(true)
.then((res) => { emailInputRef?.current?.blur()
if (res.error) { passwordInputRef?.current?.blur()
throw new Error(res.error.message)
}
appState.accountMenu.closeAccountMenu()
})
.catch((err) => {
console.error(err)
setError(err.message ?? err.toString())
setPassword('')
passwordInputRef?.current?.blur()
})
.finally(() => {
setIsSigningIn(false)
})
}
const handleKeyDown = (e: KeyboardEvent) => { application
if (e.key === 'Enter') { .signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
handleSignInFormSubmit(e) .then((res) => {
} if (res.error) {
} throw new Error(res.error.message)
const onVaultChange = useCallback(
(newIsVault: boolean, vaultedEmail?: string) => {
setIsVault(newIsVault)
if (newIsVault && vaultedEmail) {
setEmail(vaultedEmail)
} }
}, appState.accountMenu.closeAccountMenu()
[setEmail], })
) .catch((err) => {
console.error(err)
setError(err.message ?? err.toString())
setPassword('')
passwordInputRef?.current?.blur()
})
.finally(() => {
setIsSigningIn(false)
})
}
const handleSignInFormSubmit = (e: Event) => { const handleKeyDown = (e: KeyboardEvent) => {
e.preventDefault() if (e.key === 'Enter') {
handleSignInFormSubmit(e)
}
}
if (!email || email.length === 0) { const onVaultChange = useCallback(
emailInputRef?.current?.focus() (newIsVault: boolean, vaultedEmail?: string) => {
return setIsVault(newIsVault)
if (newIsVault && vaultedEmail) {
setEmail(vaultedEmail)
} }
},
[setEmail],
)
if (!password || password.length === 0) { const handleSignInFormSubmit = (e: Event) => {
passwordInputRef?.current?.focus() e.preventDefault()
return
}
signIn() if (!email || email.length === 0) {
emailInputRef?.current?.focus()
return
} }
return ( if (!password || password.length === 0) {
<> passwordInputRef?.current?.focus()
<div className="flex items-center px-3 mt-1 mb-3"> return
<IconButton }
icon="arrow-left"
title="Go back" signIn()
className="flex mr-2 color-neutral p-0" }
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
focusable={true} return (
disabled={isSigningIn} <>
/> <div className="flex items-center px-3 mt-1 mb-3">
<div className="sn-account-menu-headline">Sign in</div> <IconButton
</div> icon="arrow-left"
<div className="px-3 mb-1"> title="Go back"
<DecoratedInput className="flex mr-2 color-neutral p-0"
className={`mb-2 ${error ? 'border-dark-red' : null}`} onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
left={[<Icon type="email" className="color-neutral" />]} focusable={true}
type="email"
placeholder="Email"
value={email}
onChange={handleEmailChange}
onFocus={resetInvalid}
onKeyDown={handleKeyDown}
disabled={isSigningIn || isVault}
ref={emailInputRef}
/>
<DecoratedPasswordInput
className={`mb-2 ${error ? 'border-dark-red' : null}`}
disabled={isSigningIn}
left={[<Icon type="password" className="color-neutral" />]}
onChange={handlePasswordChange}
onFocus={resetInvalid}
onKeyDown={handleKeyDown}
placeholder="Password"
ref={passwordInputRef}
value={password}
/>
{error ? <div className="color-dark-red my-2">{error}</div> : null}
<Button
className="btn-w-full mt-1 mb-3"
label={isSigningIn ? 'Signing in...' : 'Sign in'}
variant="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>
<div className="h-1px my-2 bg-border"></div>
<AdvancedOptions
appState={appState}
application={application}
disabled={isSigningIn} disabled={isSigningIn}
onVaultChange={onVaultChange}
onStrictSignInChange={handleStrictSigninChange}
/> />
</> <div className="sn-account-menu-headline">Sign in</div>
) </div>
}, <div className="px-3 mb-1">
) <DecoratedInput
className={`mb-2 ${error ? 'border-dark-red' : null}`}
left={[<Icon type="email" className="color-neutral" />]}
type="email"
placeholder="Email"
value={email}
onChange={handleEmailChange}
onFocus={resetInvalid}
onKeyDown={handleKeyDown}
disabled={isSigningIn || isVault}
ref={emailInputRef}
/>
<DecoratedPasswordInput
className={`mb-2 ${error ? 'border-dark-red' : null}`}
disabled={isSigningIn}
left={[<Icon type="password" className="color-neutral" />]}
onChange={handlePasswordChange}
onFocus={resetInvalid}
onKeyDown={handleKeyDown}
placeholder="Password"
ref={passwordInputRef}
value={password}
/>
{error ? <div className="color-dark-red my-2">{error}</div> : null}
<Button
className="btn-w-full mt-1 mb-3"
label={isSigningIn ? 'Signing in...' : 'Sign in'}
variant="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>
<div className="h-1px my-2 bg-border"></div>
<AdvancedOptions
appState={appState}
application={application}
disabled={isSigningIn}
onVaultChange={onVaultChange}
onStrictSignInChange={handleStrictSigninChange}
/>
</>
)
})

View File

@@ -13,65 +13,55 @@ type Props = {
appState: AppState appState: AppState
} }
export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer( export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(({ mainApplicationGroup, appState }) => {
({ mainApplicationGroup, appState }) => { const buttonRef = useRef<HTMLButtonElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null) const menuRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null) const [isOpen, setIsOpen] = useState(false)
const [isOpen, setIsOpen] = useState(false) const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
const toggleMenu = () => { const toggleMenu = () => {
if (!isOpen) { if (!isOpen) {
const menuPosition = calculateSubmenuStyle(buttonRef.current) const menuPosition = calculateSubmenuStyle(buttonRef.current)
if (menuPosition) { if (menuPosition) {
setMenuStyle(menuPosition) setMenuStyle(menuPosition)
}
} }
setIsOpen(!isOpen)
} }
useEffect(() => { setIsOpen(!isOpen)
if (isOpen) { }
setTimeout(() => {
const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current)
if (newMenuPosition) { useEffect(() => {
setMenuStyle(newMenuPosition) if (isOpen) {
} setTimeout(() => {
}) const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current)
}
}, [isOpen])
return ( if (newMenuPosition) {
<> setMenuStyle(newMenuPosition)
<button }
ref={buttonRef} })
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none" }
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} }, [isOpen])
role="menuitem"
onClick={toggleMenu} return (
> <>
<div className="flex items-center"> <button
<Icon type="user-switch" className="color-neutral mr-2" /> ref={buttonRef}
Switch workspace className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
</div> tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
<Icon type="chevron-right" className="color-neutral" /> role="menuitem"
</button> onClick={toggleMenu}
{isOpen && ( >
<div <div className="flex items-center">
ref={menuRef} <Icon type="user-switch" className="color-neutral mr-2" />
className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto" Switch workspace
style={menuStyle} </div>
> <Icon type="chevron-right" className="color-neutral" />
<WorkspaceSwitcherMenu </button>
mainApplicationGroup={mainApplicationGroup} {isOpen && (
appState={appState} <div ref={menuRef} className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto" style={menuStyle}>
isOpen={isOpen} <WorkspaceSwitcherMenu mainApplicationGroup={mainApplicationGroup} appState={appState} isOpen={isOpen} />
/> </div>
</div> )}
)} </>
</> )
) })
},
)

View File

@@ -51,9 +51,7 @@ const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
/> />
) )
case AccountMenuPane.SignIn: case AccountMenuPane.SignIn:
return ( return <SignInPane appState={appState} application={application} setMenuPane={setMenuPane} />
<SignInPane appState={appState} application={application} setMenuPane={setMenuPane} />
)
case AccountMenuPane.Register: case AccountMenuPane.Register:
return ( return (
<CreateAccount <CreateAccount
@@ -82,8 +80,7 @@ const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
export const AccountMenu: FunctionComponent<Props> = observer( export const AccountMenu: FunctionComponent<Props> = observer(
({ application, appState, onClickOutside, mainApplicationGroup }) => { ({ application, appState, onClickOutside, mainApplicationGroup }) => {
const { currentPane, setCurrentPane, shouldAnimateCloseMenu, closeAccountMenu } = const { currentPane, setCurrentPane, shouldAnimateCloseMenu, closeAccountMenu } = appState.accountMenu
appState.accountMenu
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(ref, () => { useCloseOnClickOutside(ref, () => {

View File

@@ -21,12 +21,7 @@ interface Props {
* IconButton component with an icon * IconButton component with an icon
* preventDefault is already handled within the component * preventDefault is already handled within the component
*/ */
export const RoundIconButton: FunctionComponent<Props> = ({ export const RoundIconButton: FunctionComponent<Props> = ({ onClick, type, className, icon: iconType }) => {
onClick,
type,
className,
icon: iconType,
}) => {
const click = (e: MouseEvent) => { const click = (e: MouseEvent) => {
e.preventDefault() e.preventDefault()
onClick() onClick()

View File

@@ -34,10 +34,7 @@ type Props = {
onDismiss: (challenge: Challenge) => Promise<void> onDismiss: (challenge: Challenge) => Promise<void>
} }
const validateValues = ( const validateValues = (values: ChallengeModalValues, prompts: ChallengePrompt[]): ChallengeModalValues | undefined => {
values: ChallengeModalValues,
prompts: ChallengePrompt[],
): ChallengeModalValues | undefined => {
let hasInvalidValues = false let hasInvalidValues = false
const validatedValues = { ...values } const validatedValues = { ...values }
for (const prompt of prompts) { for (const prompt of prompts) {
@@ -75,10 +72,9 @@ export const ChallengeModal: FunctionComponent<Props> = ({
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
const [, setProcessingPrompts] = useState<ChallengePrompt[]>([]) const [, setProcessingPrompts] = useState<ChallengePrompt[]>([])
const [bypassModalFocusLock, setBypassModalFocusLock] = useState(false) const [bypassModalFocusLock, setBypassModalFocusLock] = useState(false)
const shouldShowForgotPasscode = [ const shouldShowForgotPasscode = [ChallengeReason.ApplicationUnlock, ChallengeReason.Migration].includes(
ChallengeReason.ApplicationUnlock, challenge.reason,
ChallengeReason.Migration, )
].includes(challenge.reason)
const shouldShowWorkspaceSwitcher = challenge.reason === ChallengeReason.ApplicationUnlock const shouldShowWorkspaceSwitcher = challenge.reason === ChallengeReason.ApplicationUnlock
const submit = async () => { const submit = async () => {
@@ -268,10 +264,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
</Button> </Button>
)} )}
{shouldShowWorkspaceSwitcher && ( {shouldShowWorkspaceSwitcher && (
<LockscreenWorkspaceSwitcher <LockscreenWorkspaceSwitcher mainApplicationGroup={mainApplicationGroup} appState={appState} />
mainApplicationGroup={mainApplicationGroup}
appState={appState}
/>
)} )}
</DialogContent> </DialogContent>
</DialogOverlay> </DialogOverlay>

View File

@@ -1,8 +1,4 @@
import { import { ChallengePrompt, ChallengeValidation, ProtectionSessionDurations } from '@standardnotes/snjs'
ChallengePrompt,
ChallengeValidation,
ProtectionSessionDurations,
} from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useEffect, useRef } from 'preact/hooks' import { useEffect, useRef } from 'preact/hooks'
import { DecoratedInput } from '@/Components/Input/DecoratedInput' import { DecoratedInput } from '@/Components/Input/DecoratedInput'
@@ -17,13 +13,7 @@ type Props = {
isInvalid: boolean isInvalid: boolean
} }
export const ChallengeModalPrompt: FunctionComponent<Props> = ({ export const ChallengeModalPrompt: FunctionComponent<Props> = ({ prompt, values, index, onValueChange, isInvalid }) => {
prompt,
values,
index,
onValueChange,
isInvalid,
}) => {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => { useEffect(() => {
@@ -49,9 +39,7 @@ export const ChallengeModalPrompt: FunctionComponent<Props> = ({
return ( return (
<label <label
className={`cursor-pointer px-2 py-1.5 rounded ${ className={`cursor-pointer px-2 py-1.5 rounded ${
selected selected ? 'bg-default color-foreground font-semibold' : 'color-grey-0 hover:bg-grey-3'
? 'bg-default color-foreground font-semibold'
: 'color-grey-0 hover:bg-grey-3'
}`} }`}
> >
<input <input
@@ -88,9 +76,7 @@ export const ChallengeModalPrompt: FunctionComponent<Props> = ({
onChange={(value) => onValueChange(value, prompt)} onChange={(value) => onValueChange(value, prompt)}
/> />
)} )}
{isInvalid && ( {isInvalid && <div className="text-sm color-danger mt-2">Invalid authentication, please try again.</div>}
<div className="text-sm color-danger mt-2">Invalid authentication, please try again.</div>
)}
</div> </div>
) )
} }

View File

@@ -13,10 +13,7 @@ type Props = {
appState: AppState appState: AppState
} }
export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplicationGroup, appState }) => {
mainApplicationGroup,
appState,
}) => {
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(null) const menuRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
@@ -51,20 +48,12 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
<Button <Button ref={buttonRef} onClick={toggleMenu} className="flex items-center justify-center min-w-76 mt-2">
ref={buttonRef}
onClick={toggleMenu}
className="flex items-center justify-center min-w-76 mt-2"
>
<Icon type="user-switch" className="color-neutral mr-2" /> <Icon type="user-switch" className="color-neutral mr-2" />
Switch workspace Switch workspace
</Button> </Button>
{isOpen && ( {isOpen && (
<div <div ref={menuRef} className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto" style={menuStyle}>
ref={menuRef}
className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto"
style={menuStyle}
>
<WorkspaceSwitcherMenu <WorkspaceSwitcherMenu
mainApplicationGroup={mainApplicationGroup} mainApplicationGroup={mainApplicationGroup}
appState={appState} appState={appState}

View File

@@ -8,13 +8,7 @@ type CheckboxProps = {
label: string label: string
} }
export const Checkbox: FunctionComponent<CheckboxProps> = ({ export const Checkbox: FunctionComponent<CheckboxProps> = ({ name, checked, onChange, disabled, label }) => {
name,
checked,
onChange,
disabled,
label,
}) => {
return ( return (
<label htmlFor={name} className="flex items-center fit-content mb-2"> <label htmlFor={name} className="flex items-center fit-content mb-2">
<input <input

View File

@@ -5,18 +5,13 @@ interface IProps {
dismissDeprecationMessage: () => void dismissDeprecationMessage: () => void
} }
export const IsDeprecated: FunctionalComponent<IProps> = ({ export const IsDeprecated: FunctionalComponent<IProps> = ({ deprecationMessage, dismissDeprecationMessage }) => {
deprecationMessage,
dismissDeprecationMessage,
}) => {
return ( return (
<div className={'sn-component'}> <div className={'sn-component'}>
<div className={'sk-app-bar no-edges no-top-edge dynamic-height'}> <div className={'sk-app-bar no-edges no-top-edge dynamic-height'}>
<div className={'left'}> <div className={'left'}>
<div className={'sk-app-bar-item'}> <div className={'sk-app-bar-item'}>
<div className={'sk-label warning'}> <div className={'sk-label warning'}>{deprecationMessage || 'This extension is deprecated.'}</div>
{deprecationMessage || 'This extension is deprecated.'}
</div>
</div> </div>
</div> </div>
<div className={'right'}> <div className={'right'}>

View File

@@ -7,21 +7,17 @@ export const OfflineRestricted: FunctionalComponent = () => {
<div className={'sk-panel-content'}> <div className={'sk-panel-content'}>
<div className={'sk-panel-section stretch'}> <div className={'sk-panel-section stretch'}>
<div className={'sk-panel-column'} /> <div className={'sk-panel-column'} />
<div className={'sk-h1 sk-bold'}> <div className={'sk-h1 sk-bold'}>You have restricted this component to not use a hosted version.</div>
You have restricted this component to not use a hosted version. <div className={'sk-subtitle'}>Locally-installed components are not available in the web application.</div>
</div>
<div className={'sk-subtitle'}>
Locally-installed components are not available in the web application.
</div>
<div className={'sk-panel-row'} /> <div className={'sk-panel-row'} />
<div className={'sk-panel-row'}> <div className={'sk-panel-row'}>
<div className={'sk-panel-column'}> <div className={'sk-panel-column'}>
<div className={'sk-p'}>To continue, choose from the following options:</div> <div className={'sk-p'}>To continue, choose from the following options:</div>
<ul> <ul>
<li className={'sk-p'}> <li className={'sk-p'}>
Enable the Hosted option for this component by opening the Preferences {'>'}{' '} Enable the Hosted option for this component by opening the Preferences {'>'} General {'>'} Advanced
General {'>'} Advanced Settings menu and toggling 'Use hosted when local is Settings menu and toggling 'Use hosted when local is unavailable' under this component's options.
unavailable' under this component's options. Then press Reload. Then press Reload.
</li> </li>
<li className={'sk-p'}>Use the desktop application.</li> <li className={'sk-p'}>Use the desktop application.</li>
</ul> </ul>

View File

@@ -10,13 +10,8 @@ export const UrlMissing: FunctionalComponent<IProps> = ({ componentName }) => {
<div className={'sk-panel static'}> <div className={'sk-panel static'}>
<div className={'sk-panel-content'}> <div className={'sk-panel-content'}>
<div className={'sk-panel-section stretch'}> <div className={'sk-panel-section stretch'}>
<div className={'sk-panel-section-title'}> <div className={'sk-panel-section-title'}>This extension is missing its URL property.</div>
This extension is missing its URL property. <p>In order to access your note immediately, please switch from {componentName} to the Plain Editor.</p>
</div>
<p>
In order to access your note immediately, please switch from {componentName} to the
Plain Editor.
</p>
<br /> <br />
<p>Please contact help@standardnotes.com to remedy this issue.</p> <p>Please contact help@standardnotes.com to remedy this issue.</p>
</div> </div>

View File

@@ -43,9 +43,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
const [hasIssueLoading, setHasIssueLoading] = useState(false) const [hasIssueLoading, setHasIssueLoading] = useState(false)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [featureStatus, setFeatureStatus] = useState<FeatureStatus>( const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(componentViewer.getFeatureStatus())
componentViewer.getFeatureStatus(),
)
const [isComponentValid, setIsComponentValid] = useState(true) const [isComponentValid, setIsComponentValid] = useState(true)
const [error, setError] = useState<ComponentViewerError | undefined>(undefined) const [error, setError] = useState<ComponentViewerError | undefined>(undefined)
const [deprecationMessage, setDeprecationMessage] = useState<string | undefined>(undefined) const [deprecationMessage, setDeprecationMessage] = useState<string | undefined>(undefined)
@@ -199,10 +197,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
/> />
)} )}
{deprecationMessage && !isDeprecationMessageDismissed && ( {deprecationMessage && !isDeprecationMessageDismissed && (
<IsDeprecated <IsDeprecated deprecationMessage={deprecationMessage} dismissDeprecationMessage={dismissDeprecationMessage} />
deprecationMessage={deprecationMessage}
dismissDeprecationMessage={dismissDeprecationMessage}
/>
)} )}
{error === ComponentViewerError.OfflineRestricted && <OfflineRestricted />} {error === ComponentViewerError.OfflineRestricted && <OfflineRestricted />}
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.name} />} {error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.name} />}

View File

@@ -1,11 +1,4 @@
import { import { ListboxArrow, ListboxButton, ListboxInput, ListboxList, ListboxOption, ListboxPopover } from '@reach/listbox'
ListboxArrow,
ListboxButton,
ListboxInput,
ListboxList,
ListboxOption,
ListboxPopover,
} from '@reach/listbox'
import VisuallyHidden from '@reach/visually-hidden' import VisuallyHidden from '@reach/visually-hidden'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
@@ -53,14 +46,7 @@ const CustomDropdownButton: FunctionComponent<ListboxButtonProps> = ({
</> </>
) )
export const Dropdown: FunctionComponent<DropdownProps> = ({ export const Dropdown: FunctionComponent<DropdownProps> = ({ id, label, items, value, onChange, disabled }) => {
id,
label,
items,
value,
onChange,
disabled,
}) => {
const labelId = `${id}-label` const labelId = `${id}-label`
const handleChange = (value: string) => { const handleChange = (value: string) => {
@@ -72,12 +58,7 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({
return ( return (
<> <>
<VisuallyHidden id={labelId}>{label}</VisuallyHidden> <VisuallyHidden id={labelId}>{label}</VisuallyHidden>
<ListboxInput <ListboxInput value={value} onChange={handleChange} aria-labelledby={labelId} disabled={disabled}>
value={value}
onChange={handleChange}
aria-labelledby={labelId}
disabled={disabled}
>
<ListboxButton <ListboxButton
className="sn-dropdown-button" className="sn-dropdown-button"
children={({ value, label, isExpanded }) => { children={({ value, label, isExpanded }) => {
@@ -105,10 +86,7 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({
> >
{item.icon ? ( {item.icon ? (
<div className="flex mr-3"> <div className="flex mr-3">
<Icon <Icon type={item.icon} className={`sn-icon--small ${item.iconClassName ?? ''}`} />
type={item.icon}
className={`sn-icon--small ${item.iconClassName ?? ''}`}
/>
</div> </div>
) : null} ) : null}
<div className="text-input">{item.label}</div> <div className="text-input">{item.label}</div>

View File

@@ -2,12 +2,7 @@ import { WebAppEvent, WebApplication } from '@/UIModels/Application'
import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
import { PureComponent } from '@/Components/Abstract/PureComponent' import { PureComponent } from '@/Components/Abstract/PureComponent'
import { preventRefreshing } from '@/Utils' import { preventRefreshing } from '@/Utils'
import { import { ApplicationEvent, ContentType, CollectionSort, ApplicationDescriptor } from '@standardnotes/snjs'
ApplicationEvent,
ContentType,
CollectionSort,
ApplicationDescriptor,
} from '@standardnotes/snjs'
import { import {
STRING_NEW_UPDATE_READY, STRING_NEW_UPDATE_READY,
STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, STRING_CONFIRM_APP_QUIT_DURING_UPGRADE,
@@ -241,9 +236,7 @@ export class Footer extends PureComponent<Props, State> {
style: 'percent', style: 'percent',
}) })
statusManager.setMessage( statusManager.setMessage(`Syncing ${stats.uploadTotalCount} items (${stringPercentage} complete)`)
`Syncing ${stats.uploadTotalCount} items (${stringPercentage} complete)`,
)
} else { } else {
statusManager.setMessage('') statusManager.setMessage('')
} }
@@ -330,9 +323,7 @@ export class Footer extends PureComponent<Props, State> {
betaMessageClickHandler = () => { betaMessageClickHandler = () => {
alertDialog({ alertDialog({
title: 'You are using a beta version of the app', title: 'You are using a beta version of the app',
text: text: 'If you wish to go back to a stable version, make sure to sign out ' + 'of this beta app first.',
'If you wish to go back to a stable version, make sure to sign out ' +
'of this beta app first.',
}).catch(console.error) }).catch(console.error)
} }
@@ -357,11 +348,7 @@ export class Footer extends PureComponent<Props, State> {
' w-8 h-full flex items-center justify-center cursor-pointer rounded-full' ' w-8 h-full flex items-center justify-center cursor-pointer rounded-full'
} }
> >
<div <div className={this.state.hasError ? 'danger' : (this.user ? 'info' : 'neutral') + ' w-5 h-5'}>
className={
this.state.hasError ? 'danger' : (this.user ? 'info' : 'neutral') + ' w-5 h-5'
}
>
<Icon type="account-circle" className="hover:color-info w-5 h-5 max-h-5" /> <Icon type="account-circle" className="hover:color-info w-5 h-5 max-h-5" />
</div> </div>
</div> </div>
@@ -382,10 +369,7 @@ export class Footer extends PureComponent<Props, State> {
<div className="h-5"> <div className="h-5">
<Icon <Icon
type="tune" type="tune"
className={ className={(this.state.showQuickSettingsMenu ? 'color-info' : '') + ' rounded hover:color-info'}
(this.state.showQuickSettingsMenu ? 'color-info' : '') +
' rounded hover:color-info'
}
/> />
</div> </div>
</div> </div>
@@ -401,10 +385,7 @@ export class Footer extends PureComponent<Props, State> {
<Fragment> <Fragment>
<div className="sk-app-bar-item border" /> <div className="sk-app-bar-item border" />
<div className="sk-app-bar-item"> <div className="sk-app-bar-item">
<a <a onClick={this.betaMessageClickHandler} className="no-decoration sk-label title">
onClick={this.betaMessageClickHandler}
className="no-decoration sk-label title"
>
You are using a beta version of the app You are using a beta version of the app
</a> </a>
</div> </div>
@@ -439,10 +420,7 @@ export class Footer extends PureComponent<Props, State> {
</div> </div>
)} )}
{this.state.showSyncResolution && ( {this.state.showSyncResolution && (
<SyncResolutionMenu <SyncResolutionMenu close={this.syncResolutionClickHandler} application={this.application} />
close={this.syncResolutionClickHandler}
application={this.application}
/>
)} )}
</div> </div>
)} )}

View File

@@ -39,9 +39,7 @@ export const DecoratedInput: FunctionalComponent<DecoratedInputProps> = forwardR
const classNames = getClassNames(hasLeftDecorations, hasRightDecorations) const classNames = getClassNames(hasLeftDecorations, hasRightDecorations)
return ( return (
<div <div className={`${classNames.container} ${disabled ? classNames.disabled : ''} ${className}`}>
className={`${classNames.container} ${disabled ? classNames.disabled : ''} ${className}`}
>
{left && ( {left && (
<div className="flex items-center px-2 py-1.5"> <div className="flex items-center px-2 py-1.5">
{left.map((leftChild) => ( {left.map((leftChild) => (

View File

@@ -22,8 +22,8 @@ const Toggle: FunctionComponent<{
/** /**
* Password input that has a toggle to show/hide password and can be decorated on the left and right side * Password input that has a toggle to show/hide password and can be decorated on the left and right side
*/ */
export const DecoratedPasswordInput: FunctionComponent<Omit<DecoratedInputProps, 'type'>> = export const DecoratedPasswordInput: FunctionComponent<Omit<DecoratedInputProps, 'type'>> = forwardRef(
forwardRef((props, ref: Ref<HTMLInputElement>) => { (props, ref: Ref<HTMLInputElement>) => {
const [isToggled, setIsToggled] = useState(false) const [isToggled, setIsToggled] = useState(false)
const rightSideDecorations = props.right ? [...props.right] : [] const rightSideDecorations = props.right ? [...props.right] : []
@@ -33,10 +33,8 @@ export const DecoratedPasswordInput: FunctionComponent<Omit<DecoratedInputProps,
{...props} {...props}
ref={ref} ref={ref}
type={isToggled ? 'text' : 'password'} type={isToggled ? 'text' : 'password'}
right={[ right={[...rightSideDecorations, <Toggle isToggled={isToggled} setIsToggled={setIsToggled} />]}
...rightSideDecorations,
<Toggle isToggled={isToggled} setIsToggled={setIsToggled} />,
]}
/> />
) )
}) },
)

View File

@@ -1,12 +1,4 @@
import { import { JSX, FunctionComponent, ComponentChildren, VNode, RefCallback, ComponentChild, toChildArray } from 'preact'
JSX,
FunctionComponent,
ComponentChildren,
VNode,
RefCallback,
ComponentChild,
toChildArray,
} from 'preact'
import { useEffect, useRef } from 'preact/hooks' import { useEffect, useRef } from 'preact/hooks'
import { JSXInternal } from 'preact/src/jsx' import { JSXInternal } from 'preact/src/jsx'
import { MenuItem, MenuItemListElement } from './MenuItem' import { MenuItem, MenuItemListElement } from './MenuItem'
@@ -70,11 +62,7 @@ export const Menu: FunctionComponent<MenuProps> = ({
} }
} }
const mapMenuItems = ( const mapMenuItems = (child: ComponentChild, index: number, array: ComponentChild[]): ComponentChild => {
child: ComponentChild,
index: number,
array: ComponentChild[],
): ComponentChild => {
if (!child || (Array.isArray(child) && child.length < 1)) { if (!child || (Array.isArray(child) && child.length < 1)) {
return return
} }
@@ -84,8 +72,7 @@ export const Menu: FunctionComponent<MenuProps> = ({
} }
const _child = child as VNode<unknown> const _child = child as VNode<unknown>
const isFirstMenuItem = const isFirstMenuItem = index === array.findIndex((child) => (child as VNode<unknown>).type === MenuItem)
index === array.findIndex((child) => (child as VNode<unknown>).type === MenuItem)
const hasMultipleItems = Array.isArray(_child.props.children) const hasMultipleItems = Array.isArray(_child.props.children)
? Array.from(_child.props.children as ComponentChild[]).some( ? Array.from(_child.props.children as ComponentChild[]).some(

View File

@@ -65,15 +65,9 @@ export const MenuItem: FunctionComponent<MenuItemProps> = forwardRef(
onBlur={onBlur} onBlur={onBlur}
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})} {...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
> >
{type === MenuItemType.IconButton && icon ? ( {type === MenuItemType.IconButton && icon ? <Icon type={icon} className={iconClassName} /> : null}
<Icon type={icon} className={iconClassName} />
) : null}
{type === MenuItemType.RadioButton && typeof checked === 'boolean' ? ( {type === MenuItemType.RadioButton && typeof checked === 'boolean' ? (
<div <div className={`pseudo-radio-btn ${checked ? 'pseudo-radio-btn--checked' : ''} mr-2 flex-shrink-0`}></div>
className={`pseudo-radio-btn ${
checked ? 'pseudo-radio-btn--checked' : ''
} mr-2 flex-shrink-0`}
></div>
) : null} ) : null}
{children} {children}
</button> </button>
@@ -81,9 +75,7 @@ export const MenuItem: FunctionComponent<MenuItemProps> = forwardRef(
}, },
) )
export const MenuItemSeparator: FunctionComponent = () => ( export const MenuItemSeparator: FunctionComponent = () => <div role="separator" className="h-1px my-2 bg-border"></div>
<div role="separator" className="h-1px my-2 bg-border"></div>
)
type ListElementProps = { type ListElementProps = {
isFirstMenuItem: boolean isFirstMenuItem: boolean

View File

@@ -27,9 +27,7 @@ export const MultipleSelectedNotes = observer(({ application, appState }: Props)
<div className="flex-grow flex flex-col justify-center items-center w-full max-w-md"> <div className="flex-grow flex flex-col justify-center items-center w-full max-w-md">
<IlNotesIcon className="block" /> <IlNotesIcon className="block" />
<h2 className="text-lg m-0 text-center mt-4">{count} selected notes</h2> <h2 className="text-lg m-0 text-center mt-4">{count} selected notes</h2>
<p className="text-sm mt-2 text-center max-w-60"> <p className="text-sm mt-2 text-center max-w-60">Actions will be performed on all selected notes.</p>
Actions will be performed on all selected notes.
</p>
</div> </div>
</div> </div>
) )

View File

@@ -6,12 +6,7 @@ import { ApplicationEvent, 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 { useCallback, useEffect, useMemo, useState } from 'preact/hooks' import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import { import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer'
PanelSide,
ResizeFinishCallback,
PanelResizer,
PanelResizeType,
} from '@/Components/PanelResizer'
type Props = { type Props = {
application: WebApplication application: WebApplication

View File

@@ -74,9 +74,7 @@ describe('editor-view', () => {
it("should hide the note if at the time of the session expiration the note wasn't edited for longer than the allowed idle time", async () => { it("should hide the note if at the time of the session expiration the note wasn't edited for longer than the allowed idle time", async () => {
jest jest
.spyOn(ctrl, 'getSecondsElapsedSinceLastEdit') .spyOn(ctrl, 'getSecondsElapsedSinceLastEdit')
.mockImplementation( .mockImplementation(() => ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction + 5)
() => ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction + 5,
)
await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired) await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
@@ -84,8 +82,7 @@ describe('editor-view', () => {
}) })
it('should postpone the note hiding by correct time if the time passed after its last modification is less than the allowed idle time', async () => { it('should postpone the note hiding by correct time if the time passed after its last modification is less than the allowed idle time', async () => {
const secondsElapsedSinceLastEdit = const secondsElapsedSinceLastEdit = ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - 3
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - 3
Object.defineProperty(ctrl.note, 'userModifiedDate', { Object.defineProperty(ctrl.note, 'userModifiedDate', {
value: new Date(Date.now() - secondsElapsedSinceLastEdit * 1000), value: new Date(Date.now() - secondsElapsedSinceLastEdit * 1000),
@@ -95,8 +92,7 @@ describe('editor-view', () => {
await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired) await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
const secondsAfterWhichTheNoteShouldHide = const secondsAfterWhichTheNoteShouldHide =
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - secondsElapsedSinceLastEdit
secondsElapsedSinceLastEdit
jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000) jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000)
expect(setShowProtectedWarningSpy).not.toHaveBeenCalled() expect(setShowProtectedWarningSpy).not.toHaveBeenCalled()
@@ -114,8 +110,7 @@ describe('editor-view', () => {
await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired) await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
let secondsAfterWhichTheNoteShouldHide = let secondsAfterWhichTheNoteShouldHide =
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - secondsElapsedSinceLastModification
secondsElapsedSinceLastModification
jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000) jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000)
// A new modification has just happened // A new modification has just happened
@@ -124,8 +119,7 @@ describe('editor-view', () => {
configurable: true, configurable: true,
}) })
secondsAfterWhichTheNoteShouldHide = secondsAfterWhichTheNoteShouldHide = ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction
jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000) jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000)
expect(setShowProtectedWarningSpy).not.toHaveBeenCalled() expect(setShowProtectedWarningSpy).not.toHaveBeenCalled()
@@ -152,9 +146,7 @@ describe('editor-view', () => {
describe('dismissProtectedWarning', () => { describe('dismissProtectedWarning', () => {
describe('the note has protection sources', () => { describe('the note has protection sources', () => {
it('should reveal note contents if the authorization has been passed', async () => { it('should reveal note contents if the authorization has been passed', async () => {
jest jest.spyOn(ctrl['application'], 'authorizeNoteAccess').mockImplementation(async () => Promise.resolve(true))
.spyOn(ctrl['application'], 'authorizeNoteAccess')
.mockImplementation(async () => Promise.resolve(true))
await ctrl.dismissProtectedWarning() await ctrl.dismissProtectedWarning()
@@ -162,9 +154,7 @@ describe('editor-view', () => {
}) })
it('should not reveal note contents if the authorization has not been passed', async () => { it('should not reveal note contents if the authorization has not been passed', async () => {
jest jest.spyOn(ctrl['application'], 'authorizeNoteAccess').mockImplementation(async () => Promise.resolve(false))
.spyOn(ctrl['application'], 'authorizeNoteAccess')
.mockImplementation(async () => Promise.resolve(false))
await ctrl.dismissProtectedWarning() await ctrl.dismissProtectedWarning()

View File

@@ -20,11 +20,7 @@ import {
import { debounce, isDesktopApplication } from '@/Utils' import { debounce, isDesktopApplication } from '@/Utils'
import { KeyboardModifier, KeyboardKey } from '@/Services/IOService' import { KeyboardModifier, KeyboardKey } from '@/Services/IOService'
import { EventSource } from '@/UIModels/AppState' import { EventSource } from '@/UIModels/AppState'
import { import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Strings'
STRING_DELETE_PLACEHOLDER_ATTEMPT,
STRING_DELETE_LOCKED_ATTEMPT,
StringDeleteNote,
} from '@/Strings'
import { confirmDialog } from '@/Services/AlertService' import { confirmDialog } from '@/Services/AlertService'
import { PureComponent } from '@/Components/Abstract/PureComponent' import { PureComponent } from '@/Components/Abstract/PureComponent'
import { ProtectedNoteOverlay } from '@/Components/ProtectedNoteOverlay' import { ProtectedNoteOverlay } from '@/Components/ProtectedNoteOverlay'
@@ -50,10 +46,7 @@ function sortAlphabetically(array: SNComponent[]): SNComponent[] {
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)) return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
} }
export const transactionForAssociateComponentWithCurrentNote = ( export const transactionForAssociateComponentWithCurrentNote = (component: SNComponent, note: SNNote) => {
component: SNComponent,
note: SNNote,
) => {
const transaction: TransactionalMutation = { const transaction: TransactionalMutation = {
itemUuid: component.uuid, itemUuid: component.uuid,
mutate: (m: ItemMutator) => { mutate: (m: ItemMutator) => {
@@ -65,10 +58,7 @@ export const transactionForAssociateComponentWithCurrentNote = (
return transaction return transaction
} }
export const transactionForDisassociateComponentWithCurrentNote = ( export const transactionForDisassociateComponentWithCurrentNote = (component: SNComponent, note: SNNote) => {
component: SNComponent,
note: SNNote,
) => {
const transaction: TransactionalMutation = { const transaction: TransactionalMutation = {
itemUuid: component.uuid, itemUuid: component.uuid,
mutate: (m: ItemMutator) => { mutate: (m: ItemMutator) => {
@@ -209,11 +199,9 @@ export class NoteView extends PureComponent<Props, State> {
this.registerKeyboardShortcuts() this.registerKeyboardShortcuts()
this.removeInnerNoteObserver = this.controller.addNoteInnerValueChangeObserver( this.removeInnerNoteObserver = this.controller.addNoteInnerValueChangeObserver((note, source) => {
(note, source) => { this.onNoteInnerChange(note, source)
this.onNoteInnerChange(note, source) })
},
)
this.autorun(() => { this.autorun(() => {
this.setState({ this.setState({
@@ -281,18 +269,15 @@ export class NoteView extends PureComponent<Props, State> {
this.reloadSpellcheck().catch(console.error) this.reloadSpellcheck().catch(console.error)
const isTemplateNoteInsertedToBeInteractableWithEditor = const isTemplateNoteInsertedToBeInteractableWithEditor = source === PayloadEmitSource.LocalInserted && note.dirty
source === PayloadEmitSource.LocalInserted && note.dirty
if (isTemplateNoteInsertedToBeInteractableWithEditor) { if (isTemplateNoteInsertedToBeInteractableWithEditor) {
return return
} }
if (note.lastSyncBegan || note.dirty) { if (note.lastSyncBegan || note.dirty) {
if (note.lastSyncEnd) { if (note.lastSyncEnd) {
const shouldShowSavingStatus = const shouldShowSavingStatus = note.lastSyncBegan && note.lastSyncBegan.getTime() > note.lastSyncEnd.getTime()
note.lastSyncBegan && note.lastSyncBegan.getTime() > note.lastSyncEnd.getTime() const shouldShowSavedStatus = note.lastSyncBegan && note.lastSyncEnd.getTime() > note.lastSyncBegan.getTime()
const shouldShowSavedStatus =
note.lastSyncBegan && note.lastSyncEnd.getTime() > note.lastSyncBegan.getTime()
if (note.dirty || shouldShowSavingStatus) { if (note.dirty || shouldShowSavingStatus) {
this.showSavingStatus() this.showSavingStatus()
} else if (this.state.noteStatus && shouldShowSavedStatus) { } else if (this.state.noteStatus && shouldShowSavedStatus) {
@@ -367,15 +352,11 @@ export class NoteView extends PureComponent<Props, State> {
hideProtectedNoteIfInactive(): void { hideProtectedNoteIfInactive(): void {
const secondsElapsedSinceLastEdit = this.getSecondsElapsedSinceLastEdit() const secondsElapsedSinceLastEdit = this.getSecondsElapsedSinceLastEdit()
if ( if (secondsElapsedSinceLastEdit >= ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction) {
secondsElapsedSinceLastEdit >=
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction
) {
this.setShowProtectedOverlay(true) this.setShowProtectedOverlay(true)
} else { } else {
const secondsUntilTheNextCheck = const secondsUntilTheNextCheck =
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - secondsElapsedSinceLastEdit
secondsElapsedSinceLastEdit
this.startNoteProtectionInactivityTimer(secondsUntilTheNextCheck) this.startNoteProtectionInactivityTimer(secondsUntilTheNextCheck)
} }
} }
@@ -406,36 +387,24 @@ export class NoteView extends PureComponent<Props, State> {
} }
streamItems() { streamItems() {
this.removeComponentStreamObserver = this.application.streamItems( this.removeComponentStreamObserver = this.application.streamItems(ContentType.Component, async ({ source }) => {
ContentType.Component, if (isPayloadSourceInternalChange(source) || source === PayloadEmitSource.InitialObserverRegistrationPush) {
async ({ source }) => { return
if ( }
isPayloadSourceInternalChange(source) || if (!this.note) {
source === PayloadEmitSource.InitialObserverRegistrationPush return
) { }
return await this.reloadStackComponents()
} this.debounceReloadEditorComponent()
if (!this.note) { })
return
}
await this.reloadStackComponents()
this.debounceReloadEditorComponent()
},
)
} }
private createComponentViewer(component: SNComponent) { private createComponentViewer(component: SNComponent) {
const viewer = this.application.componentManager.createComponentViewer( const viewer = this.application.componentManager.createComponentViewer(component, this.note.uuid)
component,
this.note.uuid,
)
return viewer return viewer
} }
public editorComponentViewerRequestsReload = async ( public editorComponentViewerRequestsReload = async (viewer: ComponentViewer, force?: boolean): Promise<void> => {
viewer: ComponentViewer,
force?: boolean,
): Promise<void> => {
if (this.state.editorComponentViewerDidAlreadyReload && !force) { if (this.state.editorComponentViewerDidAlreadyReload && !force) {
return return
} }
@@ -698,10 +667,7 @@ export class NoteView extends PureComponent<Props, State> {
async reloadPreferences() { async reloadPreferences() {
const monospaceFont = this.application.getPreference(PrefKey.EditorMonospaceEnabled, true) const monospaceFont = this.application.getPreference(PrefKey.EditorMonospaceEnabled, true)
const marginResizersEnabled = this.application.getPreference( const marginResizersEnabled = this.application.getPreference(PrefKey.EditorResizersEnabled, true)
PrefKey.EditorResizersEnabled,
true,
)
await this.reloadSpellcheck() await this.reloadSpellcheck()
@@ -758,9 +724,7 @@ export class NoteView extends PureComponent<Props, State> {
const newViewers: ComponentViewer[] = [] const newViewers: ComponentViewer[] = []
for (const component of needsNewViewer) { for (const component of needsNewViewer) {
newViewers.push( newViewers.push(this.application.componentManager.createComponentViewer(component, this.note.uuid))
this.application.componentManager.createComponentViewer(component, this.note.uuid),
)
} }
for (const viewer of needsDestroyViewer) { for (const viewer of needsDestroyViewer) {
@@ -773,9 +737,7 @@ export class NoteView extends PureComponent<Props, State> {
} }
stackComponentExpanded = (component: SNComponent): boolean => { stackComponentExpanded = (component: SNComponent): boolean => {
return !!this.state.stackComponentViewers.find( return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUuid === component.uuid)
(viewer) => viewer.componentUuid === component.uuid,
)
} }
toggleStackComponent = async (component: SNComponent) => { toggleStackComponent = async (component: SNComponent) => {
@@ -942,9 +904,7 @@ export class NoteView extends PureComponent<Props, State> {
className="sk-app-bar-item" className="sk-app-bar-item"
> >
<div className="sk-label warning flex items-center"> <div className="sk-label warning flex items-center">
{this.state.showLockedIcon && ( {this.state.showLockedIcon && <Icon type="pencil-off" className="flex fill-current mr-2" />}
<Icon type="pencil-off" className="flex fill-current mr-2" />
)}
{this.state.lockText} {this.state.lockText}
</div> </div>
</div> </div>
@@ -984,9 +944,7 @@ export class NoteView extends PureComponent<Props, State> {
> >
{this.state.noteStatus?.message} {this.state.noteStatus?.message}
</div> </div>
{this.state.noteStatus?.desc && ( {this.state.noteStatus?.desc && <div className="desc">{this.state.noteStatus.desc}</div>}
<div className="desc">{this.state.noteStatus.desc}</div>
)}
</div> </div>
</div> </div>
{this.appState.features.isFilesEnabled && ( {this.appState.features.isFilesEnabled && (
@@ -1022,11 +980,7 @@ export class NoteView extends PureComponent<Props, State> {
</div> </div>
)} )}
<div <div id={ElementIds.EditorContent} className={ElementIds.EditorContent} ref={this.editorContentRef}>
id={ElementIds.EditorContent}
className={ElementIds.EditorContent}
ref={this.editorContentRef}
>
{this.state.marginResizersEnabled && this.editorContentRef.current ? ( {this.state.marginResizersEnabled && this.editorContentRef.current ? (
<PanelResizer <PanelResizer
minWidth={300} minWidth={300}
@@ -1053,22 +1007,20 @@ export class NoteView extends PureComponent<Props, State> {
</div> </div>
)} )}
{this.state.editorStateDidLoad && {this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading && (
!this.state.editorComponentViewer && <textarea
!this.state.textareaUnloading && ( autocomplete="off"
<textarea className="editable font-editor"
autocomplete="off" dir="auto"
className="editable font-editor" id={ElementIds.NoteTextEditor}
dir="auto" onChange={this.onTextAreaChange}
id={ElementIds.NoteTextEditor} value={this.state.editorText}
onChange={this.onTextAreaChange} readonly={this.state.noteLocked}
value={this.state.editorText} onFocus={this.onContentFocus}
readonly={this.state.noteLocked} spellcheck={this.state.spellcheck}
onFocus={this.onContentFocus} ref={(ref) => this.onSystemEditorLoad(ref)}
spellcheck={this.state.spellcheck} ></textarea>
ref={(ref) => this.onSystemEditorLoad(ref)} )}
></textarea>
)}
{this.state.marginResizersEnabled && this.editorContentRef.current ? ( {this.state.marginResizersEnabled && this.editorContentRef.current ? (
<PanelResizer <PanelResizer
@@ -1101,9 +1053,7 @@ export class NoteView extends PureComponent<Props, State> {
<div className="sk-app-bar-item-column"> <div className="sk-app-bar-item-column">
<div <div
className={ className={
(this.stackComponentExpanded(component) && component.active (this.stackComponentExpanded(component) && component.active ? 'info ' : '') +
? 'info '
: '') +
(!this.stackComponentExpanded(component) ? 'neutral ' : '') + (!this.stackComponentExpanded(component) ? 'neutral ' : '') +
' sk-circle small' ' sk-circle small'
} }

View File

@@ -15,9 +15,7 @@ export const NotesContextMenu = observer(({ application, appState }: Props) => {
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = appState.notes const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = appState.notes
const contextMenuRef = useRef<HTMLDivElement>(null) const contextMenuRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => appState.notes.setContextMenuOpen(open))
appState.notes.setContextMenuOpen(open),
)
useCloseOnClickOutside(contextMenuRef, () => appState.notes.setContextMenuOpen(false)) useCloseOnClickOutside(contextMenuRef, () => appState.notes.setContextMenuOpen(false))

View File

@@ -16,30 +16,14 @@ type Props = {
export const NotesListOptionsMenu: FunctionComponent<Props> = observer( export const NotesListOptionsMenu: FunctionComponent<Props> = observer(
({ closeDisplayOptionsMenu, closeOnBlur, application, isOpen }) => { ({ closeDisplayOptionsMenu, closeOnBlur, application, isOpen }) => {
const [sortBy, setSortBy] = useState(() => const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt))
application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt), const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false))
) const [hidePreview, setHidePreview] = useState(() => application.getPreference(PrefKey.NotesHideNotePreview, false))
const [sortReverse, setSortReverse] = useState(() => const [hideDate, setHideDate] = useState(() => application.getPreference(PrefKey.NotesHideDate, false))
application.getPreference(PrefKey.SortNotesReverse, false), const [hideTags, setHideTags] = useState(() => application.getPreference(PrefKey.NotesHideTags, true))
) const [hidePinned, setHidePinned] = useState(() => application.getPreference(PrefKey.NotesHidePinned, false))
const [hidePreview, setHidePreview] = useState(() => const [showArchived, setShowArchived] = useState(() => application.getPreference(PrefKey.NotesShowArchived, false))
application.getPreference(PrefKey.NotesHideNotePreview, false), const [showTrashed, setShowTrashed] = useState(() => application.getPreference(PrefKey.NotesShowTrashed, false))
)
const [hideDate, setHideDate] = useState(() =>
application.getPreference(PrefKey.NotesHideDate, false),
)
const [hideTags, setHideTags] = useState(() =>
application.getPreference(PrefKey.NotesHideTags, true),
)
const [hidePinned, setHidePinned] = useState(() =>
application.getPreference(PrefKey.NotesHidePinned, false),
)
const [showArchived, setShowArchived] = useState(() =>
application.getPreference(PrefKey.NotesShowArchived, false),
)
const [showTrashed, setShowTrashed] = useState(() =>
application.getPreference(PrefKey.NotesShowTrashed, false),
)
const [hideProtected, setHideProtected] = useState(() => const [hideProtected, setHideProtected] = useState(() =>
application.getPreference(PrefKey.NotesHideProtected, false), application.getPreference(PrefKey.NotesHideProtected, false),
) )

View File

@@ -76,11 +76,7 @@ type ListedActionsMenuProps = {
recalculateMenuStyle: () => void recalculateMenuStyle: () => void
} }
const ListedActionsMenu: FunctionComponent<ListedActionsMenuProps> = ({ const ListedActionsMenu: FunctionComponent<ListedActionsMenuProps> = ({ application, note, recalculateMenuStyle }) => {
application,
note,
recalculateMenuStyle,
}) => {
const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([]) const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([])
const [isFetchingAccounts, setIsFetchingAccounts] = useState(true) const [isFetchingAccounts, setIsFetchingAccounts] = useState(true)
@@ -251,11 +247,7 @@ export const ListedActionsOption: FunctionComponent<Props> = ({ application, not
return ( return (
<div ref={menuContainerRef}> <div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}> <Disclosure open={isMenuOpen} onChange={toggleListedMenu}>
<DisclosureButton <DisclosureButton ref={menuButtonRef} onBlur={closeOnBlur} className="sn-dropdown-item justify-between">
ref={menuButtonRef}
onBlur={closeOnBlur}
className="sn-dropdown-item justify-between"
>
<div className="flex items-center"> <div className="flex items-center">
<Icon type="listed" className="color-neutral mr-2" /> <Icon type="listed" className="color-neutral mr-2" />
Listed actions Listed actions
@@ -271,11 +263,7 @@ export const ListedActionsOption: FunctionComponent<Props> = ({ application, not
className="sn-dropdown flex flex-col max-h-120 min-w-68 pb-1 fixed overflow-y-auto" className="sn-dropdown flex flex-col max-h-120 min-w-68 pb-1 fixed overflow-y-auto"
> >
{isMenuOpen && ( {isMenuOpen && (
<ListedActionsMenu <ListedActionsMenu application={application} note={note} recalculateMenuStyle={recalculateMenuStyle} />
application={application}
note={note}
recalculateMenuStyle={recalculateMenuStyle}
/>
)} )}
</DisclosurePanel> </DisclosurePanel>
</Disclosure> </Disclosure>

View File

@@ -15,76 +15,72 @@ type Props = {
onClickPreprocessing?: () => Promise<void> onClickPreprocessing?: () => Promise<void>
} }
export const NotesOptionsPanel = observer( export const NotesOptionsPanel = observer(({ application, appState, onClickPreprocessing }: Props) => {
({ application, appState, onClickPreprocessing }: Props) => { const [open, setOpen] = useState(false)
const [open, setOpen] = useState(false) const [position, setPosition] = useState({
const [position, setPosition] = useState({ top: 0,
top: 0, right: 0,
right: 0, })
}) const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto') const buttonRef = useRef<HTMLButtonElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null) const panelRef = useRef<HTMLDivElement>(null)
const panelRef = useRef<HTMLDivElement>(null) const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen)
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen)
return ( return (
<Disclosure <Disclosure
open={open} open={open}
onChange={async () => { onChange={async () => {
const rect = buttonRef.current?.getBoundingClientRect() const rect = buttonRef.current?.getBoundingClientRect()
if (rect) { if (rect) {
const { clientHeight } = document.documentElement const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect() const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) { if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2) setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2)
} }
setPosition({ setPosition({
top: rect.bottom, top: rect.bottom,
right: document.body.clientWidth - rect.right, right: document.body.clientWidth - rect.right,
}) })
const newOpenState = !open const newOpenState = !open
if (newOpenState && onClickPreprocessing) { if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing() await onClickPreprocessing()
} }
setOpen(newOpenState) setOpen(newOpenState)
}
}}
>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
} }
}} }}
onBlur={closeOnBlur}
ref={buttonRef}
className="sn-icon-button border-contrast"
> >
<DisclosureButton <VisuallyHidden>Actions</VisuallyHidden>
onKeyDown={(event) => { <Icon type="more" className="block" />
if (event.key === 'Escape') { </DisclosureButton>
setOpen(false) <DisclosurePanel
} onKeyDown={(event) => {
}} if (event.key === 'Escape') {
onBlur={closeOnBlur} setOpen(false)
ref={buttonRef} buttonRef.current?.focus()
className="sn-icon-button border-contrast" }
> }}
<VisuallyHidden>Actions</VisuallyHidden> ref={panelRef}
<Icon type="more" className="block" /> style={{
</DisclosureButton> ...position,
<DisclosurePanel maxHeight,
onKeyDown={(event) => { }}
if (event.key === 'Escape') { className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col pt-2 overflow-y-auto fixed"
setOpen(false) onBlur={closeOnBlur}
buttonRef.current?.focus() tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
} >
}} {open && <NotesOptions application={application} appState={appState} closeOnBlur={closeOnBlur} />}
ref={panelRef} </DisclosurePanel>
style={{ </Disclosure>
...position, )
maxHeight, })
}}
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col pt-2 overflow-y-auto fixed"
onBlur={closeOnBlur}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
{open && (
<NotesOptions application={application} appState={appState} closeOnBlur={closeOnBlur} />
)}
</DisclosurePanel>
</Disclosure>
)
},
)

View File

@@ -10,12 +10,7 @@ import { NoAccountWarning } from '@/Components/NoAccountWarning'
import { NotesList } from '@/Components/NotesList' import { NotesList } from '@/Components/NotesList'
import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu' import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu'
import { SearchOptions } from '@/Components/SearchOptions' import { SearchOptions } from '@/Components/SearchOptions'
import { import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer'
PanelSide,
ResizeFinishCallback,
PanelResizer,
PanelResizeType,
} from '@/Components/PanelResizer'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
@@ -51,10 +46,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false) const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
const [focusedSearch, setFocusedSearch] = useState(false) const [focusedSearch, setFocusedSearch] = useState(false)
const [closeDisplayOptMenuOnBlur] = useCloseOnBlur( const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu)
displayOptionsMenuRef,
setShowDisplayOptionsMenu,
)
useEffect(() => { useEffect(() => {
handleFilterTextChanged() handleFilterTextChanged()
@@ -125,12 +117,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
} }
} }
const panelResizeFinishCallback: ResizeFinishCallback = ( const panelResizeFinishCallback: ResizeFinishCallback = (width, _lastLeft, _isMaxWidth, isCollapsed) => {
width,
_lastLeft,
_isMaxWidth,
isCollapsed,
) => {
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error) application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
appState.noteTags.reloadTagsContainerMaxWidth() appState.noteTags.reloadTagsContainerMaxWidth()
appState.panelDidResize(PANEL_NAME_NOTES, isCollapsed) appState.panelDidResize(PANEL_NAME_NOTES, isCollapsed)
@@ -229,9 +216,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
</div> </div>
</div> </div>
</div> </div>
{completedFullSync && !renderedNotes.length ? ( {completedFullSync && !renderedNotes.length ? <p className="empty-notes-list faded">No notes.</p> : null}
<p className="empty-notes-list faded">No notes.</p>
) : null}
{!completedFullSync && !renderedNotes.length ? ( {!completedFullSync && !renderedNotes.length ? (
<p className="empty-notes-list faded">Loading notes...</p> <p className="empty-notes-list faded">Loading notes...</p>
) : null} ) : null}

View File

@@ -34,9 +34,9 @@ const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props)
</AlertDialogLabel> </AlertDialogLabel>
<AlertDialogDescription className="sk-panel-row"> <AlertDialogDescription className="sk-panel-row">
<p className="color-foreground"> <p className="color-foreground">
This action will sign out all other devices signed into your account, and remove This action will sign out all other devices signed into your account, and remove your data from
your data from those devices when they next regain connection to the internet. those devices when they next regain connection to the internet. You may sign back in on those
You may sign back in on those devices at any time. devices at any time.
</p> </p>
</AlertDialogDescription> </AlertDialogDescription>
<div className="flex my-1 mt-4"> <div className="flex my-1 mt-4">
@@ -49,11 +49,7 @@ const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props)
application.revokeAllOtherSessions().catch(console.error) application.revokeAllOtherSessions().catch(console.error)
closeDialog() closeDialog()
application.alertService application.alertService
.alert( .alert('You have successfully revoked your sessions from other devices.', undefined, 'Finish')
'You have successfully revoked your sessions from other devices.',
undefined,
'Finish',
)
.catch(console.error) .catch(console.error)
}} }}
> >

View File

@@ -107,9 +107,7 @@ export class PanelResizer extends Component<Props, State> {
isAtMaxWidth = () => { isAtMaxWidth = () => {
const marginOfError = 5 const marginOfError = 5
const difference = Math.abs( const difference = Math.abs(Math.round(this.lastWidth + this.lastLeft) - Math.round(this.getParentRect().width))
Math.round(this.lastWidth + this.lastLeft) - Math.round(this.getParentRect().width),
)
return difference < marginOfError return difference < marginOfError
} }
@@ -159,12 +157,7 @@ export class PanelResizer extends Component<Props, State> {
if (finish) { if (finish) {
this.finishSettingWidth() this.finishSettingWidth()
if (this.props.resizeFinishCallback) { if (this.props.resizeFinishCallback) {
this.props.resizeFinishCallback( this.props.resizeFinishCallback(this.lastWidth, this.lastLeft, this.isAtMaxWidth(), this.isCollapsed())
this.lastWidth,
this.lastLeft,
this.isAtMaxWidth(),
this.isCollapsed(),
)
} }
} }
} }
@@ -184,12 +177,7 @@ export class PanelResizer extends Component<Props, State> {
} }
this.finishSettingWidth() this.finishSettingWidth()
this.props.resizeFinishCallback?.( this.props.resizeFinishCallback?.(this.lastWidth, this.lastLeft, this.isAtMaxWidth(), this.isCollapsed())
this.lastWidth,
this.lastLeft,
this.isAtMaxWidth(),
this.isCollapsed(),
)
} }
handleWidthEvent(event?: MouseEvent) { handleWidthEvent(event?: MouseEvent) {

View File

@@ -109,9 +109,7 @@ export class PasswordWizard extends PureComponent<Props, State> {
const currentPassword = this.state.formData.currentPassword const currentPassword = this.state.formData.currentPassword
const newPass = this.state.formData.newPassword const newPass = this.state.formData.newPassword
if (!currentPassword || currentPassword.length === 0) { if (!currentPassword || currentPassword.length === 0) {
this.application.alertService this.application.alertService.alert('Please enter your current password.').catch(console.error)
.alert('Please enter your current password.')
.catch(console.error)
return false return false
} }
@@ -120,9 +118,7 @@ export class PasswordWizard extends PureComponent<Props, State> {
return false return false
} }
if (newPass !== this.state.formData.newPasswordConfirmation) { if (newPass !== this.state.formData.newPasswordConfirmation) {
this.application.alertService this.application.alertService.alert('Your new password does not match its confirmation.').catch(console.error)
.alert('Your new password does not match its confirmation.')
.catch(console.error)
this.setFormDataState({ this.setFormDataState({
status: undefined, status: undefined,
}).catch(console.error) }).catch(console.error)
@@ -131,9 +127,7 @@ export class PasswordWizard extends PureComponent<Props, State> {
if (!this.application.getUser()?.email) { if (!this.application.getUser()?.email) {
this.application.alertService this.application.alertService
.alert( .alert("We don't have your email stored. Please sign 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.",
)
.catch(console.error) .catch(console.error)
this.setFormDataState({ this.setFormDataState({
status: undefined, status: undefined,
@@ -142,9 +136,7 @@ export class PasswordWizard extends PureComponent<Props, State> {
} }
/** Validate current password */ /** Validate current password */
const success = await this.application.validateAccountPassword( const success = await this.application.validateAccountPassword(this.state.formData.currentPassword as string)
this.state.formData.currentPassword as string,
)
if (!success) { if (!success) {
this.application.alertService this.application.alertService
.alert('The current password you entered is not correct. Please try again.') .alert('The current password you entered is not correct. Please try again.')
@@ -194,9 +186,7 @@ export class PasswordWizard extends PureComponent<Props, State> {
dismiss = () => { dismiss = () => {
if (this.state.lockContinue) { if (this.state.lockContinue) {
this.application.alertService this.application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
.alert('Cannot close window until pending tasks are complete.')
.catch(console.error)
} else { } else {
this.dismissModal() this.dismissModal()
} }
@@ -211,25 +201,19 @@ export class PasswordWizard extends PureComponent<Props, State> {
}) })
} }
handleCurrentPasswordInputChange = ({ handleCurrentPasswordInputChange = ({ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>) => {
currentTarget,
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
this.setFormDataState({ this.setFormDataState({
currentPassword: currentTarget.value, currentPassword: currentTarget.value,
}).catch(console.error) }).catch(console.error)
} }
handleNewPasswordInputChange = ({ handleNewPasswordInputChange = ({ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>) => {
currentTarget,
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
this.setFormDataState({ this.setFormDataState({
newPassword: currentTarget.value, newPassword: currentTarget.value,
}).catch(console.error) }).catch(console.error)
} }
handleNewPasswordConfirmationInputChange = ({ handleNewPasswordConfirmationInputChange = ({ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>) => {
currentTarget,
}: JSX.TargetedEvent<HTMLInputElement, Event>) => {
this.setFormDataState({ this.setFormDataState({
newPasswordConfirmation: currentTarget.value, newPasswordConfirmation: currentTarget.value,
}).catch(console.error) }).catch(console.error)
@@ -283,10 +267,7 @@ export class PasswordWizard extends PureComponent<Props, State> {
/> />
<div className="sk-panel-row" /> <div className="sk-panel-row" />
<label <label htmlFor="password-wiz-confirm-new-password" className="block mb-1">
htmlFor="password-wiz-confirm-new-password"
className="block mb-1"
>
Confirm New Password Confirm New Password
</label> </label>
@@ -304,12 +285,10 @@ export class PasswordWizard extends PureComponent<Props, State> {
)} )}
{this.state.step === Steps.FinishStep && ( {this.state.step === Steps.FinishStep && (
<div className="sk-panel-section"> <div className="sk-panel-section">
<div className="sk-label sk-bold info"> <div className="sk-label sk-bold info">Your password has been successfully changed.</div>
Your password has been successfully changed.
</div>
<p className="sk-p"> <p className="sk-p">
Please ensure you are running the latest version of Standard Notes on all Please ensure you are running the latest version of Standard Notes on all platforms to ensure
platforms to ensure maximum compatibility. maximum compatibility.
</p> </p>
</div> </div>
)} )}

View File

@@ -77,10 +77,7 @@ export class PermissionsModal extends Component<Props> {
</div> </div>
</div> </div>
<div className="sk-panel-footer"> <div className="sk-panel-footer">
<button <button onClick={this.accept} className="sn-button info block w-full text-base py-3">
onClick={this.accept}
className="sn-button info block w-full text-base py-3"
>
Continue Continue
</button> </button>
</div> </div>

View File

@@ -1,8 +1,5 @@
import { FunctionalComponent } from 'preact' import { FunctionalComponent } from 'preact'
import { import { PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
PreferencesGroup,
PreferencesSegment,
} from '@/Components/Preferences/PreferencesComponents'
import { OfflineSubscription } from '@/Components/Preferences/Panes/Account/OfflineSubscription' import { OfflineSubscription } from '@/Components/Preferences/Panes/Account/OfflineSubscription'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
@@ -17,25 +14,23 @@ interface IProps {
extensionsLatestVersions: ExtensionsLatestVersions extensionsLatestVersions: ExtensionsLatestVersions
} }
export const Advanced: FunctionalComponent<IProps> = observer( export const Advanced: FunctionalComponent<IProps> = observer(({ application, appState, extensionsLatestVersions }) => {
({ application, appState, extensionsLatestVersions }) => { return (
return ( <PreferencesGroup>
<PreferencesGroup> <PreferencesSegment>
<PreferencesSegment> <AccordionItem title={'Advanced Settings'}>
<AccordionItem title={'Advanced Settings'}> <div className="flex flex-row items-center">
<div className="flex flex-row items-center"> <div className="flex-grow flex flex-col">
<div className="flex-grow flex flex-col"> <OfflineSubscription application={application} appState={appState} />
<OfflineSubscription application={application} appState={appState} /> <Extensions
<Extensions className={'mt-3'}
className={'mt-3'} application={application}
application={application} extensionsLatestVersions={extensionsLatestVersions}
extensionsLatestVersions={extensionsLatestVersions} />
/>
</div>
</div> </div>
</AccordionItem> </div>
</PreferencesSegment> </AccordionItem>
</PreferencesGroup> </PreferencesSegment>
) </PreferencesGroup>
}, )
) })

View File

@@ -1,11 +1,6 @@
import { AccountMenuPane } from '@/Components/AccountMenu' import { AccountMenuPane } from '@/Components/AccountMenu'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
import { import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/Components/Preferences/PreferencesComponents'
PreferencesGroup,
PreferencesSegment,
Text,
Title,
} from '@/Components/Preferences/PreferencesComponents'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
@@ -35,21 +30,12 @@ export const Authentication: FunctionComponent<{
<AccountIllustration className="mb-3" /> <AccountIllustration className="mb-3" />
<Title>You're not signed in</Title> <Title>You're not signed in</Title>
<Text className="text-center mb-3"> <Text className="text-center mb-3">
Sign in to sync your notes and preferences across all your devices and enable end-to-end Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.
encryption.
</Text> </Text>
<Button <Button variant="primary" label="Create free account" onClick={clickRegister} className="mb-3" />
variant="primary"
label="Create free account"
onClick={clickRegister}
className="mb-3"
/>
<div className="text-input"> <div className="text-input">
Already have an account?{' '} Already have an account?{' '}
<button <button className="border-0 p-0 bg-default color-info underline cursor-pointer" onClick={clickSignIn}>
className="border-0 p-0 bg-default color-info underline cursor-pointer"
onClick={clickSignIn}
>
Sign in Sign in
</button> </button>
</div> </div>

View File

@@ -10,10 +10,7 @@ const labelClassName = 'block mb-1'
const inputClassName = 'sk-input contrast' const inputClassName = 'sk-input contrast'
export const ChangeEmailForm: FunctionalComponent<Props> = ({ export const ChangeEmailForm: FunctionalComponent<Props> = ({ setNewEmail, setCurrentPassword }) => {
setNewEmail,
setCurrentPassword,
}) => {
return ( return (
<div className="w-full flex flex-col"> <div className="w-full flex flex-col">
<div className="mt-2 mb-3"> <div className="mt-2 mb-3">

View File

@@ -5,8 +5,8 @@ export const ChangeEmailSuccess: FunctionalComponent = () => {
<div> <div>
<div className={'sk-label sk-bold info mt-2'}>Your email has been successfully changed.</div> <div className={'sk-label sk-bold info mt-2'}>Your email has been successfully changed.</div>
<p className={'sk-p'}> <p className={'sk-p'}>
Please ensure you are running the latest version of Standard Notes on all platforms to Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum
ensure maximum compatibility. compatibility.
</p> </p>
</div> </div>
) )

View File

@@ -63,9 +63,7 @@ export const ChangeEmail: FunctionalComponent<Props> = ({ onCloseDialog, applica
const validateNewEmail = async () => { const validateNewEmail = async () => {
if (!isEmailValid(newEmail)) { if (!isEmailValid(newEmail)) {
applicationAlertService applicationAlertService
.alert( .alert('The email you entered has an invalid format. Please review your input and try again.')
'The email you entered has an invalid format. Please review your input and try again.',
)
.catch(console.error) .catch(console.error)
return false return false
@@ -95,9 +93,7 @@ export const ChangeEmail: FunctionalComponent<Props> = ({ onCloseDialog, applica
const dismiss = () => { const dismiss = () => {
if (lockContinue) { if (lockContinue) {
applicationAlertService applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
.alert('Cannot close window until pending tasks are complete.')
.catch(console.error)
} else { } else {
onCloseDialog() onCloseDialog()
} }
@@ -139,9 +135,7 @@ export const ChangeEmail: FunctionalComponent<Props> = ({ onCloseDialog, applica
const handleDialogClose = () => { const handleDialogClose = () => {
if (lockContinue) { if (lockContinue) {
applicationAlertService applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
.alert('Cannot close window until pending tasks are complete.')
.catch(console.error)
} else { } else {
onCloseDialog() onCloseDialog()
} }
@@ -158,12 +152,7 @@ export const ChangeEmail: FunctionalComponent<Props> = ({ onCloseDialog, applica
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />} {currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
</ModalDialogDescription> </ModalDialogDescription>
<ModalDialogButtons className="px-4.5"> <ModalDialogButtons className="px-4.5">
<Button <Button className="min-w-20" variant="primary" label={submitButtonTitle} onClick={handleSubmit} />
className="min-w-20"
variant="primary"
label={submitButtonTitle}
onClick={handleSubmit}
/>
</ModalDialogButtons> </ModalDialogButtons>
</ModalDialog> </ModalDialog>
</div> </div>

View File

@@ -30,10 +30,7 @@ export const Credentials: FunctionComponent<Props> = observer(({ application }:
const passwordCreatedOn = dateToLocalizedString(passwordCreatedAtTimestamp) const passwordCreatedOn = dateToLocalizedString(passwordCreatedAtTimestamp)
const presentPasswordWizard = useCallback(() => { const presentPasswordWizard = useCallback(() => {
render( render(<PasswordWizard application={application} />, document.body.appendChild(document.createElement('div')))
<PasswordWizard application={application} />,
document.body.appendChild(document.createElement('div')),
)
}, [application]) }, [application])
return ( return (
@@ -57,17 +54,9 @@ export const Credentials: FunctionComponent<Props> = observer(({ application }:
<Text> <Text>
Current password was set on <span className="font-bold">{passwordCreatedOn}</span> Current password was set on <span className="font-bold">{passwordCreatedOn}</span>
</Text> </Text>
<Button <Button className="min-w-20 mt-3" variant="normal" label="Change password" onClick={presentPasswordWizard} />
className="min-w-20 mt-3"
variant="normal"
label="Change password"
onClick={presentPasswordWizard}
/>
{isChangeEmailDialogOpen && ( {isChangeEmailDialogOpen && (
<ChangeEmail <ChangeEmail onCloseDialog={() => setIsChangeEmailDialogOpen(false)} application={application} />
onCloseDialog={() => setIsChangeEmailDialogOpen(false)}
application={application}
/>
)} )}
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>

View File

@@ -28,9 +28,7 @@ export const OfflineSubscription: FunctionalComponent<IProps> = observer(({ appl
}, [application]) }, [application])
const shouldShowOfflineSubscription = () => { const shouldShowOfflineSubscription = () => {
return ( return !application.hasAccount() || application.isThirdPartyHostUsed() || hasUserPreviouslyStoredCode
!application.hasAccount() || application.isThirdPartyHostUsed() || hasUserPreviouslyStoredCode
)
} }
const handleSubscriptionCodeSubmit = async (event: Event) => { const handleSubscriptionCodeSubmit = async (event: Event) => {
@@ -98,8 +96,8 @@ export const OfflineSubscription: FunctionalComponent<IProps> = observer(({ appl
</div> </div>
{(isSuccessfullyActivated || isSuccessfullyRemoved) && ( {(isSuccessfullyActivated || isSuccessfullyRemoved) && (
<div className={'mt-3 mb-3 info'}> <div className={'mt-3 mb-3 info'}>
Your offline subscription code has been successfully{' '} Your offline subscription code has been successfully {isSuccessfullyActivated ? 'activated' : 'removed'}
{isSuccessfullyActivated ? 'activated' : 'removed'}. .
</div> </div>
)} )}
{hasUserPreviouslyStoredCode && ( {hasUserPreviouslyStoredCode && (

View File

@@ -33,11 +33,7 @@ const SignOutView: FunctionComponent<{
appState.accountMenu.setOtherSessionsSignOut(true) appState.accountMenu.setOtherSessionsSignOut(true)
}} }}
/> />
<Button <Button variant="normal" label="Manage sessions" onClick={() => appState.openSessionsModal()} />
variant="normal"
label="Manage sessions"
onClick={() => appState.openSessionsModal()}
/>
</div> </div>
</PreferencesSegment> </PreferencesSegment>
<PreferencesSegment> <PreferencesSegment>

View File

@@ -12,8 +12,7 @@ export const NoSubscription: FunctionalComponent<{
const [purchaseFlowError, setPurchaseFlowError] = useState<string | undefined>(undefined) const [purchaseFlowError, setPurchaseFlowError] = useState<string | undefined>(undefined)
const onPurchaseClick = async () => { const onPurchaseClick = async () => {
const errorMessage = const errorMessage = 'There was an error when attempting to redirect you to the subscription page.'
'There was an error when attempting to redirect you to the subscription page.'
setIsLoadingPurchaseFlow(true) setIsLoadingPurchaseFlow(true)
try { try {
if (!(await loadPurchaseFlowUrl(application))) { if (!(await loadPurchaseFlowUrl(application))) {
@@ -32,18 +31,9 @@ export const NoSubscription: FunctionalComponent<{
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>} {isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
{purchaseFlowError && <Text className="color-danger">{purchaseFlowError}</Text>} {purchaseFlowError && <Text className="color-danger">{purchaseFlowError}</Text>}
<div className="flex"> <div className="flex">
<LinkButton <LinkButton className="min-w-20 mt-3 mr-3" label="Learn More" link={window.plansUrl as string} />
className="min-w-20 mt-3 mr-3"
label="Learn More"
link={window.plansUrl as string}
/>
{application.hasAccount() && ( {application.hasAccount() && (
<Button <Button className="min-w-20 mt-3" variant="primary" label="Subscribe" onClick={onPurchaseClick} />
className="min-w-20 mt-3"
variant="primary"
label="Subscribe"
onClick={onPurchaseClick}
/>
)} )}
</div> </div>
</> </>

View File

@@ -1,8 +1,4 @@
import { import { PreferencesGroup, PreferencesSegment, Title } from '@/Components/Preferences/PreferencesComponents'
PreferencesGroup,
PreferencesSegment,
Title,
} from '@/Components/Preferences/PreferencesComponents'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { SubscriptionInformation } from './SubscriptionInformation' import { SubscriptionInformation } from './SubscriptionInformation'
import { NoSubscription } from './NoSubscription' import { NoSubscription } from './NoSubscription'
@@ -15,31 +11,26 @@ type Props = {
appState: AppState appState: AppState
} }
export const Subscription: FunctionComponent<Props> = observer( export const Subscription: FunctionComponent<Props> = observer(({ application, appState }: Props) => {
({ application, appState }: Props) => { const subscriptionState = appState.subscription
const subscriptionState = appState.subscription const { userSubscription } = subscriptionState
const { userSubscription } = subscriptionState
const now = new Date().getTime() const now = new Date().getTime()
return ( return (
<PreferencesGroup> <PreferencesGroup>
<PreferencesSegment> <PreferencesSegment>
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<div className="flex-grow flex flex-col"> <div className="flex-grow flex flex-col">
<Title>Subscription</Title> <Title>Subscription</Title>
{userSubscription && userSubscription.endsAt > now ? ( {userSubscription && userSubscription.endsAt > now ? (
<SubscriptionInformation <SubscriptionInformation subscriptionState={subscriptionState} application={application} />
subscriptionState={subscriptionState} ) : (
application={application} <NoSubscription application={application} />
/> )}
) : (
<NoSubscription application={application} />
)}
</div>
</div> </div>
</PreferencesSegment> </div>
</PreferencesGroup> </PreferencesSegment>
) </PreferencesGroup>
}, )
) })

View File

@@ -10,49 +10,16 @@ type Props = {
application: WebApplication application: WebApplication
} }
const StatusText = observer( const StatusText = observer(({ subscriptionState }: { subscriptionState: Props['subscriptionState'] }) => {
({ subscriptionState }: { subscriptionState: Props['subscriptionState'] }) => { const {
const { userSubscriptionName,
userSubscriptionName, userSubscriptionExpirationDate,
userSubscriptionExpirationDate, isUserSubscriptionExpired,
isUserSubscriptionExpired, isUserSubscriptionCanceled,
isUserSubscriptionCanceled, } = subscriptionState
} = subscriptionState const expirationDateString = userSubscriptionExpirationDate?.toLocaleString()
const expirationDateString = userSubscriptionExpirationDate?.toLocaleString()
if (isUserSubscriptionCanceled) {
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription has been canceled{' '}
{isUserSubscriptionExpired ? (
<span className="font-bold">and expired on {expirationDateString}</span>
) : (
<span className="font-bold">but will remain valid until {expirationDateString}</span>
)}
. You may resubscribe below if you wish.
</Text>
)
}
if (isUserSubscriptionExpired) {
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription <span className="font-bold">expired on {expirationDateString}</span>. You may
resubscribe below if you wish.
</Text>
)
}
if (isUserSubscriptionCanceled) {
return ( return (
<Text className="mt-1"> <Text className="mt-1">
Your{' '} Your{' '}
@@ -60,11 +27,42 @@ const StatusText = observer(
Standard Notes{userSubscriptionName ? ' ' : ''} Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName} {userSubscriptionName}
</span>{' '} </span>{' '}
subscription will be <span className="font-bold">renewed on {expirationDateString}</span>. subscription has been canceled{' '}
{isUserSubscriptionExpired ? (
<span className="font-bold">and expired on {expirationDateString}</span>
) : (
<span className="font-bold">but will remain valid until {expirationDateString}</span>
)}
. You may resubscribe below if you wish.
</Text> </Text>
) )
}, }
)
if (isUserSubscriptionExpired) {
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription <span className="font-bold">expired on {expirationDateString}</span>. You may resubscribe below if
you wish.
</Text>
)
}
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription will be <span className="font-bold">renewed on {expirationDateString}</span>.
</Text>
)
})
export const SubscriptionInformation = observer(({ subscriptionState, application }: Props) => { export const SubscriptionInformation = observer(({ subscriptionState, application }: Props) => {
const manageSubscription = async () => { const manageSubscription = async () => {

View File

@@ -1,9 +1,4 @@
import { import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/Components/Preferences/PreferencesComponents'
PreferencesGroup,
PreferencesSegment,
Text,
Title,
} from '@/Components/Preferences/PreferencesComponents'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
import { SyncQueueStrategy, dateToLocalizedString } from '@standardnotes/snjs' import { SyncQueueStrategy, dateToLocalizedString } from '@standardnotes/snjs'
import { STRING_GENERIC_SYNC_ERROR } from '@/Strings' import { STRING_GENERIC_SYNC_ERROR } from '@/Strings'
@@ -22,9 +17,7 @@ export const formatLastSyncDate = (lastUpdatedDate: Date) => {
export const Sync: FunctionComponent<Props> = observer(({ application }: Props) => { export const Sync: FunctionComponent<Props> = observer(({ application }: Props) => {
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false) const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
const [lastSyncDate, setLastSyncDate] = useState( const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
formatLastSyncDate(application.sync.getLastSyncDate() as Date),
)
const doSynchronization = async () => { const doSynchronization = async () => {
setIsSyncingInProgress(true) setIsSyncingInProgress(true)

View File

@@ -3,14 +3,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
import { Switch } from '@/Components/Switch' import { Switch } from '@/Components/Switch'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { import { ContentType, FeatureIdentifier, FeatureStatus, PrefKey, GetFeatures, SNTheme } from '@standardnotes/snjs'
ContentType,
FeatureIdentifier,
FeatureStatus,
PrefKey,
GetFeatures,
SNTheme,
} from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
@@ -31,8 +24,7 @@ type Props = {
export const Appearance: FunctionComponent<Props> = observer(({ application }) => { export const Appearance: FunctionComponent<Props> = observer(({ application }) => {
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const isEntitledToMidnightTheme = const isEntitledToMidnightTheme =
application.features.getFeatureStatus(FeatureIdentifier.MidnightTheme) === application.features.getFeatureStatus(FeatureIdentifier.MidnightTheme) === FeatureStatus.Entitled
FeatureStatus.Entitled
const [themeItems, setThemeItems] = useState<DropdownItem[]>([]) const [themeItems, setThemeItems] = useState<DropdownItem[]>([])
const [autoLightTheme, setAutoLightTheme] = useState<string>( const [autoLightTheme, setAutoLightTheme] = useState<string>(
@@ -100,9 +92,7 @@ export const Appearance: FunctionComponent<Props> = observer(({ application }) =
if (item.icon === 'premium-feature') { if (item.icon === 'premium-feature') {
premiumModal.activate(`${item.label} theme`) premiumModal.activate(`${item.label} theme`)
} else { } else {
application application.setPreference(PrefKey.AutoLightThemeIdentifier, value as FeatureIdentifier).catch(console.error)
.setPreference(PrefKey.AutoLightThemeIdentifier, value as FeatureIdentifier)
.catch(console.error)
setAutoLightTheme(value) setAutoLightTheme(value)
} }
} }
@@ -111,9 +101,7 @@ export const Appearance: FunctionComponent<Props> = observer(({ application }) =
if (item.icon === 'premium-feature') { if (item.icon === 'premium-feature') {
premiumModal.activate(`${item.label} theme`) premiumModal.activate(`${item.label} theme`)
} else { } else {
application application.setPreference(PrefKey.AutoDarkThemeIdentifier, value as FeatureIdentifier).catch(console.error)
.setPreference(PrefKey.AutoDarkThemeIdentifier, value as FeatureIdentifier)
.catch(console.error)
setAutoDarkTheme(value) setAutoDarkTheme(value)
} }
} }

View File

@@ -66,18 +66,13 @@ export const CloudBackupProvider: FunctionComponent<Props> = ({
const performBackupNow = async () => { const performBackupNow = async () => {
// A backup is performed anytime the setting is updated with the integration token, so just update it here // A backup is performed anytime the setting is updated with the integration token, so just update it here
try { try {
await application.settings.updateSetting( await application.settings.updateSetting(backupFrequencySettingName, backupFrequency as string)
backupFrequencySettingName,
backupFrequency as string,
)
void application.alertService.alert( void application.alertService.alert(
'A backup has been triggered for this provider. Please allow a couple minutes for your backup to be processed.', 'A backup has been triggered for this provider. Please allow a couple minutes for your backup to be processed.',
) )
} catch (err) { } catch (err) {
application.alertService application.alertService
.alert( .alert('There was an error while trying to trigger a backup for this provider. Please try again.')
'There was an error while trying to trigger a backup for this provider. Please try again.',
)
.catch(console.error) .catch(console.error)
} }
} }
@@ -184,8 +179,8 @@ export const CloudBackupProvider: FunctionComponent<Props> = ({
{authBegan && ( {authBegan && (
<div> <div>
<p className="sk-panel-row"> <p className="sk-panel-row">
Complete authentication from the newly opened window. Upon completion, a confirmation Complete authentication from the newly opened window. Upon completion, a confirmation code will be
code will be displayed. Enter this code below: displayed. Enter this code below:
</p> </p>
<div className={'mt-1'}> <div className={'mt-1'}>
<input <input

View File

@@ -22,11 +22,7 @@ import { Switch } from '@/Components/Switch'
import { convertStringifiedBooleanToBoolean } from '@/Utils' import { convertStringifiedBooleanToBoolean } from '@/Utils'
import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Strings' import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Strings'
const providerData = [ const providerData = [{ name: CloudProvider.Dropbox }, { name: CloudProvider.Google }, { name: CloudProvider.OneDrive }]
{ name: CloudProvider.Dropbox },
{ name: CloudProvider.Google },
{ name: CloudProvider.OneDrive },
]
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -62,20 +58,12 @@ export const CloudLink: FunctionComponent<Props> = ({ application }) => {
}, [application]) }, [application])
useEffect(() => { useEffect(() => {
const dailyDropboxBackupStatus = application.features.getFeatureStatus( const dailyDropboxBackupStatus = application.features.getFeatureStatus(FeatureIdentifier.DailyDropboxBackup)
FeatureIdentifier.DailyDropboxBackup, const dailyGdriveBackupStatus = application.features.getFeatureStatus(FeatureIdentifier.DailyGDriveBackup)
const dailyOneDriveBackupStatus = application.features.getFeatureStatus(FeatureIdentifier.DailyOneDriveBackup)
const isCloudBackupsAllowed = [dailyDropboxBackupStatus, dailyGdriveBackupStatus, dailyOneDriveBackupStatus].every(
(status) => status === FeatureStatus.Entitled,
) )
const dailyGdriveBackupStatus = application.features.getFeatureStatus(
FeatureIdentifier.DailyGDriveBackup,
)
const dailyOneDriveBackupStatus = application.features.getFeatureStatus(
FeatureIdentifier.DailyOneDriveBackup,
)
const isCloudBackupsAllowed = [
dailyDropboxBackupStatus,
dailyGdriveBackupStatus,
dailyOneDriveBackupStatus,
].every((status) => status === FeatureStatus.Entitled)
setIsEntitledToCloudBackups(isCloudBackupsAllowed) setIsEntitledToCloudBackups(isCloudBackupsAllowed)
loadIsFailedCloudBackupEmailMutedSetting().catch(console.error) loadIsFailedCloudBackupEmailMutedSetting().catch(console.error)
@@ -114,9 +102,8 @@ export const CloudLink: FunctionComponent<Props> = ({ application }) => {
{!isEntitledToCloudBackups && ( {!isEntitledToCloudBackups && (
<> <>
<Text> <Text>
A <span className={'font-bold'}>Plus</span> or{' '} A <span className={'font-bold'}>Plus</span> or <span className={'font-bold'}>Pro</span> subscription plan
<span className={'font-bold'}>Pro</span> subscription plan is required to enable Cloud is required to enable Cloud Backups.{' '}
Backups.{' '}
<a target="_blank" href="https://standardnotes.com/features"> <a target="_blank" href="https://standardnotes.com/features">
Learn more Learn more
</a> </a>
@@ -127,8 +114,8 @@ export const CloudLink: FunctionComponent<Props> = ({ application }) => {
)} )}
<div> <div>
<Text className={additionalClass}> <Text className={additionalClass}>
Configure the integrations below to enable automatic daily backups of your encrypted Configure the integrations below to enable automatic daily backups of your encrypted data set to your
data set to your third-party cloud provider. third-party cloud provider.
</Text> </Text>
<div> <div>
<HorizontalSeparator classes={`mt-3 mb-3 ${additionalClass}`} /> <HorizontalSeparator classes={`mt-3 mb-3 ${additionalClass}`} />

View File

@@ -160,8 +160,7 @@ export const DataBackups = observer(({ application, appState }: Props) => {
{!isDesktopApplication() && ( {!isDesktopApplication() && (
<Text className="mb-3"> <Text className="mb-3">
Backups are automatically created on desktop and can be managed via the "Backups" Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.
top-level menu.
</Text> </Text>
)} )}
@@ -171,43 +170,25 @@ export const DataBackups = observer(({ application, appState }: Props) => {
<form className="sk-panel-form sk-panel-row"> <form className="sk-panel-form sk-panel-row">
<div className="sk-input-group"> <div className="sk-input-group">
<label className="sk-horizontal-group tight"> <label className="sk-horizontal-group tight">
<input <input type="radio" onChange={() => setIsBackupEncrypted(true)} checked={isBackupEncrypted} />
type="radio"
onChange={() => setIsBackupEncrypted(true)}
checked={isBackupEncrypted}
/>
<Subtitle>Encrypted</Subtitle> <Subtitle>Encrypted</Subtitle>
</label> </label>
<label className="sk-horizontal-group tight"> <label className="sk-horizontal-group tight">
<input <input type="radio" onChange={() => setIsBackupEncrypted(false)} checked={!isBackupEncrypted} />
type="radio"
onChange={() => setIsBackupEncrypted(false)}
checked={!isBackupEncrypted}
/>
<Subtitle>Decrypted</Subtitle> <Subtitle>Decrypted</Subtitle>
</label> </label>
</div> </div>
</form> </form>
)} )}
<Button <Button variant="normal" onClick={downloadDataArchive} label="Download backup" className="mt-2" />
variant="normal"
onClick={downloadDataArchive}
label="Download backup"
className="mt-2"
/>
</PreferencesSegment> </PreferencesSegment>
<PreferencesSegment> <PreferencesSegment>
<Subtitle>Import a previously saved backup file</Subtitle> <Subtitle>Import a previously saved backup file</Subtitle>
<div class="flex flex-row items-center mt-3"> <div class="flex flex-row items-center mt-3">
<Button variant="normal" label="Import backup" onClick={handleImportFile} /> <Button variant="normal" label="Import backup" onClick={handleImportFile} />
<input <input type="file" ref={fileInputRef} onChange={importFileSelected} className="hidden" />
type="file"
ref={fileInputRef}
onChange={importFileSelected}
className="hidden"
/>
{isImportDataLoading && <div className="sk-spinner normal info ml-4" />} {isImportDataLoading && <div className="sk-spinner normal info ml-4" />}
</div> </div>
</PreferencesSegment> </PreferencesSegment>

View File

@@ -27,9 +27,7 @@ type Props = {
export const EmailBackups = observer(({ application }: Props) => { export const EmailBackups = observer(({ application }: Props) => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [emailFrequency, setEmailFrequency] = useState<EmailBackupFrequency>( const [emailFrequency, setEmailFrequency] = useState<EmailBackupFrequency>(EmailBackupFrequency.Disabled)
EmailBackupFrequency.Disabled,
)
const [emailFrequencyOptions, setEmailFrequencyOptions] = useState<DropdownItem[]>([]) const [emailFrequencyOptions, setEmailFrequencyOptions] = useState<DropdownItem[]>([])
const [isFailedBackupEmailMuted, setIsFailedBackupEmailMuted] = useState(true) const [isFailedBackupEmailMuted, setIsFailedBackupEmailMuted] = useState(true)
const [isEntitledToEmailBackups, setIsEntitledToEmailBackups] = useState(false) const [isEntitledToEmailBackups, setIsEntitledToEmailBackups] = useState(false)
@@ -64,9 +62,7 @@ export const EmailBackups = observer(({ application }: Props) => {
}, [application]) }, [application])
useEffect(() => { useEffect(() => {
const emailBackupsFeatureStatus = application.features.getFeatureStatus( const emailBackupsFeatureStatus = application.features.getFeatureStatus(FeatureIdentifier.DailyEmailBackup)
FeatureIdentifier.DailyEmailBackup,
)
setIsEntitledToEmailBackups(emailBackupsFeatureStatus === FeatureStatus.Entitled) setIsEntitledToEmailBackups(emailBackupsFeatureStatus === FeatureStatus.Entitled)
const frequencyOptions = [] const frequencyOptions = []
@@ -109,10 +105,7 @@ export const EmailBackups = observer(({ application }: Props) => {
const previousValue = isFailedBackupEmailMuted const previousValue = isFailedBackupEmailMuted
setIsFailedBackupEmailMuted(!isFailedBackupEmailMuted) setIsFailedBackupEmailMuted(!isFailedBackupEmailMuted)
const updateResult = await updateSetting( const updateResult = await updateSetting(SettingName.MuteFailedBackupsEmails, `${!isFailedBackupEmailMuted}`)
SettingName.MuteFailedBackupsEmails,
`${!isFailedBackupEmailMuted}`,
)
if (!updateResult) { if (!updateResult) {
setIsFailedBackupEmailMuted(previousValue) setIsFailedBackupEmailMuted(previousValue)
} }
@@ -132,9 +125,8 @@ export const EmailBackups = observer(({ application }: Props) => {
{!isEntitledToEmailBackups && ( {!isEntitledToEmailBackups && (
<> <>
<Text> <Text>
A <span className={'font-bold'}>Plus</span> or{' '} A <span className={'font-bold'}>Plus</span> or <span className={'font-bold'}>Pro</span> subscription plan
<span className={'font-bold'}>Pro</span> subscription plan is required to enable Email is required to enable Email Backups.{' '}
Backups.{' '}
<a target="_blank" href="https://standardnotes.com/features"> <a target="_blank" href="https://standardnotes.com/features">
Learn more Learn more
</a> </a>
@@ -146,8 +138,7 @@ export const EmailBackups = observer(({ application }: Props) => {
<div className={isEntitledToEmailBackups ? '' : 'faded cursor-default pointer-events-none'}> <div className={isEntitledToEmailBackups ? '' : 'faded cursor-default pointer-events-none'}>
{!isDesktopApplication() && ( {!isDesktopApplication() && (
<Text className="mb-3"> <Text className="mb-3">
Daily encrypted email backups of your entire data set delivered directly to your Daily encrypted email backups of your entire data set delivered directly to your inbox.
inbox.
</Text> </Text>
)} )}
<Subtitle>Email frequency</Subtitle> <Subtitle>Email frequency</Subtitle>

View File

@@ -17,10 +17,10 @@ export const CloudLink: FunctionComponent = () => (
<div className="h-2 w-full" /> <div className="h-2 w-full" />
<Subtitle>Who can read my private notes?</Subtitle> <Subtitle>Who can read my private notes?</Subtitle>
<Text> <Text>
Quite simply: no one but you. Not us, not your ISP, not a hacker, and not a government Quite simply: no one but you. Not us, not your ISP, not a hacker, and not a government agency. As long as you
agency. As long as you keep your password safe, and your password is reasonably strong, keep your password safe, and your password is reasonably strong, then you are the only person in the world
then you are the only person in the world with the ability to decrypt your notes. For more with the ability to decrypt your notes. For more on how we handle your privacy and security, check out our
on how we handle your privacy and security, check out our easy to read{' '} easy to read{' '}
<a target="_blank" href="https://standardnotes.com/privacy"> <a target="_blank" href="https://standardnotes.com/privacy">
Privacy Manifesto. Privacy Manifesto.
</a> </a>
@@ -29,21 +29,17 @@ export const CloudLink: FunctionComponent = () => (
<PreferencesSegment> <PreferencesSegment>
<Subtitle>Can I collaborate with others on a note?</Subtitle> <Subtitle>Can I collaborate with others on a note?</Subtitle>
<Text> <Text>
Because of our encrypted architecture, Standard Notes does not currently provide a Because of our encrypted architecture, Standard Notes does not currently provide a real-time collaboration
real-time collaboration solution. Multiple users can share the same account however, but solution. Multiple users can share the same account however, but editing at the same time may result in sync
editing at the same time may result in sync conflicts, which may result in the duplication conflicts, which may result in the duplication of notes.
of notes.
</Text> </Text>
</PreferencesSegment> </PreferencesSegment>
<PreferencesSegment> <PreferencesSegment>
<Subtitle>Can I use Standard Notes totally offline?</Subtitle> <Subtitle>Can I use Standard Notes totally offline?</Subtitle>
<Text> <Text>
Standard Notes can be used totally offline without an account, and without an internet Standard Notes can be used totally offline without an account, and without an internet connection. You can
connection. You can find{' '} find{' '}
<a <a target="_blank" href="https://standardnotes.com/help/59/can-i-use-standard-notes-totally-offline">
target="_blank"
href="https://standardnotes.com/help/59/can-i-use-standard-notes-totally-offline"
>
more details here. more details here.
</a> </a>
</Text> </Text>
@@ -57,38 +53,25 @@ export const CloudLink: FunctionComponent = () => (
<PreferencesSegment> <PreferencesSegment>
<Title>Community forum</Title> <Title>Community forum</Title>
<Text> <Text>
If you have an issue, found a bug or want to suggest a feature, you can browse or post to If you have an issue, found a bug or want to suggest a feature, you can browse or post to the forum. Its
the forum. Its recommended for non-account related issues. Please read our{' '} recommended for non-account related issues. Please read our{' '}
<a target="_blank" href="https://standardnotes.com/longevity/"> <a target="_blank" href="https://standardnotes.com/longevity/">
Longevity statement Longevity statement
</a>{' '} </a>{' '}
before advocating for a feature request. before advocating for a feature request.
</Text> </Text>
<LinkButton <LinkButton className="mt-3" label="Go to the forum" link="https://forum.standardnotes.org/" />
className="mt-3"
label="Go to the forum"
link="https://forum.standardnotes.org/"
/>
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>
<PreferencesGroup> <PreferencesGroup>
<PreferencesSegment> <PreferencesSegment>
<Title>Community groups</Title> <Title>Community groups</Title>
<Text> <Text>
Want to meet other passionate note-takers and privacy enthusiasts? Want to share your Want to meet other passionate note-takers and privacy enthusiasts? Want to share your feedback with us? Join
feedback with us? Join the Standard Notes community groups for discussions on security, the Standard Notes community groups for discussions on security, themes, editors and more.
themes, editors and more.
</Text> </Text>
<LinkButton <LinkButton className="mt-3" link="https://standardnotes.com/slack" label="Join our Slack" />
className="mt-3" <LinkButton className="mt-3" link="https://standardnotes.com/discord" label="Join our Discord" />
link="https://standardnotes.com/slack"
label="Join our Slack"
/>
<LinkButton
className="mt-3"
link="https://standardnotes.com/discord"
label="Join our Discord"
/>
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>
<PreferencesGroup> <PreferencesGroup>

View File

@@ -1,12 +1,7 @@
import { DisplayStringForContentType, SNComponent } from '@standardnotes/snjs' import { DisplayStringForContentType, SNComponent } from '@standardnotes/snjs'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { import { Title, Text, Subtitle, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
Title,
Text,
Subtitle,
PreferencesSegment,
} from '@/Components/Preferences/PreferencesComponents'
export const ConfirmCustomExtension: FunctionComponent<{ export const ConfirmCustomExtension: FunctionComponent<{
component: SNComponent component: SNComponent
@@ -59,21 +54,11 @@ export const ConfirmCustomExtension: FunctionComponent<{
<div className="min-h-3" /> <div className="min-h-3" />
<div className="flex flex-row"> <div className="flex flex-row">
<Button <Button className="min-w-20" variant="normal" label="Cancel" onClick={() => callback(false)} />
className="min-w-20"
variant="normal"
label="Cancel"
onClick={() => callback(false)}
/>
<div className="min-w-3" /> <div className="min-w-3" />
<Button <Button className="min-w-20" variant="normal" label="Install" onClick={() => callback(true)} />
className="min-w-20"
variant="normal"
label="Install"
onClick={() => callback(true)}
/>
</div> </div>
</PreferencesSegment> </PreferencesSegment>
) )

View File

@@ -1,10 +1,6 @@
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { SNComponent } from '@standardnotes/snjs' import { SNComponent } from '@standardnotes/snjs'
import { import { PreferencesSegment, SubtitleLight, Title } from '@/Components/Preferences/PreferencesComponents'
PreferencesSegment,
SubtitleLight,
Title,
} from '@/Components/Preferences/PreferencesComponents'
import { Switch } from '@/Components/Switch' import { Switch } from '@/Components/Switch'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
@@ -30,12 +26,7 @@ export interface ExtensionItemProps {
toggleActivate?: (extension: SNComponent) => void toggleActivate?: (extension: SNComponent) => void
} }
export const ExtensionItem: FunctionComponent<ExtensionItemProps> = ({ export const ExtensionItem: FunctionComponent<ExtensionItemProps> = ({ application, extension, first, uninstall }) => {
application,
extension,
first,
uninstall,
}) => {
const [offlineOnly, setOfflineOnly] = useState(extension.offlineOnly ?? false) const [offlineOnly, setOfflineOnly] = useState(extension.offlineOnly ?? false)
const [extensionName, setExtensionName] = useState(extension.name) const [extensionName, setExtensionName] = useState(extension.name)
@@ -95,12 +86,7 @@ export const ExtensionItem: FunctionComponent<ExtensionItemProps> = ({
<> <>
<div className="min-h-2" /> <div className="min-h-2" />
<div className="flex flex-row"> <div className="flex flex-row">
<Button <Button className="min-w-20" variant="normal" label="Uninstall" onClick={() => uninstall(extension)} />
className="min-w-20"
variant="normal"
label="Uninstall"
onClick={() => uninstall(extension)}
/>
</div> </div>
</> </>
</PreferencesSegment> </PreferencesSegment>

View File

@@ -29,10 +29,7 @@ export class ExtensionsLatestVersions {
} }
} }
function collectFeatures( function collectFeatures(features: FeatureDescription[] | undefined, versionMap: Map<string, string>) {
features: FeatureDescription[] | undefined,
versionMap: Map<string, string>,
) {
if (features == undefined) { if (features == undefined) {
return return
} }

View File

@@ -11,11 +11,7 @@ import { ExtensionItem } from './ExtensionItem'
import { ConfirmCustomExtension } from './ConfirmCustomExtension' import { ConfirmCustomExtension } from './ConfirmCustomExtension'
const loadExtensions = (application: WebApplication) => const loadExtensions = (application: WebApplication) =>
application.items.getItems([ application.items.getItems([ContentType.ActionsExtension, ContentType.Component, ContentType.Theme]) as SNComponent[]
ContentType.ActionsExtension,
ContentType.Component,
ContentType.Theme,
]) as SNComponent[]
export const Extensions: FunctionComponent<{ export const Extensions: FunctionComponent<{
application: WebApplication application: WebApplication
@@ -23,9 +19,7 @@ export const Extensions: FunctionComponent<{
className?: string className?: string
}> = observer(({ application, extensionsLatestVersions, className = '' }) => { }> = observer(({ application, extensionsLatestVersions, className = '' }) => {
const [customUrl, setCustomUrl] = useState('') const [customUrl, setCustomUrl] = useState('')
const [confirmableExtension, setConfirmableExtension] = useState<SNComponent | undefined>( const [confirmableExtension, setConfirmableExtension] = useState<SNComponent | undefined>(undefined)
undefined,
)
const [extensions, setExtensions] = useState(loadExtensions(application)) const [extensions, setExtensions] = useState(loadExtensions(application))
const confirmableEnd = useRef<HTMLDivElement>(null) const confirmableEnd = useRef<HTMLDivElement>(null)
@@ -122,10 +116,7 @@ export const Extensions: FunctionComponent<{
)} )}
{confirmableExtension && ( {confirmableExtension && (
<PreferencesSegment> <PreferencesSegment>
<ConfirmCustomExtension <ConfirmCustomExtension component={confirmableExtension} callback={handleConfirmExtensionSubmit} />
component={confirmableExtension}
callback={handleConfirmExtensionSubmit}
/>
<div ref={confirmableEnd} /> <div ref={confirmableEnd} />
</PreferencesSegment> </PreferencesSegment>
)} )}

View File

@@ -29,19 +29,16 @@ export const LabsPane: FunctionComponent<Props> = ({ application }) => {
const [experimentalFeatures, setExperimentalFeatures] = useState<ExperimentalFeatureItem[]>([]) const [experimentalFeatures, setExperimentalFeatures] = useState<ExperimentalFeatureItem[]>([])
const reloadExperimentalFeatures = useCallback(() => { const reloadExperimentalFeatures = useCallback(() => {
const experimentalFeatures = application.features const experimentalFeatures = application.features.getExperimentalFeatures().map((featureIdentifier) => {
.getExperimentalFeatures() const feature = FindNativeFeature(featureIdentifier)
.map((featureIdentifier) => { return {
const feature = FindNativeFeature(featureIdentifier) identifier: featureIdentifier,
return { name: feature?.name ?? featureIdentifier,
identifier: featureIdentifier, description: feature?.description ?? '',
name: feature?.name ?? featureIdentifier, isEnabled: application.features.isExperimentalFeatureEnabled(featureIdentifier),
description: feature?.description ?? '', isEntitled: application.features.getFeatureStatus(featureIdentifier) === FeatureStatus.Entitled,
isEnabled: application.features.isExperimentalFeatureEnabled(featureIdentifier), }
isEntitled: })
application.features.getFeatureStatus(featureIdentifier) === FeatureStatus.Entitled,
}
})
setExperimentalFeatures(experimentalFeatures) setExperimentalFeatures(experimentalFeatures)
}, [application.features]) }, [application.features])
@@ -56,35 +53,32 @@ export const LabsPane: FunctionComponent<Props> = ({ application }) => {
<PreferencesSegment> <PreferencesSegment>
<Title>Labs</Title> <Title>Labs</Title>
<div> <div>
{experimentalFeatures.map( {experimentalFeatures.map(({ identifier, name, description, isEnabled, isEntitled }, index: number) => {
({ identifier, name, description, isEnabled, isEntitled }, index: number) => { const toggleFeature = () => {
const toggleFeature = () => { if (!isEntitled) {
if (!isEntitled) { premiumModal.activate(name)
premiumModal.activate(name) return
return
}
application.features.toggleExperimentalFeature(identifier)
reloadExperimentalFeatures()
} }
const showHorizontalSeparator = application.features.toggleExperimentalFeature(identifier)
experimentalFeatures.length > 1 && index !== experimentalFeatures.length - 1 reloadExperimentalFeatures()
}
return ( const showHorizontalSeparator = experimentalFeatures.length > 1 && index !== experimentalFeatures.length - 1
<>
<div className="flex items-center justify-between"> return (
<div className="flex flex-col"> <>
<Subtitle>{name}</Subtitle> <div className="flex items-center justify-between">
<Text>{description}</Text> <div className="flex flex-col">
</div> <Subtitle>{name}</Subtitle>
<Switch onChange={toggleFeature} checked={isEnabled} /> <Text>{description}</Text>
</div> </div>
{showHorizontalSeparator && <HorizontalSeparator classes="mt-5 mb-3" />} <Switch onChange={toggleFeature} checked={isEnabled} />
</> </div>
) {showHorizontalSeparator && <HorizontalSeparator classes="mt-5 mb-3" />}
}, </>
)} )
})}
{experimentalFeatures.length === 0 && ( {experimentalFeatures.length === 0 && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">

View File

@@ -21,11 +21,7 @@ export const General: FunctionComponent<GeneralProps> = observer(
<Tools application={application} /> <Tools application={application} />
<Defaults application={application} /> <Defaults application={application} />
<LabsPane application={application} /> <LabsPane application={application} />
<Advanced <Advanced application={application} appState={appState} extensionsLatestVersions={extensionsLatestVersions} />
application={application}
appState={appState}
extensionsLatestVersions={extensionsLatestVersions}
/>
</PreferencesPane> </PreferencesPane>
), ),
) )

View File

@@ -17,10 +17,10 @@ export const HelpAndFeedback: FunctionComponent = () => (
<div className="h-2 w-full" /> <div className="h-2 w-full" />
<Subtitle>Who can read my private notes?</Subtitle> <Subtitle>Who can read my private notes?</Subtitle>
<Text> <Text>
Quite simply: no one but you. Not us, not your ISP, not a hacker, and not a government Quite simply: no one but you. Not us, not your ISP, not a hacker, and not a government agency. As long as you
agency. As long as you keep your password safe, and your password is reasonably strong, keep your password safe, and your password is reasonably strong, then you are the only person in the world
then you are the only person in the world with the ability to decrypt your notes. For more with the ability to decrypt your notes. For more on how we handle your privacy and security, check out our
on how we handle your privacy and security, check out our easy to read{' '} easy to read{' '}
<a target="_blank" href="https://standardnotes.com/privacy"> <a target="_blank" href="https://standardnotes.com/privacy">
Privacy Manifesto. Privacy Manifesto.
</a> </a>
@@ -29,21 +29,17 @@ export const HelpAndFeedback: FunctionComponent = () => (
<PreferencesSegment> <PreferencesSegment>
<Subtitle>Can I collaborate with others on a note?</Subtitle> <Subtitle>Can I collaborate with others on a note?</Subtitle>
<Text> <Text>
Because of our encrypted architecture, Standard Notes does not currently provide a Because of our encrypted architecture, Standard Notes does not currently provide a real-time collaboration
real-time collaboration solution. Multiple users can share the same account however, but solution. Multiple users can share the same account however, but editing at the same time may result in sync
editing at the same time may result in sync conflicts, which may result in the duplication conflicts, which may result in the duplication of notes.
of notes.
</Text> </Text>
</PreferencesSegment> </PreferencesSegment>
<PreferencesSegment> <PreferencesSegment>
<Subtitle>Can I use Standard Notes totally offline?</Subtitle> <Subtitle>Can I use Standard Notes totally offline?</Subtitle>
<Text> <Text>
Standard Notes can be used totally offline without an account, and without an internet Standard Notes can be used totally offline without an account, and without an internet connection. You can
connection. You can find{' '} find{' '}
<a <a target="_blank" href="https://standardnotes.com/help/59/can-i-use-standard-notes-totally-offline">
target="_blank"
href="https://standardnotes.com/help/59/can-i-use-standard-notes-totally-offline"
>
more details here. more details here.
</a> </a>
</Text> </Text>
@@ -57,38 +53,25 @@ export const HelpAndFeedback: FunctionComponent = () => (
<PreferencesSegment> <PreferencesSegment>
<Title>Community forum</Title> <Title>Community forum</Title>
<Text> <Text>
If you have an issue, found a bug or want to suggest a feature, you can browse or post to If you have an issue, found a bug or want to suggest a feature, you can browse or post to the forum. Its
the forum. Its recommended for non-account related issues. Please read our{' '} recommended for non-account related issues. Please read our{' '}
<a target="_blank" href="https://standardnotes.com/longevity/"> <a target="_blank" href="https://standardnotes.com/longevity/">
Longevity statement Longevity statement
</a>{' '} </a>{' '}
before advocating for a feature request. before advocating for a feature request.
</Text> </Text>
<LinkButton <LinkButton className="mt-3" label="Go to the forum" link="https://forum.standardnotes.org/" />
className="mt-3"
label="Go to the forum"
link="https://forum.standardnotes.org/"
/>
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>
<PreferencesGroup> <PreferencesGroup>
<PreferencesSegment> <PreferencesSegment>
<Title>Community groups</Title> <Title>Community groups</Title>
<Text> <Text>
Want to meet other passionate note-takers and privacy enthusiasts? Want to share your Want to meet other passionate note-takers and privacy enthusiasts? Want to share your feedback with us? Join
feedback with us? Join the Standard Notes community groups for discussions on security, the Standard Notes community groups for discussions on security, themes, editors and more.
themes, editors and more.
</Text> </Text>
<LinkButton <LinkButton className="mt-3" link="https://standardnotes.com/slack" label="Join our Slack" />
className="mt-3" <LinkButton className="mt-3" link="https://standardnotes.com/discord" label="Join our Discord" />
link="https://standardnotes.com/slack"
label="Join our Slack"
/>
<LinkButton
className="mt-3"
link="https://standardnotes.com/discord"
label="Join our Discord"
/>
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>
<PreferencesGroup> <PreferencesGroup>

View File

@@ -11,11 +11,7 @@ type Props = {
application: WebApplication application: WebApplication
} }
export const ListedAccountItem: FunctionalComponent<Props> = ({ export const ListedAccountItem: FunctionalComponent<Props> = ({ account, showSeparator, application }) => {
account,
showSeparator,
application,
}) => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [accountInfo, setAccountInfo] = useState<ListedAccountInfo>() const [accountInfo, setAccountInfo] = useState<ListedAccountInfo>()

View File

@@ -84,8 +84,8 @@ export const Listed = observer(({ application }: Props) => {
<div className="h-2 w-full" /> <div className="h-2 w-full" />
<Subtitle>What is Listed?</Subtitle> <Subtitle>What is Listed?</Subtitle>
<Text> <Text>
Listed is a free blogging platform that allows you to create a public journal published Listed is a free blogging platform that allows you to create a public journal published directly from your
directly from your notes.{' '} notes.{' '}
<a target="_blank" href="https://listed.to" rel="noreferrer noopener"> <a target="_blank" href="https://listed.to" rel="noreferrer noopener">
Learn more Learn more
</a> </a>

View File

@@ -3,12 +3,7 @@ import { STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_LOCAL_ENC_ENABLED }
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { ComponentChild, FunctionComponent } from 'preact' import { ComponentChild, FunctionComponent } from 'preact'
import { import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/Components/Preferences/PreferencesComponents'
PreferencesGroup,
PreferencesSegment,
Text,
Title,
} from '@/Components/Preferences/PreferencesComponents'
const formatCount = (count: number, itemType: string) => `${count} / ${count} ${itemType}` const formatCount = (count: number, itemType: string) => `${count} / ${count} ${itemType}`

View File

@@ -18,12 +18,7 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { ApplicationEvent } from '@standardnotes/snjs' import { ApplicationEvent } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { import { PreferencesSegment, Title, Text, PreferencesGroup } from '@/Components/Preferences/PreferencesComponents'
PreferencesSegment,
Title,
Text,
PreferencesGroup,
} from '@/Components/Preferences/PreferencesComponents'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
type Props = { type Props = {
@@ -35,8 +30,7 @@ export const PasscodeLock = observer(({ application, appState }: Props) => {
const keyStorageInfo = StringUtils.keyStorageInfo(application) const keyStorageInfo = StringUtils.keyStorageInfo(application)
const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions() const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions()
const { setIsEncryptionEnabled, setIsBackupEncrypted, setEncryptionStatusString } = const { setIsEncryptionEnabled, setIsBackupEncrypted, setEncryptionStatusString } = appState.accountMenu
appState.accountMenu
const passcodeInputRef = useRef<HTMLInputElement>(null) const passcodeInputRef = useRef<HTMLInputElement>(null)
@@ -109,9 +103,7 @@ export const PasscodeLock = observer(({ application, appState }: Props) => {
setPasscodeConfirmation(value) setPasscodeConfirmation(value)
} }
const submitPasscodeForm = async ( const submitPasscodeForm = async (event: TargetedEvent<HTMLFormElement> | TargetedMouseEvent<HTMLButtonElement>) => {
event: TargetedEvent<HTMLFormElement> | TargetedMouseEvent<HTMLButtonElement>,
) => {
event.preventDefault() event.preventDefault()
if (!passcode || passcode.length === 0) { if (!passcode || passcode.length === 0) {
@@ -183,22 +175,18 @@ export const PasscodeLock = observer(({ application, appState }: Props) => {
{!hasPasscode && canAddPasscode && ( {!hasPasscode && canAddPasscode && (
<> <>
<Text className="mb-3"> <Text className="mb-3">Add a passcode to lock the application and encrypt on-device key storage.</Text>
Add a passcode to lock the application and encrypt on-device key storage.
</Text>
{keyStorageInfo && <Text className="mb-3">{keyStorageInfo}</Text>} {keyStorageInfo && <Text className="mb-3">{keyStorageInfo}</Text>}
{!showPasscodeForm && ( {!showPasscodeForm && <Button label="Add passcode" onClick={handleAddPassCode} variant="primary" />}
<Button label="Add passcode" onClick={handleAddPassCode} variant="primary" />
)}
</> </>
)} )}
{!hasPasscode && !canAddPasscode && ( {!hasPasscode && !canAddPasscode && (
<Text> <Text>
Adding a passcode is not supported in temporary sessions. Please sign out, then sign Adding a passcode is not supported in temporary sessions. Please sign out, then sign back in with the
back in with the "Stay signed in" option checked. "Stay signed in" option checked.
</Text> </Text>
)} )}
@@ -221,12 +209,7 @@ export const PasscodeLock = observer(({ application, appState }: Props) => {
placeholder="Confirm Passcode" placeholder="Confirm Passcode"
/> />
<div className="min-h-2" /> <div className="min-h-2" />
<Button <Button variant="primary" onClick={submitPasscodeForm} label="Set Passcode" className="mr-3" />
variant="primary"
onClick={submitPasscodeForm}
label="Set Passcode"
className="mr-3"
/>
<Button variant="normal" onClick={() => setShowPasscodeForm(false)} label="Cancel" /> <Button variant="normal" onClick={() => setShowPasscodeForm(false)} label="Cancel" />
</form> </form>
)} )}
@@ -235,17 +218,8 @@ export const PasscodeLock = observer(({ application, appState }: Props) => {
<> <>
<Text>Passcode lock is enabled.</Text> <Text>Passcode lock is enabled.</Text>
<div className="flex flex-row mt-3"> <div className="flex flex-row mt-3">
<Button <Button variant="normal" label="Change Passcode" onClick={changePasscodePressed} className="mr-3" />
variant="normal" <Button dangerStyle={true} label="Remove Passcode" onClick={removePasscodePressed} />
label="Change Passcode"
onClick={changePasscodePressed}
className="mr-3"
/>
<Button
dangerStyle={true}
label="Remove Passcode"
onClick={removePasscodePressed}
/>
</div> </div>
</> </>
)} )}
@@ -258,16 +232,12 @@ export const PasscodeLock = observer(({ application, appState }: Props) => {
<PreferencesGroup> <PreferencesGroup>
<PreferencesSegment> <PreferencesSegment>
<Title>Autolock</Title> <Title>Autolock</Title>
<Text className="mb-3"> <Text className="mb-3">The autolock timer begins when the window or tab loses focus.</Text>
The autolock timer begins when the window or tab loses focus.
</Text>
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
{passcodeAutoLockOptions.map((option) => { {passcodeAutoLockOptions.map((option) => {
return ( return (
<a <a
className={`sk-a info mr-3 ${ className={`sk-a info mr-3 ${option.value === selectedAutoLockInterval ? 'boxed' : ''}`}
option.value === selectedAutoLockInterval ? 'boxed' : ''
}`}
onClick={() => selectAutoLockInterval(option.value)} onClick={() => selectAutoLockInterval(option.value)}
> >
{option.label} {option.label}

View File

@@ -71,9 +71,7 @@ export const Privacy: FunctionalComponent<Props> = observer(({ application }: Pr
const toggleMuteSignInEmails = async () => { const toggleMuteSignInEmails = async () => {
const previousValue = signInEmailsMutedValue const previousValue = signInEmailsMutedValue
const newValue = const newValue =
previousValue === MuteSignInEmailsOption.Muted previousValue === MuteSignInEmailsOption.Muted ? MuteSignInEmailsOption.NotMuted : MuteSignInEmailsOption.Muted
? MuteSignInEmailsOption.NotMuted
: MuteSignInEmailsOption.Muted
setSignInEmailsMutedValue(newValue) setSignInEmailsMutedValue(newValue)
const updateResult = await updateSetting(SettingName.MuteSignInEmails, newValue) const updateResult = await updateSetting(SettingName.MuteSignInEmails, newValue)
@@ -107,8 +105,8 @@ export const Privacy: FunctionalComponent<Props> = observer(({ application }: Pr
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Disable sign-in notification emails</Subtitle> <Subtitle>Disable sign-in notification emails</Subtitle>
<Text> <Text>
Disables email notifications when a new sign-in occurs on your account. (Email Disables email notifications when a new sign-in occurs on your account. (Email notifications are
notifications are available to paid subscribers). available to paid subscribers).
</Text> </Text>
</div> </div>
{isLoading ? ( {isLoading ? (
@@ -125,9 +123,9 @@ export const Privacy: FunctionalComponent<Props> = observer(({ application }: Pr
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Session user agent logging</Subtitle> <Subtitle>Session user agent logging</Subtitle>
<Text> <Text>
User agent logging allows you to identify the devices or browsers signed into your User agent logging allows you to identify the devices or browsers signed into your account. For
account. For increased privacy, you can disable this feature, which will remove all increased privacy, you can disable this feature, which will remove all saved user agent values from our
saved user agent values from our server, and disable future logging of this value. server, and disable future logging of this value.
</Text> </Text>
</div> </div>
{isLoading ? ( {isLoading ? (

View File

@@ -3,12 +3,7 @@ import { FunctionalComponent } from 'preact'
import { useCallback, useState, useEffect } from 'preact/hooks' import { useCallback, useState, useEffect } from 'preact/hooks'
import { ApplicationEvent } from '@standardnotes/snjs' import { ApplicationEvent } from '@standardnotes/snjs'
import { isSameDay } from '@/Utils' import { isSameDay } from '@/Utils'
import { import { PreferencesGroup, PreferencesSegment, Title, Text } from '@/Components/Preferences/PreferencesComponents'
PreferencesGroup,
PreferencesSegment,
Title,
Text,
} from '@/Components/Preferences/PreferencesComponents'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
type Props = { type Props = {
@@ -47,9 +42,7 @@ export const Protections: FunctionalComponent<Props> = ({ application }) => {
return null return null
}, [application]) }, [application])
const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState( const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil())
getProtectionsDisabledUntil(),
)
useEffect(() => { useEffect(() => {
const removeUnprotectedSessionBeginObserver = application.addEventObserver(async () => { const removeUnprotectedSessionBeginObserver = application.addEventObserver(async () => {
@@ -85,17 +78,11 @@ export const Protections: FunctionalComponent<Props> = ({ application }) => {
<Text className="info">Protections are enabled.</Text> <Text className="info">Protections are enabled.</Text>
)} )}
<Text className="mt-2"> <Text className="mt-2">
Actions like viewing or searching protected notes, exporting decrypted backups, or Actions like viewing or searching protected notes, exporting decrypted backups, or revoking an active session
revoking an active session require additional authentication such as entering your account require additional authentication such as entering your account password or application passcode.
password or application passcode.
</Text> </Text>
{protectionsDisabledUntil && ( {protectionsDisabledUntil && (
<Button <Button className="mt-3" variant="primary" label="End Unprotected Access" onClick={enableProtections} />
className="mt-3"
variant="primary"
label="End Unprotected Access"
onClick={enableProtections}
/>
)} )}
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>

View File

@@ -14,9 +14,7 @@ const DisclosureIconButton: FunctionComponent<{
<DisclosureButton <DisclosureButton
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${ className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${className ?? ''}`}
className ?? ''
}`}
> >
<Icon type={icon} /> <Icon type={icon} />
</DisclosureButton> </DisclosureButton>
@@ -56,8 +54,8 @@ export const AuthAppInfoTooltip: FunctionComponent = () => {
className={`bg-inverted-default color-inverted-default text-center rounded shadow-overlay className={`bg-inverted-default color-inverted-default text-center rounded shadow-overlay
py-1.5 px-2 absolute w-103 -top-10 -left-51`} py-1.5 px-2 absolute w-103 -top-10 -left-51`}
> >
Some apps, like Google Authenticator, do not back up and restore your secret keys if you Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or
lose your device or get a new one. get a new one.
</div> </div>
</DisclosurePanel> </DisclosurePanel>
</div> </div>

View File

@@ -76,18 +76,8 @@ export const SaveSecretKey: FunctionComponent<{
</div> </div>
</ModalDialogDescription> </ModalDialogDescription>
<ModalDialogButtons> <ModalDialogButtons>
<Button <Button className="min-w-20" variant="normal" label="Back" onClick={() => act.openScanQRCode()} />
className="min-w-20" <Button className="min-w-20" variant="primary" label="Next" onClick={() => act.openVerification()} />
variant="normal"
label="Back"
onClick={() => act.openScanQRCode()}
/>
<Button
className="min-w-20"
variant="primary"
label="Next"
onClick={() => act.openVerification()}
/>
</ModalDialogButtons> </ModalDialogButtons>
</ModalDialog> </ModalDialog>
) )

View File

@@ -19,16 +19,10 @@ export const ScanQRCode: FunctionComponent<{
}> = observer(({ activation: act }) => { }> = observer(({ activation: act }) => {
return ( return (
<ModalDialog> <ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}> <ModalDialogLabel closeDialog={act.cancelActivation}>Step 1 of 3 - Scan QR code</ModalDialogLabel>
Step 1 of 3 - Scan QR code
</ModalDialogLabel>
<ModalDialogDescription className="h-33"> <ModalDialogDescription className="h-33">
<div className="w-25 h-25 flex items-center justify-center bg-info"> <div className="w-25 h-25 flex items-center justify-center bg-info">
<QRCode <QRCode className="border-neutral-contrast-bg border-solid border-2" value={act.qrCode} size={100} />
className="border-neutral-contrast-bg border-solid border-2"
value={act.qrCode}
size={100}
/>
</div> </div>
<div className="min-w-5" /> <div className="min-w-5" />
<div className="flex-grow flex flex-col"> <div className="flex-grow flex flex-col">
@@ -59,18 +53,8 @@ export const ScanQRCode: FunctionComponent<{
</div> </div>
</ModalDialogDescription> </ModalDialogDescription>
<ModalDialogButtons> <ModalDialogButtons>
<Button <Button className="min-w-20" variant="normal" label="Cancel" onClick={() => act.cancelActivation()} />
className="min-w-20" <Button className="min-w-20" variant="primary" label="Next" onClick={() => act.openSaveSecretKey()} />
variant="normal"
label="Cancel"
onClick={() => act.cancelActivation()}
/>
<Button
className="min-w-20"
variant="primary"
label="Next"
onClick={() => act.openSaveSecretKey()}
/>
</ModalDialogButtons> </ModalDialogButtons>
</ModalDialog> </ModalDialog>
) )

View File

@@ -25,12 +25,7 @@ export class TwoFactorActivation {
makeAutoObservable< makeAutoObservable<
TwoFactorActivation, TwoFactorActivation,
| '_secretKey' '_secretKey' | '_authCode' | '_step' | '_enable2FAVerification' | 'inputOtpToken' | 'inputSecretKey'
| '_authCode'
| '_step'
| '_enable2FAVerification'
| 'inputOtpToken'
| 'inputSecretKey'
>( >(
this, this,
{ {

View File

@@ -10,23 +10,16 @@ export const is2FADisabled = (status: TwoFactorStatus): status is 'two-factor-di
export const is2FAActivation = (status: TwoFactorStatus): status is TwoFactorActivation => export const is2FAActivation = (status: TwoFactorStatus): status is TwoFactorActivation =>
(status as TwoFactorActivation)?.type === 'two-factor-activation' (status as TwoFactorActivation)?.type === 'two-factor-activation'
export const is2FAEnabled = (status: TwoFactorStatus): status is 'two-factor-enabled' => export const is2FAEnabled = (status: TwoFactorStatus): status is 'two-factor-enabled' => status === 'two-factor-enabled'
status === 'two-factor-enabled'
export class TwoFactorAuth { export class TwoFactorAuth {
private _status: TwoFactorStatus | 'fetching' = 'fetching' private _status: TwoFactorStatus | 'fetching' = 'fetching'
private _errorMessage: string | null private _errorMessage: string | null
constructor( constructor(private readonly mfaProvider: MfaProvider, private readonly userProvider: UserProvider) {
private readonly mfaProvider: MfaProvider,
private readonly userProvider: UserProvider,
) {
this._errorMessage = null this._errorMessage = null
makeAutoObservable< makeAutoObservable<TwoFactorAuth, '_status' | '_errorMessage' | 'deactivateMfa' | 'startActivation'>(
TwoFactorAuth,
'_status' | '_errorMessage' | 'deactivateMfa' | 'startActivation'
>(
this, this,
{ {
_status: observable, _status: observable,

View File

@@ -1,10 +1,5 @@
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { import { Title, Text, PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
Title,
Text,
PreferencesGroup,
PreferencesSegment,
} from '@/Components/Preferences/PreferencesComponents'
import { Switch } from '@/Components/Switch' import { Switch } from '@/Components/Switch'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth' import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth'

View File

@@ -16,18 +16,11 @@ export const TwoFactorSuccess: FunctionComponent<{
<ModalDialogLabel closeDialog={act.finishActivation}>Successfully Enabled</ModalDialogLabel> <ModalDialogLabel closeDialog={act.finishActivation}>Successfully Enabled</ModalDialogLabel>
<ModalDialogDescription> <ModalDialogDescription>
<div className="flex flex-row items-center justify-center pt-2"> <div className="flex flex-row items-center justify-center pt-2">
<Subtitle> <Subtitle>Two-factor authentication has been successfully enabled for your account.</Subtitle>
Two-factor authentication has been successfully enabled for your account.
</Subtitle>
</div> </div>
</ModalDialogDescription> </ModalDialogDescription>
<ModalDialogButtons> <ModalDialogButtons>
<Button <Button className="min-w-20" variant="primary" label="Finish" onClick={act.finishActivation} />
className="min-w-20"
variant="primary"
label="Finish"
onClick={act.finishActivation}
/>
</ModalDialogButtons> </ModalDialogButtons>
</ModalDialog> </ModalDialog>
)) ))

View File

@@ -18,9 +18,7 @@ export const Verification: FunctionComponent<{
const authTokenClass = act.verificationStatus === 'invalid-auth-code' ? 'border-danger' : '' const authTokenClass = act.verificationStatus === 'invalid-auth-code' ? 'border-danger' : ''
return ( return (
<ModalDialog> <ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}> <ModalDialogLabel closeDialog={act.cancelActivation}>Step 3 of 3 - Verification</ModalDialogLabel>
Step 3 of 3 - Verification
</ModalDialogLabel>
<ModalDialogDescription className="h-33"> <ModalDialogDescription className="h-33">
<div className="flex-grow flex flex-col"> <div className="flex-grow flex flex-col">
<div className="flex flex-row items-center mb-4"> <div className="flex flex-row items-center mb-4">
@@ -45,21 +43,12 @@ export const Verification: FunctionComponent<{
</ModalDialogDescription> </ModalDialogDescription>
<ModalDialogButtons> <ModalDialogButtons>
{act.verificationStatus === 'invalid-auth-code' && ( {act.verificationStatus === 'invalid-auth-code' && (
<div className="text-sm color-danger flex-grow"> <div className="text-sm color-danger flex-grow">Incorrect authentication code, please try again.</div>
Incorrect authentication code, please try again.
</div>
)} )}
{act.verificationStatus === 'invalid-secret' && ( {act.verificationStatus === 'invalid-secret' && (
<div className="text-sm color-danger flex-grow"> <div className="text-sm color-danger flex-grow">Incorrect secret key, please try again.</div>
Incorrect secret key, please try again.
</div>
)} )}
<Button <Button className="min-w-20" variant="normal" label="Back" onClick={act.openSaveSecretKey} />
className="min-w-20"
variant="normal"
label="Back"
onClick={act.openSaveSecretKey}
/>
<Button className="min-w-20" variant="primary" label="Next" onClick={act.enable2FA} /> <Button className="min-w-20" variant="primary" label="Next" onClick={act.enable2FA} />
</ModalDialogButtons> </ModalDialogButtons>
</ModalDialog> </ModalDialog>

View File

@@ -7,15 +7,13 @@ export const Title: FunctionComponent = ({ children }) => (
</> </>
) )
export const Subtitle: FunctionComponent<{ className?: string }> = ({ export const Subtitle: FunctionComponent<{ className?: string }> = ({ children, className = '' }) => (
children, <h4 className={`font-medium text-sm m-0 mb-1 ${className}`}>{children}</h4>
className = '', )
}) => <h4 className={`font-medium text-sm m-0 mb-1 ${className}`}>{children}</h4>
export const SubtitleLight: FunctionComponent<{ className?: string }> = ({ export const SubtitleLight: FunctionComponent<{ className?: string }> = ({ children, className = '' }) => (
children, <h4 className={`font-normal text-sm m-0 mb-1 ${className}`}>{children}</h4>
className = '', )
}) => <h4 className={`font-normal text-sm m-0 mb-1 ${className}`}>{children}</h4>
export const Text: FunctionComponent<{ className?: string }> = ({ children, className = '' }) => ( export const Text: FunctionComponent<{ className?: string }> = ({ children, className = '' }) => (
<p className={`${className} text-xs`}>{children}</p> <p className={`${className} text-xs`}>{children}</p>

View File

@@ -10,13 +10,7 @@ interface Props {
onClick: () => void onClick: () => void
} }
export const MenuItem: FunctionComponent<Props> = ({ export const MenuItem: FunctionComponent<Props> = ({ iconType, label, selected, onClick, hasBubble }) => (
iconType,
label,
selected,
onClick,
hasBubble,
}) => (
<div <div
className={`preferences-menu-item select-none ${selected ? 'selected' : ''}`} className={`preferences-menu-item select-none ${selected ? 'selected' : ''}`}
onClick={(e) => { onClick={(e) => {

View File

@@ -4,9 +4,7 @@ export const PreferencesPane: FunctionComponent = ({ children }) => (
<div className="color-foreground flex-grow flex flex-row overflow-y-auto min-h-0"> <div className="color-foreground flex-grow flex flex-row overflow-y-auto min-h-0">
<div className="flex-grow flex flex-col py-6 items-center"> <div className="flex-grow flex flex-col py-6 items-center">
<div className="w-125 max-w-125 flex flex-col"> <div className="w-125 max-w-125 flex flex-col">
{children != undefined && Array.isArray(children) {children != undefined && Array.isArray(children) ? children.filter((child) => child != undefined) : children}
? children.filter((child) => child != undefined)
: children}
</div> </div>
</div> </div>
<div className="flex-basis-55 flex-shrink" /> <div className="flex-basis-55 flex-shrink" />

View File

@@ -59,27 +59,16 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
export class PreferencesMenu { export class PreferencesMenu {
private _selectedPane: PreferenceId = 'account' private _selectedPane: PreferenceId = 'account'
private _menu: PreferencesMenuItem[] private _menu: PreferencesMenuItem[]
private _extensionLatestVersions: ExtensionsLatestVersions = new ExtensionsLatestVersions( private _extensionLatestVersions: ExtensionsLatestVersions = new ExtensionsLatestVersions(new Map())
new Map(),
)
constructor( constructor(private application: WebApplication, private readonly _enableUnfinishedFeatures: boolean) {
private application: WebApplication, this._menu = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS
private readonly _enableUnfinishedFeatures: boolean,
) {
this._menu = this._enableUnfinishedFeatures
? PREFERENCES_MENU_ITEMS
: READY_PREFERENCES_MENU_ITEMS
this.loadLatestVersions() this.loadLatestVersions()
makeAutoObservable< makeAutoObservable<
PreferencesMenu, PreferencesMenu,
| '_selectedPane' '_selectedPane' | '_twoFactorAuth' | '_extensionPanes' | '_extensionLatestVersions' | 'loadLatestVersions'
| '_twoFactorAuth'
| '_extensionPanes'
| '_extensionLatestVersions'
| 'loadLatestVersions'
>(this, { >(this, {
_twoFactorAuth: observable, _twoFactorAuth: observable,
_selectedPane: observable, _selectedPane: observable,

View File

@@ -67,14 +67,12 @@ const PaneSelector: FunctionComponent<PreferencesProps & { menu: PreferencesMenu
}, },
) )
const PreferencesCanvas: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = observer( const PreferencesCanvas: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = observer((props) => (
(props) => ( <div className="flex flex-row flex-grow min-h-0 justify-between">
<div className="flex flex-row flex-grow min-h-0 justify-between"> <PreferencesMenuView menu={props.menu} />
<PreferencesMenuView menu={props.menu} /> <PaneSelector {...props} />
<PaneSelector {...props} /> </div>
</div> ))
),
)
export const PreferencesView: FunctionComponent<PreferencesProps> = observer((props) => { export const PreferencesView: FunctionComponent<PreferencesProps> = observer((props) => {
const menu = useMemo( const menu = useMemo(

View File

@@ -51,8 +51,8 @@ export const PremiumFeaturesModal: FunctionalComponent<Props> = ({
<div className="text-lg text-center font-bold mb-1">Enable Premium Features</div> <div className="text-lg text-center font-bold mb-1">Enable Premium Features</div>
</AlertDialogLabel> </AlertDialogLabel>
<AlertDialogDescription className="text-sm text-center color-grey-1 px-4.5 mb-2"> <AlertDialogDescription className="text-sm text-center color-grey-1 px-4.5 mb-2">
In order to use <span className="font-semibold">{featureName}</span> and other premium In order to use <span className="font-semibold">{featureName}</span> and other premium features, please
features, please purchase a subscription or upgrade your current plan. purchase a subscription or upgrade your current plan.
</AlertDialogDescription> </AlertDialogDescription>
<div className="p-4"> <div className="p-4">
<button <button

View File

@@ -8,12 +8,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'
import { FloatingLabelInput } from '@/Components/Input/FloatingLabelInput' import { FloatingLabelInput } from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils' import { isEmailValid } from '@/Utils'
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper' import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/stylekit'
BlueDotIcon,
CircleIcon,
DiamondIcon,
CreateAccountIllustration,
} from '@standardnotes/stylekit'
type Props = { type Props = {
appState: AppState appState: AppState
@@ -145,9 +140,7 @@ export const CreateAccount: FunctionComponent<Props> = observer(({ appState, app
disabled={isCreatingAccount} disabled={isCreatingAccount}
isInvalid={isEmailInvalid} isInvalid={isEmailInvalid}
/> />
{isEmailInvalid ? ( {isEmailInvalid ? <div className="color-dark-red mb-4">Please provide a valid email.</div> : null}
<div className="color-dark-red mb-4">Please provide a valid email.</div>
) : null}
<FloatingLabelInput <FloatingLabelInput
className="min-w-90 xs:min-w-auto mb-4" className="min-w-90 xs:min-w-auto mb-4"
id="purchase-create-account-password" id="purchase-create-account-password"

View File

@@ -114,9 +114,7 @@ export const SignIn: FunctionComponent<Props> = observer(({ appState, applicatio
<form onSubmit={handleSignIn}> <form onSubmit={handleSignIn}>
<div className="flex flex-col"> <div className="flex flex-col">
<FloatingLabelInput <FloatingLabelInput
className={`min-w-90 xs:min-w-auto ${ className={`min-w-90 xs:min-w-auto ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'
}`}
id="purchase-sign-in-email" id="purchase-sign-in-email"
type="email" type="email"
label="Email" label="Email"
@@ -140,9 +138,7 @@ export const SignIn: FunctionComponent<Props> = observer(({ appState, applicatio
disabled={isSigningIn} disabled={isSigningIn}
isInvalid={isPasswordInvalid} isInvalid={isPasswordInvalid}
/> />
{otherErrorMessage ? ( {otherErrorMessage ? <div className="color-dark-red mb-4">{otherErrorMessage}</div> : null}
<div className="color-dark-red mb-4">{otherErrorMessage}</div>
) : null}
</div> </div>
<Button <Button
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} py-2.5 mb-5`} className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} py-2.5 mb-5`}

View File

@@ -16,11 +16,7 @@ type PurchaseFlowViewProps = {
application: WebApplication application: WebApplication
} }
const PurchaseFlowPaneSelector: FunctionComponent<PaneSelectorProps> = ({ const PurchaseFlowPaneSelector: FunctionComponent<PaneSelectorProps> = ({ currentPane, appState, application }) => {
currentPane,
appState,
application,
}) => {
switch (currentPane) { switch (currentPane) {
case PurchaseFlowPane.CreateAccount: case PurchaseFlowPane.CreateAccount:
return <CreateAccount appState={appState} application={application} /> return <CreateAccount appState={appState} application={application} />
@@ -29,41 +25,35 @@ const PurchaseFlowPaneSelector: FunctionComponent<PaneSelectorProps> = ({
} }
} }
export const PurchaseFlowView: FunctionComponent<PurchaseFlowViewProps> = observer( export const PurchaseFlowView: FunctionComponent<PurchaseFlowViewProps> = observer(({ appState, application }) => {
({ appState, application }) => { const { currentPane } = appState.purchaseFlow
const { currentPane } = appState.purchaseFlow
return ( return (
<div className="flex items-center justify-center overflow-hidden h-full w-full absolute top-left-0 z-index-purchase-flow bg-grey-super-light"> <div className="flex items-center justify-center overflow-hidden h-full w-full absolute top-left-0 z-index-purchase-flow bg-grey-super-light">
<div className="relative fit-content"> <div className="relative fit-content">
<div className="relative p-12 xs:px-8 mb-4 bg-default border-1 border-solid border-main rounded xs:rounded-0"> <div className="relative p-12 xs:px-8 mb-4 bg-default border-1 border-solid border-main rounded xs:rounded-0">
<SNLogoFull className="mb-5" /> <SNLogoFull className="mb-5" />
<PurchaseFlowPaneSelector <PurchaseFlowPaneSelector currentPane={currentPane} appState={appState} application={application} />
currentPane={currentPane} </div>
appState={appState} <div className="flex justify-end xs:px-4">
application={application} <a
/> className="mr-3 font-medium color-grey-1"
</div> href="https://standardnotes.com/privacy"
<div className="flex justify-end xs:px-4"> target="_blank"
<a rel="noopener noreferrer"
className="mr-3 font-medium color-grey-1" >
href="https://standardnotes.com/privacy" Privacy
target="_blank" </a>
rel="noopener noreferrer" <a
> className="font-medium color-grey-1"
Privacy href="https://standardnotes.com/help"
</a> target="_blank"
<a rel="noopener noreferrer"
className="font-medium color-grey-1" >
href="https://standardnotes.com/help" Help
target="_blank" </a>
rel="noopener noreferrer"
>
Help
</a>
</div>
</div> </div>
</div> </div>
) </div>
}, )
) })

View File

@@ -10,9 +10,7 @@ export type PurchaseFlowWrapperProps = {
application: WebApplication application: WebApplication
} }
export const getPurchaseFlowUrl = async ( export const getPurchaseFlowUrl = async (application: WebApplication): Promise<string | undefined> => {
application: WebApplication,
): Promise<string | undefined> => {
const currentUrl = window.location.origin const currentUrl = window.location.origin
const successUrl = isDesktopApplication() ? 'standardnotes://' : currentUrl const successUrl = isDesktopApplication() ? 'standardnotes://' : currentUrl
if (application.noAccount()) { if (application.noAccount()) {

View File

@@ -9,8 +9,7 @@ export const quickSettingsKeyDownHandler = (
themesMenuOpen: boolean, themesMenuOpen: boolean,
) => { ) => {
if (quickSettingsMenuRef?.current) { if (quickSettingsMenuRef?.current) {
const items: NodeListOf<HTMLButtonElement> = const items: NodeListOf<HTMLButtonElement> = quickSettingsMenuRef.current.querySelectorAll(':scope > button')
quickSettingsMenuRef.current.querySelectorAll(':scope > button')
const currentFocusedIndex = Array.from(items).findIndex((btn) => btn === document.activeElement) const currentFocusedIndex = Array.from(items).findIndex((btn) => btn === document.activeElement)
if (!themesMenuOpen) { if (!themesMenuOpen) {
@@ -45,9 +44,7 @@ export const themesMenuKeyDownHandler = (
) => { ) => {
if (themesMenuRef?.current) { if (themesMenuRef?.current) {
const themes = themesMenuRef.current.querySelectorAll('button') const themes = themesMenuRef.current.querySelectorAll('button')
const currentFocusedIndex = Array.from(themes).findIndex( const currentFocusedIndex = Array.from(themes).findIndex((themeBtn) => themeBtn === document.activeElement)
(themeBtn) => themeBtn === document.activeElement,
)
switch (event.key) { switch (event.key) {
case 'Escape': case 'Escape':

View File

@@ -14,15 +14,9 @@ type Props = {
isEnabled: boolean isEnabled: boolean
} }
export const FocusModeSwitch: FunctionComponent<Props> = ({ export const FocusModeSwitch: FunctionComponent<Props> = ({ application, onToggle, onClose, isEnabled }) => {
application,
onToggle,
onClose,
isEnabled,
}) => {
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const isEntitled = const isEntitled = application.features.getFeatureStatus(FeatureIdentifier.FocusMode) === FeatureStatus.Entitled
application.features.getFeatureStatus(FeatureIdentifier.FocusMode) === FeatureStatus.Entitled
const toggle = useCallback( const toggle = useCallback(
(e: JSXInternal.TargetedMouseEvent<HTMLButtonElement>) => { (e: JSXInternal.TargetedMouseEvent<HTMLButtonElement>) => {
@@ -40,10 +34,7 @@ export const FocusModeSwitch: FunctionComponent<Props> = ({
return ( return (
<> <>
<button <button className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between" onClick={toggle}>
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between"
onClick={toggle}
>
<div className="flex items-center"> <div className="flex items-center">
<Icon type="menu-close" className="color-neutral mr-2" /> <Icon type="menu-close" className="color-neutral mr-2" />
Focused Writing Focused Writing

View File

@@ -25,10 +25,7 @@ export const ThemesMenuButton: FunctionComponent<Props> = ({ application, item,
() => application.features.getFeatureStatus(item.identifier) === FeatureStatus.Entitled, () => application.features.getFeatureStatus(item.identifier) === FeatureStatus.Entitled,
[application, item.identifier], [application, item.identifier],
) )
const canActivateTheme = useMemo( const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme])
() => isEntitledToTheme || isThirdPartyTheme,
[isEntitledToTheme, isThirdPartyTheme],
)
const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = (e) => { const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault() e.preventDefault()
@@ -61,14 +58,8 @@ export const ThemesMenuButton: FunctionComponent<Props> = ({ application, item,
) : ( ) : (
<> <>
<div className="flex items-center"> <div className="flex items-center">
<div <div className={`pseudo-radio-btn ${item.component?.active ? 'pseudo-radio-btn--checked' : ''} mr-2`}></div>
className={`pseudo-radio-btn ${ <span className={item.component?.active ? 'font-semibold' : undefined}>{item.name}</span>
item.component?.active ? 'pseudo-radio-btn--checked' : ''
} mr-2`}
></div>
<span className={item.component?.active ? 'font-semibold' : undefined}>
{item.name}
</span>
</div> </div>
{item.component && canActivateTheme ? ( {item.component && canActivateTheme ? (
<div <div

View File

@@ -1,14 +1,7 @@
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent, SNTheme } from '@standardnotes/snjs'
ComponentArea,
ContentType,
FeatureIdentifier,
GetFeatures,
SNComponent,
SNTheme,
} from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
@@ -25,8 +18,7 @@ import { sortThemes } from '@/Utils/SortThemes'
const focusModeAnimationDuration = 1255 const focusModeAnimationDuration = 1255
const MENU_CLASSNAME = const MENU_CLASSNAME = 'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto'
'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto'
type MenuProps = { type MenuProps = {
appState: AppState appState: AppState
@@ -48,266 +40,241 @@ const toggleFocusMode = (enabled: boolean) => {
} }
} }
export const QuickSettingsMenu: FunctionComponent<MenuProps> = observer( export const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(({ application, appState, onClickOutside }) => {
({ application, appState, onClickOutside }) => { const { closeQuickSettingsMenu, shouldAnimateCloseMenu, focusModeEnabled, setFocusModeEnabled } =
const { appState.quickSettingsMenu
closeQuickSettingsMenu, const [themes, setThemes] = useState<ThemeItem[]>([])
shouldAnimateCloseMenu, const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
focusModeEnabled, const [themesMenuOpen, setThemesMenuOpen] = useState(false)
setFocusModeEnabled, const [themesMenuPosition, setThemesMenuPosition] = useState({})
} = appState.quickSettingsMenu const [defaultThemeOn, setDefaultThemeOn] = useState(false)
const [themes, setThemes] = useState<ThemeItem[]>([])
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
const [themesMenuOpen, setThemesMenuOpen] = useState(false)
const [themesMenuPosition, setThemesMenuPosition] = useState({})
const [defaultThemeOn, setDefaultThemeOn] = useState(false)
const themesMenuRef = useRef<HTMLDivElement>(null) const themesMenuRef = useRef<HTMLDivElement>(null)
const themesButtonRef = useRef<HTMLButtonElement>(null) const themesButtonRef = useRef<HTMLButtonElement>(null)
const prefsButtonRef = useRef<HTMLButtonElement>(null) const prefsButtonRef = useRef<HTMLButtonElement>(null)
const quickSettingsMenuRef = useRef<HTMLDivElement>(null) const quickSettingsMenuRef = useRef<HTMLDivElement>(null)
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null) const defaultThemeButtonRef = useRef<HTMLButtonElement>(null)
const mainRef = useRef<HTMLDivElement>(null) const mainRef = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(mainRef, () => { useCloseOnClickOutside(mainRef, () => {
onClickOutside() onClickOutside()
})
useEffect(() => {
toggleFocusMode(focusModeEnabled)
}, [focusModeEnabled])
const reloadThemes = useCallback(() => {
const themes = application.items.getDisplayableItems<SNTheme>(ContentType.Theme).map((item) => {
return {
name: item.name,
identifier: item.identifier,
component: item,
}
}) as ThemeItem[]
GetFeatures()
.filter((feature) => feature.content_type === ContentType.Theme && !feature.layerable)
.forEach((theme) => {
if (themes.findIndex((item) => item.identifier === theme.identifier) === -1) {
themes.push({
name: theme.name as string,
identifier: theme.identifier,
})
}
})
setThemes(themes.sort(sortThemes))
setDefaultThemeOn(!themes.map((item) => item?.component).find((theme) => theme?.active && !theme.isLayerable()))
}, [application])
const reloadToggleableComponents = useCallback(() => {
const toggleableComponents = application.items
.getDisplayableItems<SNComponent>(ContentType.Component)
.filter(
(component) =>
[ComponentArea.EditorStack].includes(component.area) &&
component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent,
)
setToggleableComponents(toggleableComponents)
}, [application])
useEffect(() => {
if (!themes.length) {
reloadThemes()
}
}, [reloadThemes, themes.length])
useEffect(() => {
const cleanupItemStream = application.streamItems(ContentType.Theme, () => {
reloadThemes()
}) })
useEffect(() => { return () => {
toggleFocusMode(focusModeEnabled) cleanupItemStream()
}, [focusModeEnabled]) }
}, [application, reloadThemes])
const reloadThemes = useCallback(() => { useEffect(() => {
const themes = application.items const cleanupItemStream = application.streamItems(ContentType.Component, () => {
.getDisplayableItems<SNTheme>(ContentType.Theme) reloadToggleableComponents()
.map((item) => { })
return {
name: item.name,
identifier: item.identifier,
component: item,
}
}) as ThemeItem[]
GetFeatures() return () => {
.filter((feature) => feature.content_type === ContentType.Theme && !feature.layerable) cleanupItemStream()
.forEach((theme) => { }
if (themes.findIndex((item) => item.identifier === theme.identifier) === -1) { }, [application, reloadToggleableComponents])
themes.push({
name: theme.name as string,
identifier: theme.identifier,
})
}
})
setThemes(themes.sort(sortThemes)) useEffect(() => {
if (themesMenuOpen) {
defaultThemeButtonRef.current?.focus()
}
}, [themesMenuOpen])
setDefaultThemeOn( useEffect(() => {
!themes prefsButtonRef.current?.focus()
.map((item) => item?.component) }, [])
.find((theme) => theme?.active && !theme.isLayerable()),
)
}, [application])
const reloadToggleableComponents = useCallback(() => { const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen)
const toggleableComponents = application.items
.getDisplayableItems<SNComponent>(ContentType.Component)
.filter(
(component) =>
[ComponentArea.EditorStack].includes(component.area) &&
component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent,
)
setToggleableComponents(toggleableComponents) const toggleThemesMenu = () => {
}, [application]) if (!themesMenuOpen && themesButtonRef.current) {
const themesButtonRect = themesButtonRef.current.getBoundingClientRect()
useEffect(() => { setThemesMenuPosition({
if (!themes.length) { left: themesButtonRect.right,
reloadThemes() bottom: document.documentElement.clientHeight - themesButtonRect.bottom,
}
}, [reloadThemes, themes.length])
useEffect(() => {
const cleanupItemStream = application.streamItems(ContentType.Theme, () => {
reloadThemes()
}) })
setThemesMenuOpen(true)
} else {
setThemesMenuOpen(false)
}
}
return () => { const openPreferences = () => {
cleanupItemStream() closeQuickSettingsMenu()
} appState.preferences.openPreferences()
}, [application, reloadThemes]) }
useEffect(() => { const toggleComponent = (component: SNComponent) => {
const cleanupItemStream = application.streamItems(ContentType.Component, () => { if (component.isTheme()) {
reloadToggleableComponents() application.mutator.toggleTheme(component).catch(console.error)
}) } else {
application.mutator.toggleComponent(component).catch(console.error)
}
}
return () => { const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (event) => {
cleanupItemStream() switch (event.key) {
} case 'Escape':
}, [application, reloadToggleableComponents])
useEffect(() => {
if (themesMenuOpen) {
defaultThemeButtonRef.current?.focus()
}
}, [themesMenuOpen])
useEffect(() => {
prefsButtonRef.current?.focus()
}, [])
const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen)
const toggleThemesMenu = () => {
if (!themesMenuOpen && themesButtonRef.current) {
const themesButtonRect = themesButtonRef.current.getBoundingClientRect()
setThemesMenuPosition({
left: themesButtonRect.right,
bottom: document.documentElement.clientHeight - themesButtonRect.bottom,
})
setThemesMenuOpen(true)
} else {
setThemesMenuOpen(false) setThemesMenuOpen(false)
} themesButtonRef.current?.focus()
break
case 'ArrowRight':
if (!themesMenuOpen) {
toggleThemesMenu()
}
} }
}
const openPreferences = () => { const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (event) => {
closeQuickSettingsMenu() quickSettingsKeyDownHandler(closeQuickSettingsMenu, event, quickSettingsMenuRef, themesMenuOpen)
appState.preferences.openPreferences() }
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => {
themesMenuKeyDownHandler(event, themesMenuRef, setThemesMenuOpen, themesButtonRef)
}
const toggleDefaultTheme = () => {
const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable())
if (activeTheme) {
application.mutator.toggleTheme(activeTheme).catch(console.error)
} }
}
const toggleComponent = (component: SNComponent) => { return (
if (component.isTheme()) { <div ref={mainRef} className="sn-component">
application.mutator.toggleTheme(component).catch(console.error) <div
} else { className={`sn-quick-settings-menu absolute ${MENU_CLASSNAME} ${
application.mutator.toggleComponent(component).catch(console.error) shouldAnimateCloseMenu ? 'slide-up-animation' : 'sn-dropdown--animated'
} }`}
} ref={quickSettingsMenuRef}
onKeyDown={handleQuickSettingsKeyDown}
const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (event) => { >
switch (event.key) { <div className="px-3 mt-1 mb-2 font-semibold color-text uppercase">Quick Settings</div>
case 'Escape': <Disclosure open={themesMenuOpen} onChange={toggleThemesMenu}>
setThemesMenuOpen(false) <DisclosureButton
themesButtonRef.current?.focus() onKeyDown={handleBtnKeyDown}
break onBlur={closeOnBlur}
case 'ArrowRight': ref={themesButtonRef}
if (!themesMenuOpen) { className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
toggleThemesMenu()
}
}
}
const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (
event,
) => {
quickSettingsKeyDownHandler(
closeQuickSettingsMenu,
event,
quickSettingsMenuRef,
themesMenuOpen,
)
}
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => {
themesMenuKeyDownHandler(event, themesMenuRef, setThemesMenuOpen, themesButtonRef)
}
const toggleDefaultTheme = () => {
const activeTheme = themes
.map((item) => item.component)
.find((theme) => theme?.active && !theme.isLayerable())
if (activeTheme) {
application.mutator.toggleTheme(activeTheme).catch(console.error)
}
}
return (
<div ref={mainRef} className="sn-component">
<div
className={`sn-quick-settings-menu absolute ${MENU_CLASSNAME} ${
shouldAnimateCloseMenu ? 'slide-up-animation' : 'sn-dropdown--animated'
}`}
ref={quickSettingsMenuRef}
onKeyDown={handleQuickSettingsKeyDown}
>
<div className="px-3 mt-1 mb-2 font-semibold color-text uppercase">Quick Settings</div>
<Disclosure open={themesMenuOpen} onChange={toggleThemesMenu}>
<DisclosureButton
onKeyDown={handleBtnKeyDown}
onBlur={closeOnBlur}
ref={themesButtonRef}
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="themes" className="color-neutral mr-2" />
Themes
</div>
<Icon type="chevron-right" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
onBlur={closeOnBlur}
ref={themesMenuRef}
onKeyDown={handlePanelKeyDown}
style={{
...themesMenuPosition,
}}
className={`${MENU_CLASSNAME} fixed sn-dropdown--animated`}
>
<div className="px-3 my-1 font-semibold color-text uppercase">Themes</div>
<button
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
onClick={toggleDefaultTheme}
onBlur={closeOnBlur}
ref={defaultThemeButtonRef}
>
<div
className={`pseudo-radio-btn ${
defaultThemeOn ? 'pseudo-radio-btn--checked' : ''
} mr-2`}
></div>
Default
</button>
{themes.map((theme) => (
<ThemesMenuButton
item={theme}
application={application}
key={theme.component?.uuid ?? theme.identifier}
onBlur={closeOnBlur}
/>
))}
</DisclosurePanel>
</Disclosure>
{toggleableComponents.map((component) => (
<button
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
toggleComponent(component)
}}
>
<div className="flex items-center">
<Icon type="window" className="color-neutral mr-2" />
{component.name}
</div>
<Switch checked={component.active} className="px-0" />
</button>
))}
<FocusModeSwitch
application={application}
onToggle={setFocusModeEnabled}
onClose={closeQuickSettingsMenu}
isEnabled={focusModeEnabled}
/>
<div className="h-1px my-2 bg-border"></div>
<button
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
onClick={openPreferences}
ref={prefsButtonRef}
> >
<Icon type="more" className="color-neutral mr-2" /> <div className="flex items-center">
Open Preferences <Icon type="themes" className="color-neutral mr-2" />
Themes
</div>
<Icon type="chevron-right" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
onBlur={closeOnBlur}
ref={themesMenuRef}
onKeyDown={handlePanelKeyDown}
style={{
...themesMenuPosition,
}}
className={`${MENU_CLASSNAME} fixed sn-dropdown--animated`}
>
<div className="px-3 my-1 font-semibold color-text uppercase">Themes</div>
<button
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
onClick={toggleDefaultTheme}
onBlur={closeOnBlur}
ref={defaultThemeButtonRef}
>
<div className={`pseudo-radio-btn ${defaultThemeOn ? 'pseudo-radio-btn--checked' : ''} mr-2`}></div>
Default
</button>
{themes.map((theme) => (
<ThemesMenuButton
item={theme}
application={application}
key={theme.component?.uuid ?? theme.identifier}
onBlur={closeOnBlur}
/>
))}
</DisclosurePanel>
</Disclosure>
{toggleableComponents.map((component) => (
<button
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
toggleComponent(component)
}}
>
<div className="flex items-center">
<Icon type="window" className="color-neutral mr-2" />
{component.name}
</div>
<Switch checked={component.active} className="px-0" />
</button> </button>
</div> ))}
<FocusModeSwitch
application={application}
onToggle={setFocusModeEnabled}
onClose={closeQuickSettingsMenu}
isEnabled={focusModeEnabled}
/>
<div className="h-1px my-2 bg-border"></div>
<button
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
onClick={openPreferences}
ref={prefsButtonRef}
>
<Icon type="more" className="color-neutral mr-2" />
Open Preferences
</button>
</div> </div>
) </div>
}, )
) })

View File

@@ -1,12 +1,5 @@
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { import { Action, ActionVerb, HistoryEntry, NoteHistoryEntry, RevisionListEntry, SNNote } from '@standardnotes/snjs'
Action,
ActionVerb,
HistoryEntry,
NoteHistoryEntry,
RevisionListEntry,
SNNote,
} from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { StateUpdater, useCallback, useState } from 'preact/hooks' import { StateUpdater, useCallback, useState } from 'preact/hooks'
@@ -55,26 +48,19 @@ export const HistoryListContainer: FunctionComponent<Props> = observer(
const fetchLegacyHistory = async () => { const fetchLegacyHistory = async () => {
const actionExtensions = application.actionsManager.getExtensions() const actionExtensions = application.actionsManager.getExtensions()
actionExtensions.forEach(async (ext) => { actionExtensions.forEach(async (ext) => {
const actionExtension = await application.actionsManager.loadExtensionInContextOfItem( const actionExtension = await application.actionsManager.loadExtensionInContextOfItem(ext, note)
ext,
note,
)
if (!actionExtension) { if (!actionExtension) {
return return
} }
const isLegacyNoteHistoryExt = actionExtension?.actions.some( const isLegacyNoteHistoryExt = actionExtension?.actions.some((action) => action.verb === ActionVerb.Nested)
(action) => action.verb === ActionVerb.Nested,
)
if (!isLegacyNoteHistoryExt) { if (!isLegacyNoteHistoryExt) {
return return
} }
const legacyHistoryEntries = actionExtension.actions.filter( const legacyHistoryEntries = actionExtension.actions.filter((action) => action.subactions?.[0])
(action) => action.subactions?.[0],
)
setLegacyHistory(legacyHistoryEntries) setLegacyHistory(legacyHistoryEntries)
}) })
@@ -114,10 +100,7 @@ export const HistoryListContainer: FunctionComponent<Props> = observer(
throw new Error('Could not find revision action url') throw new Error('Could not find revision action url')
} }
const response = await application.actionsManager.runAction( const response = await application.actionsManager.runAction(revisionListEntry.subactions[0], note)
revisionListEntry.subactions[0],
note,
)
if (!response) { if (!response) {
throw new Error('Could not fetch revision') throw new Error('Could not fetch revision')
@@ -131,13 +114,7 @@ export const HistoryListContainer: FunctionComponent<Props> = observer(
setIsFetchingSelectedRevision(false) setIsFetchingSelectedRevision(false)
} }
}, },
[ [application.actionsManager, note, setIsFetchingSelectedRevision, setSelectedRemoteEntry, setSelectedRevision],
application.actionsManager,
note,
setIsFetchingSelectedRevision,
setSelectedRemoteEntry,
setSelectedRevision,
],
) )
const fetchAndSetRemoteRevision = useCallback( const fetchAndSetRemoteRevision = useCallback(
@@ -150,10 +127,7 @@ export const HistoryListContainer: FunctionComponent<Props> = observer(
setSelectedRemoteEntry(undefined) setSelectedRemoteEntry(undefined)
try { try {
const remoteRevision = await application.historyManager.fetchRemoteRevision( const remoteRevision = await application.historyManager.fetchRemoteRevision(note, revisionListEntry)
note,
revisionListEntry,
)
setSelectedRevision(remoteRevision) setSelectedRevision(remoteRevision)
setSelectedRemoteEntry(revisionListEntry) setSelectedRemoteEntry(revisionListEntry)
} catch (err) { } catch (err) {
@@ -177,17 +151,11 @@ export const HistoryListContainer: FunctionComponent<Props> = observer(
) )
return ( return (
<div <div className={'flex flex-col min-w-60 border-0 border-r-1px border-solid border-main overflow-auto h-full'}>
className={
'flex flex-col min-w-60 border-0 border-r-1px border-solid border-main overflow-auto h-full'
}
>
<div className="flex border-0 border-b-1 border-solid border-main"> <div className="flex border-0 border-b-1 border-solid border-main">
<TabButton type={RevisionListTabType.Remote} /> <TabButton type={RevisionListTabType.Remote} />
<TabButton type={RevisionListTabType.Session} /> <TabButton type={RevisionListTabType.Session} />
{legacyHistory && legacyHistory.length > 0 && ( {legacyHistory && legacyHistory.length > 0 && <TabButton type={RevisionListTabType.Legacy} />}
<TabButton type={RevisionListTabType.Legacy} />
)}
</div> </div>
<div className={'min-h-0 overflow-auto py-1.5 h-full'}> <div className={'min-h-0 overflow-auto py-1.5 h-full'}>
{selectedTab === RevisionListTabType.Session && ( {selectedTab === RevisionListTabType.Session && (

View File

@@ -6,23 +6,15 @@ type HistoryListItemProps = {
onClick: () => void onClick: () => void
} }
export const HistoryListItem: FunctionComponent<HistoryListItemProps> = ({ export const HistoryListItem: FunctionComponent<HistoryListItemProps> = ({ children, isSelected, onClick }) => {
children,
isSelected,
onClick,
}) => {
return ( return (
<button <button
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
className={`sn-dropdown-item py-2.5 focus:bg-contrast focus:shadow-none ${ className={`sn-dropdown-item py-2.5 focus:bg-contrast focus:shadow-none ${isSelected ? 'bg-info-backdrop' : ''}`}
isSelected ? 'bg-info-backdrop' : ''
}`}
onClick={onClick} onClick={onClick}
data-selected={isSelected} data-selected={isSelected}
> >
<div <div className={`pseudo-radio-btn ${isSelected ? 'pseudo-radio-btn--checked' : ''} mr-2`}></div>
className={`pseudo-radio-btn ${isSelected ? 'pseudo-radio-btn--checked' : ''} mr-2`}
></div>
{children} {children}
</button> </button>
) )

View File

@@ -68,9 +68,7 @@ export const LegacyHistoryList: FunctionComponent<Props> = ({
</HistoryListItem> </HistoryListItem>
) )
})} })}
{!legacyHistory?.length && ( {!legacyHistory?.length && <div className="color-grey-0 select-none">No legacy history found</div>}
<div className="color-grey-0 select-none">No legacy history found</div>
)}
</div> </div>
) )
} }

View File

@@ -43,13 +43,7 @@ export const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = obse
if (firstEntry && !selectedEntryUuid.length) { if (firstEntry && !selectedEntryUuid.length) {
selectFirstEntry() selectFirstEntry()
} }
}, [ }, [fetchAndSetRemoteRevision, firstEntry, remoteHistory, selectFirstEntry, selectedEntryUuid.length])
fetchAndSetRemoteRevision,
firstEntry,
remoteHistory,
selectFirstEntry,
selectedEntryUuid.length,
])
return ( return (
<div <div
@@ -76,9 +70,7 @@ export const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = obse
> >
<div className="flex flex-grow items-center justify-between"> <div className="flex flex-grow items-center justify-between">
<div>{previewHistoryEntryTitle(entry)}</div> <div>{previewHistoryEntryTitle(entry)}</div>
{!application.features.hasMinimumRole(entry.required_role) && ( {!application.features.hasMinimumRole(entry.required_role) && <Icon type="premium-feature" />}
<Icon type="premium-feature" />
)}
</div> </div>
</HistoryListItem> </HistoryListItem>
))} ))}

View File

@@ -22,8 +22,7 @@ const getPremiumContentCopy = (planName: string | undefined) => {
export const RevisionContentLocked: FunctionComponent<{ export const RevisionContentLocked: FunctionComponent<{
appState: AppState appState: AppState
}> = observer(({ appState }) => { }> = observer(({ appState }) => {
const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } = const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } = appState.subscription
appState.subscription
return ( return (
<div className="flex w-full h-full items-center justify-center"> <div className="flex w-full h-full items-center justify-center">

View File

@@ -46,13 +46,9 @@ const RevisionContentPlaceholder: FunctionComponent<RevisionContentPlaceholderPr
: '-z-index-1' : '-z-index-1'
}`} }`}
> >
{isFetchingSelectedRevision && ( {isFetchingSelectedRevision && <div className={`sk-spinner w-5 h-5 spinner-info ${ABSOLUTE_CENTER_CLASSNAME}`} />}
<div className={`sk-spinner w-5 h-5 spinner-info ${ABSOLUTE_CENTER_CLASSNAME}`} />
)}
{!isFetchingSelectedRevision && !selectedRevision ? ( {!isFetchingSelectedRevision && !selectedRevision ? (
<div className={`color-grey-0 select-none ${ABSOLUTE_CENTER_CLASSNAME}`}> <div className={`color-grey-0 select-none ${ABSOLUTE_CENTER_CLASSNAME}`}>No revision selected</div>
No revision selected
</div>
) : null} ) : null}
</div> </div>
) )
@@ -87,8 +83,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
try { try {
const initialRemoteHistory = await application.historyManager.remoteHistoryForItem(note) const initialRemoteHistory = await application.historyManager.remoteHistoryForItem(note)
const remoteHistoryAsGroups = const remoteHistoryAsGroups = sortRevisionListIntoGroups<RevisionListEntry>(initialRemoteHistory)
sortRevisionListIntoGroups<RevisionListEntry>(initialRemoteHistory)
setRemoteHistory(remoteHistoryAsGroups) setRemoteHistory(remoteHistoryAsGroups)
} catch (err) { } catch (err) {
@@ -240,9 +235,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
isFetchingSelectedRevision={isFetchingSelectedRevision} isFetchingSelectedRevision={isFetchingSelectedRevision}
showContentLockedScreen={showContentLockedScreen} showContentLockedScreen={showContentLockedScreen}
/> />
{showContentLockedScreen && !selectedRevision && ( {showContentLockedScreen && !selectedRevision && <RevisionContentLocked appState={appState} />}
<RevisionContentLocked appState={appState} />
)}
{selectedRevision && templateNoteForRevision && ( {selectedRevision && templateNoteForRevision && (
<SelectedRevisionContent <SelectedRevisionContent
application={application} application={application}
@@ -267,11 +260,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
{selectedRevision && ( {selectedRevision && (
<div class="flex items-center"> <div class="flex items-center">
{selectedRemoteEntry && ( {selectedRemoteEntry && (
<Button <Button className="py-1.35 mr-2.5" onClick={deleteSelectedRevision} variant="normal">
className="py-1.35 mr-2.5"
onClick={deleteSelectedRevision}
variant="normal"
>
{isDeletingRevision ? ( {isDeletingRevision ? (
<div className="sk-spinner my-1 w-3 h-3 spinner-info" /> <div className="sk-spinner my-1 w-3 h-3 spinner-info" />
) : ( ) : (
@@ -285,12 +274,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
onClick={restoreAsCopy} onClick={restoreAsCopy}
variant="normal" variant="normal"
/> />
<Button <Button className="py-1.35" label="Restore version" onClick={restore} variant="primary" />
className="py-1.35"
label="Restore version"
onClick={restore}
variant="primary"
/>
</div> </div>
)} )}
</div> </div>

View File

@@ -24,8 +24,7 @@ export const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentP
return undefined return undefined
} }
const componentViewer = const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote)
application.componentManager.createComponentViewer(editorForCurrentNote)
componentViewer.setReadonly(true) componentViewer.setReadonly(true)
componentViewer.lockReadonly = true componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision componentViewer.overrideContextItem = templateNoteForRevision

View File

@@ -75,9 +75,7 @@ export const SessionHistoryList: FunctionComponent<Props> = ({
</Fragment> </Fragment>
) : null), ) : null),
)} )}
{!sessionHistoryLength && ( {!sessionHistoryLength && <div className="color-grey-0 select-none">No session history found</div>}
<div className="color-grey-0 select-none">No session history found</div>
)}
</div> </div>
) )
} }

View File

@@ -25,9 +25,7 @@ export const formatDateAsMonthYearString = (date: Date) =>
export const getGroupIndexForEntry = (entry: RevisionEntry, groups: ListGroup<RevisionEntry>[]) => { export const getGroupIndexForEntry = (entry: RevisionEntry, groups: ListGroup<RevisionEntry>[]) => {
const todayAsDate = new Date() const todayAsDate = new Date()
const entryDate = new Date( const entryDate = new Date((entry as RevisionListEntry).created_at ?? (entry as NoteHistoryEntry).payload.created_at)
(entry as RevisionListEntry).created_at ?? (entry as NoteHistoryEntry).payload.created_at,
)
const differenceBetweenDatesInDays = calculateDifferenceBetweenDatesInDays(todayAsDate, entryDate) const differenceBetweenDatesInDays = calculateDifferenceBetweenDatesInDays(todayAsDate, entryDate)
@@ -52,9 +50,7 @@ const GROUP_TITLE_TODAY = 'Today'
const GROUP_TITLE_WEEK = 'This Week' const GROUP_TITLE_WEEK = 'This Week'
const GROUP_TITLE_YEAR = 'More Than A Year Ago' const GROUP_TITLE_YEAR = 'More Than A Year Ago'
export const sortRevisionListIntoGroups = <EntryType extends RevisionEntry>( export const sortRevisionListIntoGroups = <EntryType extends RevisionEntry>(revisionList: EntryType[] | undefined) => {
revisionList: EntryType[] | undefined,
) => {
const sortedGroups: ListGroup<EntryType>[] = [ const sortedGroups: ListGroup<EntryType>[] = [
{ {
title: GROUP_TITLE_TODAY, title: GROUP_TITLE_TODAY,
@@ -82,10 +78,7 @@ export const sortRevisionListIntoGroups = <EntryType extends RevisionEntry>(
} else { } else {
addBeforeLastGroup({ addBeforeLastGroup({
title: formatDateAsMonthYearString( title: formatDateAsMonthYearString(
new Date( new Date((entry as RevisionListEntry).created_at ?? (entry as NoteHistoryEntry).payload.created_at),
(entry as RevisionListEntry).created_at ??
(entry as NoteHistoryEntry).payload.created_at,
),
), ),
entries: [entry], entries: [entry],
}) })

View File

@@ -18,28 +18,16 @@ export const SearchOptions = observer(({ appState }: Props) => {
} }
return ( return (
<div <div role="tablist" className="search-options justify-center" onMouseDown={(e) => e.preventDefault()}>
role="tablist"
className="search-options justify-center"
onMouseDown={(e) => e.preventDefault()}
>
<Bubble <Bubble
label="Protected Contents" label="Protected Contents"
selected={includeProtectedContents} selected={includeProtectedContents}
onSelect={toggleIncludeProtectedContents} onSelect={toggleIncludeProtectedContents}
/> />
<Bubble <Bubble label="Archived" selected={includeArchived} onSelect={searchOptions.toggleIncludeArchived} />
label="Archived"
selected={includeArchived}
onSelect={searchOptions.toggleIncludeArchived}
/>
<Bubble <Bubble label="Trashed" selected={includeTrashed} onSelect={searchOptions.toggleIncludeTrashed} />
label="Trashed"
selected={includeTrashed}
onSelect={searchOptions.toggleIncludeTrashed}
/>
</div> </div>
) )
}) })

View File

@@ -1,11 +1,5 @@
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { import { SNApplication, SessionStrings, UuidString, isNullOrUndefined, RemoteSession } from '@standardnotes/snjs'
SNApplication,
SessionStrings,
UuidString,
isNullOrUndefined,
RemoteSession,
} from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useState, useEffect, useRef, useMemo } from 'preact/hooks' import { useState, useEffect, useRef, useMemo } from 'preact/hooks'
import { Dialog } from '@reach/dialog' import { Dialog } from '@reach/dialog'

View File

@@ -28,11 +28,7 @@ export const AccordionItem: FunctionalComponent<Props> = ({ title, className = '
data-is-expanded={isExpanded} data-is-expanded={isExpanded}
/> />
</div> </div>
<div <div className={'accordion-contents-container cursor-auto'} data-is-expanded={isExpanded} ref={elementRef}>
className={'accordion-contents-container cursor-auto'}
data-is-expanded={isExpanded}
ref={elementRef}
>
{children} {children}
</div> </div>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More