Merge branch 'release/10.17.0'
This commit is contained in:
@@ -8,7 +8,6 @@ RAILS_LOG_LEVEL=INFO
|
|||||||
|
|
||||||
RAILS_SERVE_STATIC_FILES=true
|
RAILS_SERVE_STATIC_FILES=true
|
||||||
SECRET_KEY_BASE=test
|
SECRET_KEY_BASE=test
|
||||||
BUGSNAG_API_KEY=
|
|
||||||
|
|
||||||
APP_HOST=http://localhost:3001
|
APP_HOST=http://localhost:3001
|
||||||
PURCHASE_URL=https://standardnotes.com/purchase
|
PURCHASE_URL=https://standardnotes.com/purchase
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
# npx pretty-quick --staged # trying lint-staged for now, it is slower but uses eslint.
|
|
||||||
npx lint-staged
|
npx lint-staged
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ WORKDIR /app/
|
|||||||
|
|
||||||
COPY package.json yarn.lock Gemfile Gemfile.lock /app/
|
COPY package.json yarn.lock Gemfile Gemfile.lock /app/
|
||||||
|
|
||||||
COPY vendor /app/vendor
|
|
||||||
|
|
||||||
RUN yarn install --pure-lockfile
|
RUN yarn install --pure-lockfile
|
||||||
|
|
||||||
RUN gem install bundler && bundle install
|
RUN gem install bundler && bundle install
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
bugsnagApiKey?: string;
|
|
||||||
dashboardUrl?: string;
|
dashboardUrl?: string;
|
||||||
defaultSyncServer: string;
|
defaultSyncServer: string;
|
||||||
devAccountEmail?: string;
|
devAccountEmail?: string;
|
||||||
@@ -22,7 +21,6 @@ import { render } from 'preact';
|
|||||||
import { ApplicationGroupView } from './components/ApplicationGroupView';
|
import { ApplicationGroupView } from './components/ApplicationGroupView';
|
||||||
import { Bridge } from './services/bridge';
|
import { Bridge } from './services/bridge';
|
||||||
import { BrowserBridge } from './services/browserBridge';
|
import { BrowserBridge } from './services/browserBridge';
|
||||||
import { startErrorReporting } from './services/errorReporting';
|
|
||||||
import { StartApplication } from './startApplication';
|
import { StartApplication } from './startApplication';
|
||||||
import { ApplicationGroup } from './ui_models/application_group';
|
import { ApplicationGroup } from './ui_models/application_group';
|
||||||
import { isDev } from './utils';
|
import { isDev } from './utils';
|
||||||
@@ -34,7 +32,7 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
webSocketUrl: string
|
webSocketUrl: string
|
||||||
) {
|
) {
|
||||||
SNLog.onLog = console.log;
|
SNLog.onLog = console.log;
|
||||||
startErrorReporting();
|
SNLog.onError = console.error;
|
||||||
|
|
||||||
const mainApplicationGroup = new ApplicationGroup(
|
const mainApplicationGroup = new ApplicationGroup(
|
||||||
defaultSyncServerHost,
|
defaultSyncServerHost,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export abstract class PureComponent<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAppStateEvent(eventName: any, data: any) {
|
onAppStateEvent(_eventName: any, _data: any) {
|
||||||
/** Optional override */
|
/** Optional override */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { WebApplication } from '@/ui_models/application';
|
|||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
import { Checkbox } from '../Checkbox';
|
import { Checkbox } from '../Checkbox';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { InputWithIcon } from '../InputWithIcon';
|
import { InputWithIcon } from '../InputWithIcon';
|
||||||
@@ -11,14 +11,70 @@ type Props = {
|
|||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onVaultChange?: (isVault: boolean, vaultedEmail?: string) => void;
|
||||||
|
onStrictSignInChange?: (isStrictSignIn: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdvancedOptions: FunctionComponent<Props> = observer(
|
export const AdvancedOptions: FunctionComponent<Props> = observer(
|
||||||
({ appState, application, disabled = false, children }) => {
|
({
|
||||||
|
appState,
|
||||||
|
application,
|
||||||
|
disabled = false,
|
||||||
|
onVaultChange,
|
||||||
|
onStrictSignInChange,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
const { server, setServer, enableServerOption, setEnableServerOption } =
|
const { server, setServer, enableServerOption, setEnableServerOption } =
|
||||||
appState.accountMenu;
|
appState.accountMenu;
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
|
|
||||||
|
const [isVault, setIsVault] = useState(false);
|
||||||
|
const [vaultName, setVaultName] = useState('');
|
||||||
|
const [vaultUserphrase, setVaultUserphrase] = useState('');
|
||||||
|
|
||||||
|
const [isStrictSignin, setIsStrictSignin] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const recomputeVaultedEmail = async () => {
|
||||||
|
const vaultedEmail = await application.vaultToEmail(
|
||||||
|
vaultName,
|
||||||
|
vaultUserphrase
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!vaultedEmail) {
|
||||||
|
if (vaultName?.length > 0 && vaultUserphrase?.length > 0) {
|
||||||
|
application.alertService.alert('Unable to compute vault name.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onVaultChange?.(true, vaultedEmail);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (vaultName && vaultUserphrase) {
|
||||||
|
recomputeVaultedEmail();
|
||||||
|
}
|
||||||
|
}, [vaultName, vaultUserphrase, application, onVaultChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onVaultChange?.(isVault);
|
||||||
|
}, [isVault, onVaultChange]);
|
||||||
|
|
||||||
|
const handleIsVaultChange = () => {
|
||||||
|
setIsVault(!isVault);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVaultNameChange = (e: Event) => {
|
||||||
|
if (e.target instanceof HTMLInputElement) {
|
||||||
|
setVaultName(e.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVaultUserphraseChange = (e: Event) => {
|
||||||
|
if (e.target instanceof HTMLInputElement) {
|
||||||
|
setVaultUserphrase(e.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleServerOptionChange = (e: Event) => {
|
const handleServerOptionChange = (e: Event) => {
|
||||||
if (e.target instanceof HTMLInputElement) {
|
if (e.target instanceof HTMLInputElement) {
|
||||||
setEnableServerOption(e.target.checked);
|
setEnableServerOption(e.target.checked);
|
||||||
@@ -32,6 +88,12 @@ export const AdvancedOptions: FunctionComponent<Props> = observer(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStrictSigninChange = () => {
|
||||||
|
const newValue = !isStrictSignin;
|
||||||
|
setIsStrictSignin(newValue);
|
||||||
|
onStrictSignInChange?.(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleShowAdvanced = () => {
|
const toggleShowAdvanced = () => {
|
||||||
setShowAdvanced(!showAdvanced);
|
setShowAdvanced(!showAdvanced);
|
||||||
};
|
};
|
||||||
@@ -50,6 +112,70 @@ export const AdvancedOptions: FunctionComponent<Props> = observer(
|
|||||||
{showAdvanced ? (
|
{showAdvanced ? (
|
||||||
<div className="px-3 my-2">
|
<div className="px-3 my-2">
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
{appState.enableUnfinishedFeatures && (
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<Checkbox
|
||||||
|
name="vault-mode"
|
||||||
|
label="Vault Mode"
|
||||||
|
checked={isVault}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleIsVaultChange}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href="https://standardnotes.com/help/80"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="Learn more"
|
||||||
|
>
|
||||||
|
<Icon type="info" className="color-neutral" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{appState.enableUnfinishedFeatures && isVault && (
|
||||||
|
<>
|
||||||
|
<InputWithIcon
|
||||||
|
className={`mb-2`}
|
||||||
|
icon="folder"
|
||||||
|
inputType="text"
|
||||||
|
placeholder="Vault name"
|
||||||
|
value={vaultName}
|
||||||
|
onChange={handleVaultNameChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<InputWithIcon
|
||||||
|
className={`mb-2 `}
|
||||||
|
icon="server"
|
||||||
|
inputType={'text'}
|
||||||
|
placeholder="Vault userphrase"
|
||||||
|
value={vaultUserphrase}
|
||||||
|
onChange={handleVaultUserphraseChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onStrictSignInChange && (
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<Checkbox
|
||||||
|
name="use-strict-signin"
|
||||||
|
label="Use strict sign-in"
|
||||||
|
checked={isStrictSignin}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleStrictSigninChange}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href="https://standardnotes.com/help/security"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="Learn more"
|
||||||
|
>
|
||||||
|
<Icon type="info" className="color-neutral" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="custom-sync-server"
|
name="custom-sync-server"
|
||||||
label="Custom sync server"
|
label="Custom sync server"
|
||||||
|
|||||||
@@ -1,403 +0,0 @@
|
|||||||
import { confirmDialog } from '@Services/alertService';
|
|
||||||
import {
|
|
||||||
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
|
||||||
STRING_GENERATING_LOGIN_KEYS,
|
|
||||||
STRING_GENERATING_REGISTER_KEYS,
|
|
||||||
STRING_NON_MATCHING_PASSWORDS,
|
|
||||||
} from '@/strings';
|
|
||||||
import { JSXInternal } from 'preact/src/jsx';
|
|
||||||
import TargetedEvent = JSXInternal.TargetedEvent;
|
|
||||||
import TargetedKeyboardEvent = JSXInternal.TargetedKeyboardEvent;
|
|
||||||
import { WebApplication } from '@/ui_models/application';
|
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
||||||
import TargetedMouseEvent = JSXInternal.TargetedMouseEvent;
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { AppState } from '@/ui_models/app_state';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
application: WebApplication;
|
|
||||||
appState: AppState;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Authentication = observer(({ application, appState }: Props) => {
|
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
||||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [passwordConfirmation, setPasswordConfirmation] = useState<string>('');
|
|
||||||
const [status, setStatus] = useState<string | undefined>(undefined);
|
|
||||||
const [isEmailFocused, setIsEmailFocused] = useState(false);
|
|
||||||
|
|
||||||
const [isEphemeral, setIsEphemeral] = useState(false);
|
|
||||||
const [isStrictSignIn, setIsStrictSignIn] = useState(false);
|
|
||||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
|
|
||||||
|
|
||||||
const {
|
|
||||||
server,
|
|
||||||
notesAndTagsCount,
|
|
||||||
showSignIn,
|
|
||||||
showRegister,
|
|
||||||
setShowSignIn,
|
|
||||||
setShowRegister,
|
|
||||||
setServer,
|
|
||||||
closeAccountMenu,
|
|
||||||
} = appState.accountMenu;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEmailFocused) {
|
|
||||||
emailInputRef.current!.focus();
|
|
||||||
setIsEmailFocused(false);
|
|
||||||
}
|
|
||||||
}, [isEmailFocused]);
|
|
||||||
|
|
||||||
// Reset password and confirmation fields when hiding the form
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showSignIn && !showRegister) {
|
|
||||||
setPassword('');
|
|
||||||
setPasswordConfirmation('');
|
|
||||||
}
|
|
||||||
}, [showSignIn, showRegister]);
|
|
||||||
|
|
||||||
const handleHostInputChange = (event: TargetedEvent<HTMLInputElement>) => {
|
|
||||||
const { value } = event.target as HTMLInputElement;
|
|
||||||
setServer(value);
|
|
||||||
application.setCustomHost(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const emailInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const passwordInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const passwordConfirmationInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleSignInClick = () => {
|
|
||||||
setShowSignIn(true);
|
|
||||||
setIsEmailFocused(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRegisterClick = () => {
|
|
||||||
setShowRegister(true);
|
|
||||||
setIsEmailFocused(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const blurAuthFields = () => {
|
|
||||||
emailInputRef.current!.blur();
|
|
||||||
passwordInputRef.current!.blur();
|
|
||||||
passwordConfirmationInputRef.current?.blur();
|
|
||||||
};
|
|
||||||
|
|
||||||
const signin = async () => {
|
|
||||||
setStatus(STRING_GENERATING_LOGIN_KEYS);
|
|
||||||
setIsAuthenticating(true);
|
|
||||||
|
|
||||||
const response = await application.signIn(
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
isStrictSignIn,
|
|
||||||
isEphemeral,
|
|
||||||
shouldMergeLocal
|
|
||||||
);
|
|
||||||
const error = response.error;
|
|
||||||
if (!error) {
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
setPassword('');
|
|
||||||
setShowSignIn(false);
|
|
||||||
|
|
||||||
closeAccountMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowSignIn(true);
|
|
||||||
setStatus(undefined);
|
|
||||||
setPassword('');
|
|
||||||
|
|
||||||
if (error.message) {
|
|
||||||
await application.alertService.alert(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const register = async () => {
|
|
||||||
if (passwordConfirmation !== password) {
|
|
||||||
application.alertService.alert(STRING_NON_MATCHING_PASSWORDS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setStatus(STRING_GENERATING_REGISTER_KEYS);
|
|
||||||
setIsAuthenticating(true);
|
|
||||||
|
|
||||||
const response = await application.register(
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
isEphemeral,
|
|
||||||
shouldMergeLocal
|
|
||||||
);
|
|
||||||
|
|
||||||
const error = response.error;
|
|
||||||
if (error) {
|
|
||||||
setStatus(undefined);
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
|
|
||||||
application.alertService.alert(error.message);
|
|
||||||
} else {
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
setShowRegister(false);
|
|
||||||
closeAccountMenu();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAuthFormSubmit = (
|
|
||||||
event:
|
|
||||||
| TargetedEvent<HTMLFormElement>
|
|
||||||
| TargetedMouseEvent<HTMLButtonElement>
|
|
||||||
| TargetedKeyboardEvent<HTMLButtonElement>
|
|
||||||
) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (!email || !password) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
blurAuthFields();
|
|
||||||
|
|
||||||
if (showSignIn) {
|
|
||||||
signin();
|
|
||||||
} else {
|
|
||||||
register();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPressKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
handleAuthFormSubmit(event as TargetedKeyboardEvent<HTMLButtonElement>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasswordChange = (event: TargetedEvent<HTMLInputElement>) => {
|
|
||||||
const { value } = event.target as HTMLInputElement;
|
|
||||||
setPassword(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEmailChange = (event: TargetedEvent<HTMLInputElement>) => {
|
|
||||||
const { value } = event.target as HTMLInputElement;
|
|
||||||
setEmail(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasswordConfirmationChange = (
|
|
||||||
event: TargetedEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
const { value } = event.target as HTMLInputElement;
|
|
||||||
setPasswordConfirmation(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMergeLocalData = async (
|
|
||||||
event: TargetedEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
const { checked } = event.target as HTMLInputElement;
|
|
||||||
|
|
||||||
setShouldMergeLocal(checked);
|
|
||||||
if (!checked) {
|
|
||||||
const confirmResult = await confirmDialog({
|
|
||||||
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
|
||||||
confirmButtonStyle: 'danger',
|
|
||||||
});
|
|
||||||
setShouldMergeLocal(!confirmResult);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!application.hasAccount() && !showSignIn && !showRegister && (
|
|
||||||
<div className="sk-panel-section sk-panel-hero">
|
|
||||||
<div className="sk-panel-row">
|
|
||||||
<div className="sk-h1">
|
|
||||||
Sign in or register to enable sync and end-to-end encryption.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex my-1">
|
|
||||||
<button
|
|
||||||
className="sn-button info flex-grow text-base py-3 mr-1.5"
|
|
||||||
onClick={handleSignInClick}
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="sn-button info flex-grow text-base py-3 ml-1.5"
|
|
||||||
onClick={handleRegisterClick}
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="sk-panel-row sk-p">
|
|
||||||
Standard Notes is free on every platform, and comes standard with
|
|
||||||
sync and encryption.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(showSignIn || showRegister) && (
|
|
||||||
<div className="sk-panel-section">
|
|
||||||
<div className="sk-panel-section-title">
|
|
||||||
{showSignIn ? 'Sign In' : 'Register'}
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
className="sk-panel-form"
|
|
||||||
onSubmit={handleAuthFormSubmit}
|
|
||||||
noValidate
|
|
||||||
>
|
|
||||||
<div className="sk-panel-section">
|
|
||||||
<input
|
|
||||||
className="sk-input contrast"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={handleEmailChange}
|
|
||||||
placeholder="Email"
|
|
||||||
required
|
|
||||||
spellcheck={false}
|
|
||||||
ref={emailInputRef}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="sk-input contrast"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={handlePasswordChange}
|
|
||||||
placeholder="Password"
|
|
||||||
required
|
|
||||||
onKeyPress={handleKeyPressKeyDown}
|
|
||||||
onKeyDown={handleKeyPressKeyDown}
|
|
||||||
ref={passwordInputRef}
|
|
||||||
/>
|
|
||||||
{showRegister && (
|
|
||||||
<input
|
|
||||||
className="sk-input contrast"
|
|
||||||
name="password_conf"
|
|
||||||
type="password"
|
|
||||||
placeholder="Confirm Password"
|
|
||||||
required
|
|
||||||
onKeyPress={handleKeyPressKeyDown}
|
|
||||||
onKeyDown={handleKeyPressKeyDown}
|
|
||||||
value={passwordConfirmation}
|
|
||||||
onChange={handlePasswordConfirmationChange}
|
|
||||||
ref={passwordConfirmationInputRef!}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="sk-panel-row" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sk-a info font-bold text-left p-0 cursor-pointer hover:underline mr-1 ml-1"
|
|
||||||
onClick={() => {
|
|
||||||
setShowAdvanced(!showAdvanced);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Advanced Options
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{showAdvanced && (
|
|
||||||
<div className="sk-notification unpadded contrast advanced-options sk-panel-row">
|
|
||||||
<div className="sk-panel-column stretch">
|
|
||||||
<div className="sk-notification-title sk-panel-row padded-row">
|
|
||||||
Advanced Options
|
|
||||||
</div>
|
|
||||||
<div className="bordered-row padded-row">
|
|
||||||
<label className="sk-label">Sync Server Domain</label>
|
|
||||||
<input
|
|
||||||
className="sk-input sk-base"
|
|
||||||
name="server"
|
|
||||||
placeholder="Server URL"
|
|
||||||
onChange={handleHostInputChange}
|
|
||||||
value={server}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{showSignIn && (
|
|
||||||
<label className="sk-label padded-row sk-panel-row justify-left">
|
|
||||||
<div className="sk-horizontal-group tight cursor-pointer">
|
|
||||||
<input
|
|
||||||
className="sk-input"
|
|
||||||
type="checkbox"
|
|
||||||
checked={isStrictSignIn}
|
|
||||||
onChange={() =>
|
|
||||||
setIsStrictSignIn((prevState) => !prevState)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<p className="sk-p">Use strict sign in</p>
|
|
||||||
<span>
|
|
||||||
<a
|
|
||||||
className="info"
|
|
||||||
href="https://standardnotes.com/help/security"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
(Learn more)
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isAuthenticating && (
|
|
||||||
<div className="sk-panel-section form-submit">
|
|
||||||
<button
|
|
||||||
className="sn-button info text-base py-3 text-center"
|
|
||||||
type="submit"
|
|
||||||
disabled={isAuthenticating}
|
|
||||||
>
|
|
||||||
{showSignIn ? 'Sign In' : 'Register'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showRegister && (
|
|
||||||
<div className="sk-notification neutral">
|
|
||||||
<div className="sk-notification-title">No Password Reset.</div>
|
|
||||||
<div className="sk-notification-text">
|
|
||||||
Because your notes are encrypted using your password, Standard
|
|
||||||
Notes does not have a password reset option. You cannot forget
|
|
||||||
your password.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{status && (
|
|
||||||
<div className="sk-panel-section no-bottom-pad">
|
|
||||||
<div className="sk-horizontal-group">
|
|
||||||
<div className="sk-spinner small neutral" />
|
|
||||||
<div className="sk-label">{status}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isAuthenticating && (
|
|
||||||
<div className="sk-panel-section no-bottom-pad">
|
|
||||||
<label className="sk-panel-row justify-left">
|
|
||||||
<div className="sk-horizontal-group tight cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={!isEphemeral}
|
|
||||||
onChange={() => setIsEphemeral((prevState) => !prevState)}
|
|
||||||
/>
|
|
||||||
<p className="sk-p">Stay signed in</p>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
{notesAndTagsCount > 0 && (
|
|
||||||
<label className="sk-panel-row justify-left">
|
|
||||||
<div className="sk-horizontal-group tight cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={shouldMergeLocal}
|
|
||||||
onChange={handleMergeLocalData}
|
|
||||||
/>
|
|
||||||
<p className="sk-p">
|
|
||||||
Merge local data ({notesAndTagsCount}) notes and tags
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Authentication;
|
|
||||||
@@ -162,12 +162,6 @@ export const ConfirmPassword: FunctionComponent<Props> = observer(
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</form>
|
</form>
|
||||||
<div className="h-1px my-2 bg-border"></div>
|
|
||||||
<AdvancedOptions
|
|
||||||
appState={appState}
|
|
||||||
application={application}
|
|
||||||
disabled={isRegistering}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const CreateAccount: FunctionComponent<Props> = observer(
|
|||||||
|
|
||||||
const emailInputRef = useRef<HTMLInputElement>(null);
|
const emailInputRef = useRef<HTMLInputElement>(null);
|
||||||
const passwordInputRef = useRef<HTMLInputElement>(null);
|
const passwordInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isVault, setIsVault] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emailInputRef.current) {
|
if (emailInputRef.current) {
|
||||||
@@ -82,6 +83,13 @@ export const CreateAccount: FunctionComponent<Props> = observer(
|
|||||||
setPassword('');
|
setPassword('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onVaultChange = (isVault: boolean, vaultedEmail?: string) => {
|
||||||
|
setIsVault(isVault);
|
||||||
|
if (isVault && vaultedEmail) {
|
||||||
|
setEmail(vaultedEmail);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center px-3 mt-1 mb-3">
|
<div className="flex items-center px-3 mt-1 mb-3">
|
||||||
@@ -101,6 +109,7 @@ export const CreateAccount: FunctionComponent<Props> = observer(
|
|||||||
inputType="email"
|
inputType="email"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
value={email}
|
value={email}
|
||||||
|
disabled={isVault}
|
||||||
onChange={handleEmailChange}
|
onChange={handleEmailChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
ref={emailInputRef}
|
ref={emailInputRef}
|
||||||
@@ -130,7 +139,11 @@ export const CreateAccount: FunctionComponent<Props> = observer(
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<div className="h-1px my-2 bg-border"></div>
|
<div className="h-1px my-2 bg-border"></div>
|
||||||
<AdvancedOptions application={application} appState={appState} />
|
<AdvancedOptions
|
||||||
|
application={application}
|
||||||
|
appState={appState}
|
||||||
|
onVaultChange={onVaultChange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ import { WebApplication } from '@/ui_models/application';
|
|||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { formatLastSyncDate } from '@/preferences/panes/account/Sync';
|
import { formatLastSyncDate } from '@/components/Preferences/panes/account/Sync';
|
||||||
import { SyncQueueStrategy } from '@standardnotes/snjs';
|
import { SyncQueueStrategy } from '@standardnotes/snjs';
|
||||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { AccountMenuPane } from '.';
|
import { AccountMenuPane } from '.';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { Menu } from '../menu/Menu';
|
import { Menu } from '../Menu/Menu';
|
||||||
import { MenuItem, MenuItemSeparator, MenuItemType } from '../menu/MenuItem';
|
import { MenuItem, MenuItemSeparator, MenuItemType } from '../Menu/MenuItem';
|
||||||
|
import { WorkspaceSwitcherOption } from './WorkspaceSwitcher/WorkspaceSwitcherOption';
|
||||||
|
import { ApplicationGroup } from '@/ui_models/application_group';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
|
mainApplicationGroup: ApplicationGroup;
|
||||||
setMenuPane: (pane: AccountMenuPane) => void;
|
setMenuPane: (pane: AccountMenuPane) => void;
|
||||||
closeMenu: () => void;
|
closeMenu: () => void;
|
||||||
};
|
};
|
||||||
@@ -21,7 +24,7 @@ type Props = {
|
|||||||
const iconClassName = 'color-neutral mr-2';
|
const iconClassName = 'color-neutral mr-2';
|
||||||
|
|
||||||
export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
||||||
({ application, appState, setMenuPane, closeMenu }) => {
|
({ 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)
|
||||||
@@ -54,9 +57,12 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
|||||||
|
|
||||||
const user = application.getUser();
|
const user = application.getUser();
|
||||||
|
|
||||||
|
const CREATE_ACCOUNT_INDEX = 1;
|
||||||
|
const SWITCHER_INDEX = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between px-3 mt-1 mb-3">
|
<div className="flex items-center justify-between px-3 mt-1 mb-1">
|
||||||
<div className="sn-account-menu-headline">Account</div>
|
<div className="sn-account-menu-headline">Account</div>
|
||||||
<div className="flex cursor-pointer" onClick={closeMenu}>
|
<div className="flex cursor-pointer" onClick={closeMenu}>
|
||||||
<Icon type="close" className="color-neutral" />
|
<Icon type="close" className="color-neutral" />
|
||||||
@@ -66,10 +72,10 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
|||||||
<>
|
<>
|
||||||
<div className="px-3 mb-3 color-foreground text-sm">
|
<div className="px-3 mb-3 color-foreground text-sm">
|
||||||
<div>You're signed in as:</div>
|
<div>You're signed in as:</div>
|
||||||
<div className="my-0.5 font-bold">{user.email}</div>
|
<div className="my-0.5 font-bold wrap">{user.email}</div>
|
||||||
<span className="color-neutral">{application.getHost()}</span>
|
<span className="color-neutral">{application.getHost()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start justify-between px-3 mb-2">
|
<div className="flex items-start justify-between px-3 mb-3">
|
||||||
{isSyncingInProgress ? (
|
{isSyncingInProgress ? (
|
||||||
<div className="flex items-center color-info font-semibold">
|
<div className="flex items-center color-info font-semibold">
|
||||||
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
|
<div className="sk-spinner w-5 h-5 mr-2 spinner-info"></div>
|
||||||
@@ -106,12 +112,20 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="h-1px my-2 bg-border"></div>
|
|
||||||
<Menu
|
<Menu
|
||||||
isOpen={appState.accountMenu.show}
|
isOpen={appState.accountMenu.show}
|
||||||
a11yLabel="General account menu"
|
a11yLabel="General account menu"
|
||||||
closeMenu={closeMenu}
|
closeMenu={closeMenu}
|
||||||
|
initialFocus={
|
||||||
|
!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
<WorkspaceSwitcherOption
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
<MenuItemSeparator />
|
||||||
{user ? (
|
{user ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
type={MenuItemType.IconButton}
|
type={MenuItemType.IconButton}
|
||||||
@@ -171,7 +185,7 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="signOut" className={iconClassName} />
|
<Icon type="signOut" className={iconClassName} />
|
||||||
Sign out and clear local data
|
Sign out workspace
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AppState } from '@/ui_models/app_state';
|
|||||||
import { isDev } from '@/utils';
|
import { isDev } from '@/utils';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { AccountMenuPane } from '.';
|
import { AccountMenuPane } from '.';
|
||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import { Checkbox } from '../Checkbox';
|
import { Checkbox } from '../Checkbox';
|
||||||
@@ -25,10 +25,12 @@ export const SignInPane: FunctionComponent<Props> = observer(
|
|||||||
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 [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
|
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
|
||||||
|
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);
|
||||||
@@ -106,6 +108,16 @@ export const SignInPane: FunctionComponent<Props> = observer(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onVaultChange = useCallback(
|
||||||
|
(newIsVault: boolean, vaultedEmail?: string) => {
|
||||||
|
setIsVault(newIsVault);
|
||||||
|
if (newIsVault && vaultedEmail) {
|
||||||
|
setEmail(vaultedEmail);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setEmail]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSignInFormSubmit = (e: Event) => {
|
const handleSignInFormSubmit = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -145,7 +157,7 @@ export const SignInPane: FunctionComponent<Props> = observer(
|
|||||||
onChange={handleEmailChange}
|
onChange={handleEmailChange}
|
||||||
onFocus={resetInvalid}
|
onFocus={resetInvalid}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
disabled={isSigningIn}
|
disabled={isSigningIn || isVault}
|
||||||
ref={emailInputRef}
|
ref={emailInputRef}
|
||||||
/>
|
/>
|
||||||
<InputWithIcon
|
<InputWithIcon
|
||||||
@@ -197,25 +209,9 @@ export const SignInPane: FunctionComponent<Props> = observer(
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
application={application}
|
application={application}
|
||||||
disabled={isSigningIn}
|
disabled={isSigningIn}
|
||||||
>
|
onVaultChange={onVaultChange}
|
||||||
<div className="flex justify-between items-center mb-1">
|
onStrictSignInChange={handleStrictSigninChange}
|
||||||
<Checkbox
|
/>
|
||||||
name="use-strict-signin"
|
|
||||||
label="Use strict sign-in"
|
|
||||||
checked={isStrictSignin}
|
|
||||||
disabled={isSigningIn}
|
|
||||||
onChange={handleStrictSigninChange}
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href="https://standardnotes.com/help/security"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
title="Learn more"
|
|
||||||
>
|
|
||||||
<Icon type="info" className="color-neutral" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</AdvancedOptions>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Icon } from '@/components/Icon';
|
||||||
|
import { MenuItem, MenuItemType } from '@/components/Menu/MenuItem';
|
||||||
|
import { KeyboardKey } from '@/services/ioService';
|
||||||
|
import { ApplicationDescriptor } from '@standardnotes/snjs/dist/@types';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
descriptor: ApplicationDescriptor;
|
||||||
|
onClick: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
renameDescriptor: (label: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceMenuItem: FunctionComponent<Props> = ({
|
||||||
|
descriptor,
|
||||||
|
onClick,
|
||||||
|
onDelete,
|
||||||
|
renameDescriptor,
|
||||||
|
}) => {
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRenaming) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isRenaming]);
|
||||||
|
|
||||||
|
const handleInputKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === KeyboardKey.Enter) {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = (event: FocusEvent) => {
|
||||||
|
const name = (event.target as HTMLInputElement).value;
|
||||||
|
renameDescriptor(name);
|
||||||
|
setIsRenaming(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
type={MenuItemType.RadioButton}
|
||||||
|
className="sn-dropdown-item py-2 focus:bg-info-backdrop focus:shadow-none"
|
||||||
|
onClick={onClick}
|
||||||
|
checked={descriptor.primary}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
{isRenaming ? (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={descriptor.label}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>{descriptor.label}</div>
|
||||||
|
)}
|
||||||
|
{descriptor.primary && (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="w-5 h-5 p-0 mr-3 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setIsRenaming((isRenaming) => !isRenaming);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="pencil" className="sn-icon--mid color-neutral" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-5 h-5 p-0 border-0 bg-transparent hover:bg-contrast cursor-pointer"
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<Icon type="trash" className="sn-icon--mid color-danger" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { ApplicationGroup } from '@/ui_models/application_group';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { ApplicationDescriptor } from '@standardnotes/snjs';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
import { Icon } from '../../Icon';
|
||||||
|
import { Menu } from '../../Menu/Menu';
|
||||||
|
import { MenuItem, MenuItemSeparator, MenuItemType } from '../../Menu/MenuItem';
|
||||||
|
import { WorkspaceMenuItem } from './WorkspaceMenuItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
mainApplicationGroup: ApplicationGroup;
|
||||||
|
appState: AppState;
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
|
||||||
|
({ mainApplicationGroup, appState, isOpen }) => {
|
||||||
|
const [applicationDescriptors, setApplicationDescriptors] = useState<
|
||||||
|
ApplicationDescriptor[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const removeAppGroupObserver =
|
||||||
|
mainApplicationGroup.addApplicationChangeObserver(() => {
|
||||||
|
const applicationDescriptors = mainApplicationGroup.getDescriptors();
|
||||||
|
setApplicationDescriptors(applicationDescriptors);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeAppGroupObserver();
|
||||||
|
};
|
||||||
|
}, [mainApplicationGroup]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
a11yLabel="Workspace switcher menu"
|
||||||
|
className="px-0 focus:shadow-none"
|
||||||
|
isOpen={isOpen}
|
||||||
|
>
|
||||||
|
{applicationDescriptors.map((descriptor) => (
|
||||||
|
<WorkspaceMenuItem
|
||||||
|
descriptor={descriptor}
|
||||||
|
onDelete={() => {
|
||||||
|
appState.accountMenu.setSigningOut(true);
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
mainApplicationGroup.loadApplicationForDescriptor(descriptor);
|
||||||
|
}}
|
||||||
|
renameDescriptor={(label: string) =>
|
||||||
|
mainApplicationGroup.renameDescriptor(descriptor, label)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<MenuItemSeparator />
|
||||||
|
<MenuItem
|
||||||
|
type={MenuItemType.IconButton}
|
||||||
|
onClick={() => {
|
||||||
|
mainApplicationGroup.addNewApplication();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="user-add" className="color-neutral mr-2" />
|
||||||
|
Add another workspace
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants';
|
||||||
|
import { ApplicationGroup } from '@/ui_models/application_group';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import {
|
||||||
|
calculateSubmenuStyle,
|
||||||
|
SubmenuStyle,
|
||||||
|
} from '@/utils/calculateSubmenuStyle';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { Icon } from '../../Icon';
|
||||||
|
import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
mainApplicationGroup: ApplicationGroup;
|
||||||
|
appState: AppState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(
|
||||||
|
({ mainApplicationGroup, appState }) => {
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>();
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
if (!isOpen) {
|
||||||
|
const menuPosition = calculateSubmenuStyle(buttonRef.current);
|
||||||
|
if (menuPosition) {
|
||||||
|
setMenuStyle(menuPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const newMenuPosition = calculateSubmenuStyle(
|
||||||
|
buttonRef.current,
|
||||||
|
menuRef.current
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newMenuPosition) {
|
||||||
|
setMenuStyle(newMenuPosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
className="sn-dropdown-item justify-between focus:bg-info-backdrop focus:shadow-none"
|
||||||
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
|
role="menuitem"
|
||||||
|
onClick={toggleMenu}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Icon type="user-switch" className="color-neutral mr-2" />
|
||||||
|
Switch workspace
|
||||||
|
</div>
|
||||||
|
<Icon type="chevron-right" className="color-neutral" />
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className="sn-dropdown max-h-120 min-w-68 py-2 fixed overflow-y-auto"
|
||||||
|
style={menuStyle}
|
||||||
|
>
|
||||||
|
<WorkspaceSwitcherMenu
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
|
appState={appState}
|
||||||
|
isOpen={isOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -9,6 +9,7 @@ import { SignInPane } from './SignIn';
|
|||||||
import { CreateAccount } from './CreateAccount';
|
import { CreateAccount } from './CreateAccount';
|
||||||
import { ConfirmPassword } from './ConfirmPassword';
|
import { ConfirmPassword } from './ConfirmPassword';
|
||||||
import { JSXInternal } from 'preact/src/jsx';
|
import { JSXInternal } from 'preact/src/jsx';
|
||||||
|
import { ApplicationGroup } from '@/ui_models/application_group';
|
||||||
|
|
||||||
export enum AccountMenuPane {
|
export enum AccountMenuPane {
|
||||||
GeneralMenu,
|
GeneralMenu,
|
||||||
@@ -21,18 +22,27 @@ type Props = {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
onClickOutside: () => void;
|
onClickOutside: () => void;
|
||||||
|
mainApplicationGroup: ApplicationGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaneSelectorProps = {
|
type PaneSelectorProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
|
mainApplicationGroup: ApplicationGroup;
|
||||||
menuPane: AccountMenuPane;
|
menuPane: AccountMenuPane;
|
||||||
setMenuPane: (pane: AccountMenuPane) => void;
|
setMenuPane: (pane: AccountMenuPane) => void;
|
||||||
closeMenu: () => void;
|
closeMenu: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
||||||
({ application, appState, menuPane, setMenuPane, closeMenu }) => {
|
({
|
||||||
|
application,
|
||||||
|
appState,
|
||||||
|
menuPane,
|
||||||
|
setMenuPane,
|
||||||
|
closeMenu,
|
||||||
|
mainApplicationGroup,
|
||||||
|
}) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
@@ -42,6 +52,7 @@ const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
|||||||
<GeneralAccountMenu
|
<GeneralAccountMenu
|
||||||
appState={appState}
|
appState={appState}
|
||||||
application={application}
|
application={application}
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
setMenuPane={setMenuPane}
|
setMenuPane={setMenuPane}
|
||||||
closeMenu={closeMenu}
|
closeMenu={closeMenu}
|
||||||
/>
|
/>
|
||||||
@@ -81,7 +92,7 @@ const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const AccountMenu: FunctionComponent<Props> = observer(
|
export const AccountMenu: FunctionComponent<Props> = observer(
|
||||||
({ application, appState, onClickOutside }) => {
|
({ application, appState, onClickOutside, mainApplicationGroup }) => {
|
||||||
const {
|
const {
|
||||||
currentPane,
|
currentPane,
|
||||||
setCurrentPane,
|
setCurrentPane,
|
||||||
@@ -123,6 +134,7 @@ export const AccountMenu: FunctionComponent<Props> = observer(
|
|||||||
<MenuPaneSelector
|
<MenuPaneSelector
|
||||||
appState={appState}
|
appState={appState}
|
||||||
application={application}
|
application={application}
|
||||||
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
menuPane={currentPane}
|
menuPane={currentPane}
|
||||||
setMenuPane={setCurrentPane}
|
setMenuPane={setCurrentPane}
|
||||||
closeMenu={closeAccountMenu}
|
closeMenu={closeAccountMenu}
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import { ApplicationGroup } from '@/ui_models/application_group';
|
|
||||||
import { WebApplication } from '@/ui_models/application';
|
|
||||||
import { ApplicationDescriptor } from '@standardnotes/snjs';
|
|
||||||
import { PureComponent } from '@/components/Abstract/PureComponent';
|
|
||||||
import { JSX } from 'preact';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
application: WebApplication;
|
|
||||||
mainApplicationGroup: ApplicationGroup;
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
descriptors: ApplicationDescriptor[];
|
|
||||||
editingDescriptor?: ApplicationDescriptor;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AccountSwitcher extends PureComponent<Props, State> {
|
|
||||||
private removeAppGroupObserver: any;
|
|
||||||
activeApplication!: WebApplication;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props, props.application);
|
|
||||||
this.removeAppGroupObserver =
|
|
||||||
props.mainApplicationGroup.addApplicationChangeObserver(() => {
|
|
||||||
this.activeApplication = props.mainApplicationGroup
|
|
||||||
.primaryApplication as WebApplication;
|
|
||||||
this.reloadApplications();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadApplications() {
|
|
||||||
this.setState({
|
|
||||||
descriptors: this.props.mainApplicationGroup.getDescriptors(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addNewApplication = () => {
|
|
||||||
this.dismiss();
|
|
||||||
this.props.mainApplicationGroup.addNewApplication();
|
|
||||||
};
|
|
||||||
|
|
||||||
selectDescriptor = (descriptor: ApplicationDescriptor) => {
|
|
||||||
this.dismiss();
|
|
||||||
this.props.mainApplicationGroup.loadApplicationForDescriptor(descriptor);
|
|
||||||
};
|
|
||||||
|
|
||||||
inputForDescriptor(descriptor: ApplicationDescriptor) {
|
|
||||||
return document.getElementById(`input-${descriptor.identifier}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
renameDescriptor = (event: Event, descriptor: ApplicationDescriptor) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
this.setState({ editingDescriptor: descriptor });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.inputForDescriptor(descriptor)?.focus();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
submitRename = () => {
|
|
||||||
this.props.mainApplicationGroup.renameDescriptor(
|
|
||||||
this.state.editingDescriptor!,
|
|
||||||
this.state.editingDescriptor!.label
|
|
||||||
);
|
|
||||||
this.setState({ editingDescriptor: undefined });
|
|
||||||
};
|
|
||||||
|
|
||||||
deinit() {
|
|
||||||
super.deinit();
|
|
||||||
this.removeAppGroupObserver();
|
|
||||||
this.removeAppGroupObserver = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
onDescriptorInputChange = (
|
|
||||||
descriptor: ApplicationDescriptor,
|
|
||||||
{ currentTarget }: JSX.TargetedEvent<HTMLInputElement, Event>
|
|
||||||
) => {
|
|
||||||
descriptor.label = currentTarget.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
dismiss = () => {
|
|
||||||
this.dismissModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="sk-modal">
|
|
||||||
<div onClick={this.dismiss} className="sk-modal-background" />
|
|
||||||
<div id="account-switcher" className="sk-modal-content">
|
|
||||||
<div className="sn-component">
|
|
||||||
<div id="menu-panel" className="sk-menu-panel">
|
|
||||||
<div className="sk-menu-panel-header">
|
|
||||||
<div className="sk-menu-panel-column">
|
|
||||||
<div className="sk-menu-panel-header-title">
|
|
||||||
Account Switcher
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="sk-menu-panel-column">
|
|
||||||
<a onClick={this.addNewApplication} className="sk-label info">
|
|
||||||
Add Account
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.state.descriptors.map((descriptor) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={descriptor.identifier}
|
|
||||||
onClick={() => this.selectDescriptor(descriptor)}
|
|
||||||
className="sk-menu-panel-row"
|
|
||||||
>
|
|
||||||
<div className="sk-menu-panel-column stretch">
|
|
||||||
<div className="left">
|
|
||||||
{descriptor.identifier ==
|
|
||||||
this.activeApplication.identifier && (
|
|
||||||
<div className="sk-menu-panel-column">
|
|
||||||
<div className="sk-circle small success" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="sk-menu-panel-column stretch">
|
|
||||||
<input
|
|
||||||
value={descriptor.label}
|
|
||||||
disabled={
|
|
||||||
descriptor !== this.state.editingDescriptor
|
|
||||||
}
|
|
||||||
onChange={(event) =>
|
|
||||||
this.onDescriptorInputChange(descriptor, event)
|
|
||||||
}
|
|
||||||
onKeyUp={(event) =>
|
|
||||||
event.keyCode == 13 && this.submitRename()
|
|
||||||
}
|
|
||||||
id={`input-${descriptor.identifier}`}
|
|
||||||
spellcheck={false}
|
|
||||||
className="sk-label clickable"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{descriptor.identifier ==
|
|
||||||
this.activeApplication.identifier && (
|
|
||||||
<div className="sk-sublabel">
|
|
||||||
Current Application
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{descriptor.identifier ==
|
|
||||||
this.activeApplication.identifier && (
|
|
||||||
<div className="sk-menu-panel-column">
|
|
||||||
<button
|
|
||||||
onClick={(event) =>
|
|
||||||
this.renameDescriptor(event, descriptor)
|
|
||||||
}
|
|
||||||
className="sn-button success"
|
|
||||||
>
|
|
||||||
Rename
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApplicationGroup } from '@/ui_models/application_group';
|
import { ApplicationGroup } from '@/ui_models/application_group';
|
||||||
import { getPlatformString } from '@/utils';
|
import { getPlatformString, getWindowUrlParams } from '@/utils';
|
||||||
import { AppStateEvent, PanelResizedData } from '@/ui_models/app_state';
|
import { AppStateEvent, PanelResizedData } from '@/ui_models/app_state';
|
||||||
import {
|
import {
|
||||||
ApplicationEvent,
|
ApplicationEvent,
|
||||||
@@ -16,10 +16,10 @@ import { NotesView } from '@/components/NotesView';
|
|||||||
import { NoteGroupView } from '@/components/NoteGroupView';
|
import { NoteGroupView } from '@/components/NoteGroupView';
|
||||||
import { Footer } from '@/components/Footer';
|
import { Footer } from '@/components/Footer';
|
||||||
import { SessionsModal } from '@/components/SessionsModal';
|
import { SessionsModal } from '@/components/SessionsModal';
|
||||||
import { PreferencesViewWrapper } from '@/preferences/PreferencesViewWrapper';
|
import { PreferencesViewWrapper } from '@/components/Preferences/PreferencesViewWrapper';
|
||||||
import { ChallengeModal } from '@/components/ChallengeModal';
|
import { ChallengeModal } from '@/components/ChallengeModal';
|
||||||
import { NotesContextMenu } from '@/components/NotesContextMenu';
|
import { NotesContextMenu } from '@/components/NotesContextMenu';
|
||||||
import { PurchaseFlowWrapper } from '@/purchaseFlow/PurchaseFlowWrapper';
|
import { PurchaseFlowWrapper } from '@/components/PurchaseFlow/PurchaseFlowWrapper';
|
||||||
import { render } from 'preact';
|
import { render } from 'preact';
|
||||||
import { PermissionsModal } from './PermissionsModal';
|
import { PermissionsModal } from './PermissionsModal';
|
||||||
import { RevisionHistoryModalWrapper } from './RevisionHistoryModal/RevisionHistoryModalWrapper';
|
import { RevisionHistoryModalWrapper } from './RevisionHistoryModal/RevisionHistoryModalWrapper';
|
||||||
@@ -27,6 +27,7 @@ import { PremiumModalProvider } from './Premium';
|
|||||||
import { ConfirmSignoutContainer } from './ConfirmSignoutModal';
|
import { ConfirmSignoutContainer } from './ConfirmSignoutModal';
|
||||||
import { TagsContextMenu } from './Tags/TagContextMenu';
|
import { TagsContextMenu } from './Tags/TagContextMenu';
|
||||||
import { ToastContainer } from '@standardnotes/stylekit';
|
import { ToastContainer } from '@standardnotes/stylekit';
|
||||||
|
import { FilePreviewModalProvider } from './Files/FilePreviewModalProvider';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
@@ -143,15 +144,12 @@ export class ApplicationView extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDemoSignInFromParams() {
|
async handleDemoSignInFromParams() {
|
||||||
if (
|
const token = getWindowUrlParams().get('demo-token');
|
||||||
window.location.href.includes('demo') &&
|
if (!token || this.application.hasAccount()) {
|
||||||
!this.application.hasAccount()
|
return;
|
||||||
) {
|
|
||||||
await this.application.setCustomHost(
|
|
||||||
'https://syncing-server-demo.standardnotes.com'
|
|
||||||
);
|
|
||||||
this.application.signIn('demo@standardnotes.org', 'password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.application.sessions.populateSessionFromDemoShareToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
presentPermissionsDialog = (dialog: PermissionDialog) => {
|
presentPermissionsDialog = (dialog: PermissionDialog) => {
|
||||||
@@ -175,88 +173,78 @@ export class ApplicationView extends PureComponent<Props, State> {
|
|||||||
const renderAppContents = !this.state.needsUnlock && this.state.launched;
|
const renderAppContents = !this.state.needsUnlock && this.state.launched;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PremiumModalProvider
|
<FilePreviewModalProvider application={this.application}>
|
||||||
application={this.application}
|
<PremiumModalProvider
|
||||||
appState={this.appState}
|
application={this.application}
|
||||||
>
|
appState={this.appState}
|
||||||
<div className={this.platformString + ' main-ui-view sn-component'}>
|
>
|
||||||
{renderAppContents && (
|
<div className={this.platformString + ' main-ui-view sn-component'}>
|
||||||
<div
|
{renderAppContents && (
|
||||||
id="app"
|
<div
|
||||||
className={this.state.appClass + ' app app-column-container'}
|
id="app"
|
||||||
>
|
className={this.state.appClass + ' app app-column-container'}
|
||||||
<Navigation application={this.application} />
|
>
|
||||||
|
<Navigation application={this.application} />
|
||||||
<NotesView
|
<NotesView
|
||||||
application={this.application}
|
|
||||||
appState={this.appState}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NoteGroupView application={this.application} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{renderAppContents && (
|
|
||||||
<>
|
|
||||||
<Footer
|
|
||||||
application={this.application}
|
|
||||||
applicationGroup={this.props.mainApplicationGroup}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SessionsModal
|
|
||||||
application={this.application}
|
|
||||||
appState={this.appState}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PreferencesViewWrapper
|
|
||||||
appState={this.appState}
|
|
||||||
application={this.application}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RevisionHistoryModalWrapper
|
|
||||||
application={this.application}
|
|
||||||
appState={this.appState}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.challenges.map((challenge) => {
|
|
||||||
return (
|
|
||||||
<div className="sk-modal">
|
|
||||||
<ChallengeModal
|
|
||||||
key={challenge.id}
|
|
||||||
application={this.application}
|
application={this.application}
|
||||||
challenge={challenge}
|
appState={this.appState}
|
||||||
onDismiss={this.removeChallenge}
|
|
||||||
/>
|
/>
|
||||||
|
<NoteGroupView application={this.application} />
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
{renderAppContents && (
|
||||||
|
<>
|
||||||
{renderAppContents && (
|
<Footer
|
||||||
<>
|
application={this.application}
|
||||||
<NotesContextMenu
|
applicationGroup={this.props.mainApplicationGroup}
|
||||||
application={this.application}
|
/>
|
||||||
appState={this.appState}
|
<SessionsModal
|
||||||
/>
|
application={this.application}
|
||||||
|
appState={this.appState}
|
||||||
<TagsContextMenu appState={this.appState} />
|
/>
|
||||||
|
<PreferencesViewWrapper
|
||||||
<PurchaseFlowWrapper
|
appState={this.appState}
|
||||||
application={this.application}
|
application={this.application}
|
||||||
appState={this.appState}
|
/>
|
||||||
/>
|
<RevisionHistoryModalWrapper
|
||||||
|
application={this.application}
|
||||||
<ConfirmSignoutContainer
|
appState={this.appState}
|
||||||
appState={this.appState}
|
/>
|
||||||
application={this.application}
|
</>
|
||||||
/>
|
)}
|
||||||
|
{this.state.challenges.map((challenge) => {
|
||||||
<ToastContainer />
|
return (
|
||||||
</>
|
<div className="sk-modal">
|
||||||
)}
|
<ChallengeModal
|
||||||
</div>
|
key={challenge.id}
|
||||||
</PremiumModalProvider>
|
application={this.application}
|
||||||
|
challenge={challenge}
|
||||||
|
onDismiss={this.removeChallenge}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{renderAppContents && (
|
||||||
|
<>
|
||||||
|
<NotesContextMenu
|
||||||
|
application={this.application}
|
||||||
|
appState={this.appState}
|
||||||
|
/>
|
||||||
|
<TagsContextMenu appState={this.appState} />
|
||||||
|
<PurchaseFlowWrapper
|
||||||
|
application={this.application}
|
||||||
|
appState={this.appState}
|
||||||
|
/>
|
||||||
|
<ConfirmSignoutContainer
|
||||||
|
appState={this.appState}
|
||||||
|
application={this.application}
|
||||||
|
/>
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PremiumModalProvider>
|
||||||
|
</FilePreviewModalProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,23 @@ 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';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { useCloseOnClickOutside } from '../utils';
|
import { useCloseOnBlur } from '../utils';
|
||||||
import { ChallengeReason, ContentType, SNFile } from '@standardnotes/snjs';
|
import {
|
||||||
|
ChallengeReason,
|
||||||
|
ContentType,
|
||||||
|
FeatureIdentifier,
|
||||||
|
FeatureStatus,
|
||||||
|
SNFile,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
import { confirmDialog } from '@/services/alertService';
|
import { confirmDialog } from '@/services/alertService';
|
||||||
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit';
|
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit';
|
||||||
import { parseFileName } from '@standardnotes/filepicker';
|
import { StreamingFileReader } from '@standardnotes/filepicker';
|
||||||
import {
|
import {
|
||||||
PopoverFileItemAction,
|
PopoverFileItemAction,
|
||||||
PopoverFileItemActionType,
|
PopoverFileItemActionType,
|
||||||
} from './PopoverFileItemAction';
|
} from './PopoverFileItemAction';
|
||||||
import { PopoverDragNDropWrapper } from './PopoverDragNDropWrapper';
|
import { AttachedFilesPopover, PopoverTabs } from './AttachedFilesPopover';
|
||||||
|
import { usePremiumModal } from '../Premium/usePremiumModal';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
@@ -28,8 +35,26 @@ type Props = {
|
|||||||
onClickPreprocessing?: () => Promise<void>;
|
onClickPreprocessing?: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createDragOverlay = () => {
|
||||||
|
if (document.getElementById('drag-overlay')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayElementTemplate =
|
||||||
|
'<div class="sn-component" id="drag-overlay"><div class="absolute top-0 left-0 w-full h-full z-index-1001"></div></div>';
|
||||||
|
const overlayFragment = document
|
||||||
|
.createRange()
|
||||||
|
.createContextualFragment(overlayElementTemplate);
|
||||||
|
document.body.appendChild(overlayFragment);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDragOverlay = () => {
|
||||||
|
document.getElementById('drag-overlay')?.remove();
|
||||||
|
};
|
||||||
|
|
||||||
export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
||||||
({ application, appState, onClickPreprocessing }) => {
|
({ application, appState, onClickPreprocessing }) => {
|
||||||
|
const premiumModal = usePremiumModal();
|
||||||
const note = Object.values(appState.notes.selectedNotes)[0];
|
const note = Object.values(appState.notes.selectedNotes)[0];
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -41,9 +66,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
useCloseOnClickOutside(containerRef, () => {
|
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen);
|
||||||
setOpen(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [attachedFilesCount, setAttachedFilesCount] = useState(
|
const [attachedFilesCount, setAttachedFilesCount] = useState(
|
||||||
note ? application.items.getFilesForNote(note).length : 0
|
note ? application.items.getFilesForNote(note).length : 0
|
||||||
@@ -68,7 +91,15 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
};
|
};
|
||||||
}, [application, reloadAttachedFilesCount]);
|
}, [application, reloadAttachedFilesCount]);
|
||||||
|
|
||||||
const toggleAttachedFilesMenu = async () => {
|
const toggleAttachedFilesMenu = useCallback(async () => {
|
||||||
|
if (
|
||||||
|
application.features.getFeatureStatus(FeatureIdentifier.Files) !==
|
||||||
|
FeatureStatus.Entitled
|
||||||
|
) {
|
||||||
|
premiumModal.activate('Files');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rect = buttonRef.current?.getBoundingClientRect();
|
const rect = buttonRef.current?.getBoundingClientRect();
|
||||||
if (rect) {
|
if (rect) {
|
||||||
const { clientHeight } = document.documentElement;
|
const { clientHeight } = document.documentElement;
|
||||||
@@ -98,22 +129,22 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
|
|
||||||
setOpen(newOpenState);
|
setOpen(newOpenState);
|
||||||
}
|
}
|
||||||
};
|
}, [application.features, onClickPreprocessing, open, premiumModal]);
|
||||||
|
|
||||||
const deleteFile = async (file: SNFile) => {
|
const deleteFile = async (file: SNFile) => {
|
||||||
const shouldDelete = await confirmDialog({
|
const shouldDelete = await confirmDialog({
|
||||||
text: `Are you sure you want to permanently delete "${file.nameWithExt}"?`,
|
text: `Are you sure you want to permanently delete "${file.name}"?`,
|
||||||
confirmButtonStyle: 'danger',
|
confirmButtonStyle: 'danger',
|
||||||
});
|
});
|
||||||
if (shouldDelete) {
|
if (shouldDelete) {
|
||||||
const deletingToastId = addToast({
|
const deletingToastId = addToast({
|
||||||
type: ToastType.Loading,
|
type: ToastType.Loading,
|
||||||
message: `Deleting file "${file.nameWithExt}"...`,
|
message: `Deleting file "${file.name}"...`,
|
||||||
});
|
});
|
||||||
await application.deleteItem(file);
|
await application.files.deleteFile(file);
|
||||||
addToast({
|
addToast({
|
||||||
type: ToastType.Success,
|
type: ToastType.Success,
|
||||||
message: `Deleted file "${file.nameWithExt}"`,
|
message: `Deleted file "${file.name}"`,
|
||||||
});
|
});
|
||||||
dismissToast(deletingToastId);
|
dismissToast(deletingToastId);
|
||||||
}
|
}
|
||||||
@@ -123,9 +154,12 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
appState.files.downloadFile(file);
|
appState.files.downloadFile(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachFileToNote = async (file: SNFile) => {
|
const attachFileToNote = useCallback(
|
||||||
await application.items.associateFileWithNote(file, note);
|
async (file: SNFile) => {
|
||||||
};
|
await application.items.associateFileWithNote(file, note);
|
||||||
|
},
|
||||||
|
[application.items, note]
|
||||||
|
);
|
||||||
|
|
||||||
const detachFileFromNote = async (file: SNFile) => {
|
const detachFileFromNote = async (file: SNFile) => {
|
||||||
await application.items.disassociateFileWithNote(file, note);
|
await application.items.disassociateFileWithNote(file, note);
|
||||||
@@ -134,9 +168,12 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
const toggleFileProtection = async (file: SNFile) => {
|
const toggleFileProtection = async (file: SNFile) => {
|
||||||
let result: SNFile | undefined;
|
let result: SNFile | undefined;
|
||||||
if (file.protected) {
|
if (file.protected) {
|
||||||
result = await application.protections.unprotectFile(file);
|
keepMenuOpen(true);
|
||||||
|
result = await application.mutator.unprotectFile(file);
|
||||||
|
keepMenuOpen(false);
|
||||||
|
buttonRef.current?.focus();
|
||||||
} else {
|
} else {
|
||||||
result = await application.protections.protectFile(file);
|
result = await application.mutator.protectFile(file);
|
||||||
}
|
}
|
||||||
const isProtected = result ? result.protected : file.protected;
|
const isProtected = result ? result.protected : file.protected;
|
||||||
return isProtected;
|
return isProtected;
|
||||||
@@ -157,8 +194,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renameFile = async (file: SNFile, fileName: string) => {
|
const renameFile = async (file: SNFile, fileName: string) => {
|
||||||
const { name, ext } = parseFileName(fileName);
|
await application.items.renameFile(file, fileName);
|
||||||
await application.items.renameFile(file, name, ext);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileAction = async (action: PopoverFileItemAction) => {
|
const handleFileAction = async (action: PopoverFileItemAction) => {
|
||||||
@@ -172,10 +208,13 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
file.protected &&
|
file.protected &&
|
||||||
action.type !== PopoverFileItemActionType.ToggleFileProtection
|
action.type !== PopoverFileItemActionType.ToggleFileProtection
|
||||||
) {
|
) {
|
||||||
|
keepMenuOpen(true);
|
||||||
isAuthorizedForAction = await authorizeProtectedActionForFile(
|
isAuthorizedForAction = await authorizeProtectedActionForFile(
|
||||||
file,
|
file,
|
||||||
ChallengeReason.AccessProtectedFile
|
ChallengeReason.AccessProtectedFile
|
||||||
);
|
);
|
||||||
|
keepMenuOpen(false);
|
||||||
|
buttonRef.current?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthorizedForAction) {
|
if (!isAuthorizedForAction) {
|
||||||
@@ -210,6 +249,102 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isDraggingFiles, setIsDraggingFiles] = useState(false);
|
||||||
|
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles);
|
||||||
|
const dragCounter = useRef(0);
|
||||||
|
|
||||||
|
const handleDrag = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragIn = useCallback(
|
||||||
|
(event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
dragCounter.current = dragCounter.current + 1;
|
||||||
|
|
||||||
|
if (event.dataTransfer?.items.length) {
|
||||||
|
setIsDraggingFiles(true);
|
||||||
|
createDragOverlay();
|
||||||
|
if (!open) {
|
||||||
|
toggleAttachedFilesMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[open, toggleAttachedFilesMenu]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragOut = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
dragCounter.current = dragCounter.current - 1;
|
||||||
|
|
||||||
|
if (dragCounter.current > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDragOverlay();
|
||||||
|
|
||||||
|
setIsDraggingFiles(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = useCallback(
|
||||||
|
(event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
setIsDraggingFiles(false);
|
||||||
|
removeDragOverlay();
|
||||||
|
|
||||||
|
if (event.dataTransfer?.items.length) {
|
||||||
|
Array.from(event.dataTransfer.items).forEach(async (item) => {
|
||||||
|
const fileOrHandle = StreamingFileReader.available()
|
||||||
|
? ((await item.getAsFileSystemHandle()) as FileSystemFileHandle)
|
||||||
|
: item.getAsFile();
|
||||||
|
|
||||||
|
if (!fileOrHandle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadedFiles = await appState.files.uploadNewFile(
|
||||||
|
fileOrHandle
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uploadedFiles) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||||
|
uploadedFiles.forEach((file) => {
|
||||||
|
attachFileToNote(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.dataTransfer.clearData();
|
||||||
|
dragCounter.current = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[appState.files, attachFileToNote, currentTab]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('dragenter', handleDragIn);
|
||||||
|
window.addEventListener('dragleave', handleDragOut);
|
||||||
|
window.addEventListener('dragover', handleDrag);
|
||||||
|
window.addEventListener('drop', handleDrop);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('dragenter', handleDragIn);
|
||||||
|
window.removeEventListener('dragleave', handleDragOut);
|
||||||
|
window.removeEventListener('dragover', handleDrag);
|
||||||
|
window.removeEventListener('drop', handleDrop);
|
||||||
|
};
|
||||||
|
}, [handleDragIn, handleDrop]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
<Disclosure open={open} onChange={toggleAttachedFilesMenu}>
|
<Disclosure open={open} onChange={toggleAttachedFilesMenu}>
|
||||||
@@ -223,6 +358,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
className={`sn-icon-button border-contrast ${
|
className={`sn-icon-button border-contrast ${
|
||||||
attachedFilesCount > 0 ? 'py-1 px-3' : ''
|
attachedFilesCount > 0 ? 'py-1 px-3' : ''
|
||||||
}`}
|
}`}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
>
|
>
|
||||||
<VisuallyHidden>Attached files</VisuallyHidden>
|
<VisuallyHidden>Attached files</VisuallyHidden>
|
||||||
<Icon type="attachment-file" className="block" />
|
<Icon type="attachment-file" className="block" />
|
||||||
@@ -243,13 +379,18 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
|||||||
maxHeight,
|
maxHeight,
|
||||||
}}
|
}}
|
||||||
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
|
className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
|
||||||
|
onBlur={closeOnBlur}
|
||||||
>
|
>
|
||||||
{open && (
|
{open && (
|
||||||
<PopoverDragNDropWrapper
|
<AttachedFilesPopover
|
||||||
application={application}
|
application={application}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
note={note}
|
note={note}
|
||||||
fileActionHandler={handleFileAction}
|
handleFileAction={handleFileAction}
|
||||||
|
currentTab={currentTab}
|
||||||
|
closeOnBlur={closeOnBlur}
|
||||||
|
setCurrentTab={setCurrentTab}
|
||||||
|
isDraggingFiles={isDraggingFiles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
import { ContentType, SNFile } from '@standardnotes/snjs';
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { ContentType, SNFile, SNNote } from '@standardnotes/snjs';
|
||||||
import { FilesIllustration } from '@standardnotes/stylekit';
|
import { FilesIllustration } from '@standardnotes/stylekit';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { StateUpdater, useCallback, useEffect, useState } from 'preact/hooks';
|
import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { PopoverTabs, PopoverWrapperProps } from './PopoverDragNDropWrapper';
|
|
||||||
import { PopoverFileItem } from './PopoverFileItem';
|
import { PopoverFileItem } from './PopoverFileItem';
|
||||||
import { PopoverFileItemActionType } from './PopoverFileItemAction';
|
import {
|
||||||
|
PopoverFileItemAction,
|
||||||
|
PopoverFileItemActionType,
|
||||||
|
} from './PopoverFileItemAction';
|
||||||
|
|
||||||
type Props = PopoverWrapperProps & {
|
export enum PopoverTabs {
|
||||||
|
AttachedFiles,
|
||||||
|
AllFiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
appState: AppState;
|
||||||
currentTab: PopoverTabs;
|
currentTab: PopoverTabs;
|
||||||
|
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||||
|
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>;
|
||||||
|
isDraggingFiles: boolean;
|
||||||
|
note: SNNote;
|
||||||
setCurrentTab: StateUpdater<PopoverTabs>;
|
setCurrentTab: StateUpdater<PopoverTabs>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,14 +34,17 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
({
|
({
|
||||||
application,
|
application,
|
||||||
appState,
|
appState,
|
||||||
note,
|
|
||||||
fileActionHandler,
|
|
||||||
currentTab,
|
currentTab,
|
||||||
|
closeOnBlur,
|
||||||
|
handleFileAction,
|
||||||
|
isDraggingFiles,
|
||||||
|
note,
|
||||||
setCurrentTab,
|
setCurrentTab,
|
||||||
}) => {
|
}) => {
|
||||||
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([]);
|
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([]);
|
||||||
const [allFiles, setAllFiles] = useState<SNFile[]>([]);
|
const [allFiles, setAllFiles] = useState<SNFile[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const filesList =
|
const filesList =
|
||||||
currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles;
|
currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles;
|
||||||
@@ -33,39 +52,35 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
const filteredList =
|
const filteredList =
|
||||||
searchQuery.length > 0
|
searchQuery.length > 0
|
||||||
? filesList.filter(
|
? filesList.filter(
|
||||||
(file) => file.nameWithExt.toLowerCase().indexOf(searchQuery) !== -1
|
(file) =>
|
||||||
|
file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1
|
||||||
)
|
)
|
||||||
: filesList;
|
: filesList;
|
||||||
|
|
||||||
const reloadAttachedFiles = useCallback(() => {
|
|
||||||
setAttachedFiles(
|
|
||||||
application.items
|
|
||||||
.getFilesForNote(note)
|
|
||||||
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
|
|
||||||
);
|
|
||||||
}, [application.items, note]);
|
|
||||||
|
|
||||||
const reloadAllFiles = useCallback(() => {
|
|
||||||
setAllFiles(
|
|
||||||
application
|
|
||||||
.getItems(ContentType.File)
|
|
||||||
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1)) as SNFile[]
|
|
||||||
);
|
|
||||||
}, [application]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unregisterFileStream = application.streamItems(
|
const unregisterFileStream = application.streamItems(
|
||||||
ContentType.File,
|
ContentType.File,
|
||||||
() => {
|
() => {
|
||||||
reloadAttachedFiles();
|
setAttachedFiles(
|
||||||
reloadAllFiles();
|
application.items
|
||||||
|
.getFilesForNote(note)
|
||||||
|
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
|
||||||
|
);
|
||||||
|
|
||||||
|
setAllFiles(
|
||||||
|
application.items
|
||||||
|
.getItems(ContentType.File)
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.created_at < b.created_at ? 1 : -1
|
||||||
|
) as SNFile[]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unregisterFileStream();
|
unregisterFileStream();
|
||||||
};
|
};
|
||||||
}, [application, reloadAllFiles, reloadAttachedFiles]);
|
}, [application, note]);
|
||||||
|
|
||||||
const handleAttachFilesClick = async () => {
|
const handleAttachFilesClick = async () => {
|
||||||
const uploadedFiles = await appState.files.uploadNewFile();
|
const uploadedFiles = await appState.files.uploadNewFile();
|
||||||
@@ -74,7 +89,7 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
}
|
}
|
||||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||||
uploadedFiles.forEach((file) => {
|
uploadedFiles.forEach((file) => {
|
||||||
fileActionHandler({
|
handleFileAction({
|
||||||
type: PopoverFileItemActionType.AttachFileToNote,
|
type: PopoverFileItemActionType.AttachFileToNote,
|
||||||
payload: file,
|
payload: file,
|
||||||
});
|
});
|
||||||
@@ -83,7 +98,15 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div
|
||||||
|
className="flex flex-col"
|
||||||
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
|
style={{
|
||||||
|
border: isDraggingFiles
|
||||||
|
? '2px dashed var(--sn-stylekit-info-color)'
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex border-0 border-b-1 border-solid border-main">
|
<div className="flex border-0 border-b-1 border-solid border-main">
|
||||||
<button
|
<button
|
||||||
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
|
className={`bg-default border-0 cursor-pointer px-3 py-2.5 relative focus:bg-info-backdrop focus:shadow-bottom ${
|
||||||
@@ -94,6 +117,7 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentTab(PopoverTabs.AttachedFiles);
|
setCurrentTab(PopoverTabs.AttachedFiles);
|
||||||
}}
|
}}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
>
|
>
|
||||||
Attached
|
Attached
|
||||||
</button>
|
</button>
|
||||||
@@ -106,6 +130,7 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentTab(PopoverTabs.AllFiles);
|
setCurrentTab(PopoverTabs.AllFiles);
|
||||||
}}
|
}}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
>
|
>
|
||||||
All files
|
All files
|
||||||
</button>
|
</button>
|
||||||
@@ -122,13 +147,17 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setSearchQuery((e.target as HTMLInputElement).value);
|
setSearchQuery((e.target as HTMLInputElement).value);
|
||||||
}}
|
}}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
ref={searchInputRef}
|
||||||
/>
|
/>
|
||||||
{searchQuery.length > 0 && (
|
{searchQuery.length > 0 && (
|
||||||
<button
|
<button
|
||||||
className="flex absolute right-2 p-0 bg-transparent border-0 top-1/2 -translate-y-1/2 cursor-pointer"
|
className="flex absolute right-2 p-0 bg-transparent border-0 top-1/2 -translate-y-1/2 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
|
searchInputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
type="clear-circle-filled"
|
type="clear-circle-filled"
|
||||||
@@ -140,25 +169,24 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{filteredList.length > 0 ? (
|
{filteredList.length > 0 ? (
|
||||||
filteredList.map((file: SNFile) => {
|
filteredList
|
||||||
return (
|
.filter((file) => !file.deleted)
|
||||||
<PopoverFileItem
|
.map((file: SNFile) => {
|
||||||
key={file.uuid}
|
return (
|
||||||
file={file}
|
<PopoverFileItem
|
||||||
isAttachedToNote={attachedFiles.includes(file)}
|
key={file.uuid}
|
||||||
handleFileAction={fileActionHandler}
|
file={file}
|
||||||
/>
|
isAttachedToNote={attachedFiles.includes(file)}
|
||||||
);
|
handleFileAction={handleFileAction}
|
||||||
})
|
getIconType={application.iconsController.getIconForFileType}
|
||||||
|
closeOnBlur={closeOnBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center w-full py-8">
|
<div className="flex flex-col items-center justify-center w-full py-8">
|
||||||
<div className="w-18 h-18 mb-2">
|
<div className="w-18 h-18 mb-2">
|
||||||
<FilesIllustration
|
<FilesIllustration />
|
||||||
style={{
|
|
||||||
transform: 'scale(0.6)',
|
|
||||||
transformOrigin: 'top left',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-medium mb-3">
|
<div className="text-sm font-medium mb-3">
|
||||||
{searchQuery.length > 0
|
{searchQuery.length > 0
|
||||||
@@ -167,7 +195,11 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
? 'No files attached to this note'
|
? 'No files attached to this note'
|
||||||
: 'No files found in this account'}
|
: 'No files found in this account'}
|
||||||
</div>
|
</div>
|
||||||
<Button type="normal" onClick={handleAttachFilesClick}>
|
<Button
|
||||||
|
type="normal"
|
||||||
|
onClick={handleAttachFilesClick}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
>
|
||||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'}{' '}
|
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'}{' '}
|
||||||
files
|
files
|
||||||
</Button>
|
</Button>
|
||||||
@@ -181,6 +213,7 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
|
|||||||
<button
|
<button
|
||||||
className="sn-dropdown-item py-3 border-0 border-t-1px border-solid border-main focus:bg-info-backdrop"
|
className="sn-dropdown-item py-3 border-0 border-t-1px border-solid border-main focus:bg-info-backdrop"
|
||||||
onClick={handleAttachFilesClick}
|
onClick={handleAttachFilesClick}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
>
|
>
|
||||||
<Icon type="add" className="mr-2 color-neutral" />
|
<Icon type="add" className="mr-2 color-neutral" />
|
||||||
{currentTab === PopoverTabs.AttachedFiles
|
{currentTab === PopoverTabs.AttachedFiles
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants';
|
|
||||||
import { WebApplication } from '@/ui_models/application';
|
|
||||||
import { AppState } from '@/ui_models/app_state';
|
|
||||||
import { StreamingFileReader } from '@standardnotes/filepicker';
|
|
||||||
import { SNNote } from '@standardnotes/snjs';
|
|
||||||
import { FunctionComponent } from 'preact';
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
||||||
import { AttachedFilesPopover } from './AttachedFilesPopover';
|
|
||||||
import {
|
|
||||||
PopoverFileItemAction,
|
|
||||||
PopoverFileItemActionType,
|
|
||||||
} from './PopoverFileItemAction';
|
|
||||||
|
|
||||||
export enum PopoverTabs {
|
|
||||||
AttachedFiles,
|
|
||||||
AllFiles,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PopoverWrapperProps = {
|
|
||||||
application: WebApplication;
|
|
||||||
appState: AppState;
|
|
||||||
note: SNNote;
|
|
||||||
fileActionHandler: (action: PopoverFileItemAction) => Promise<boolean>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PopoverDragNDropWrapper: FunctionComponent<
|
|
||||||
PopoverWrapperProps
|
|
||||||
> = ({ fileActionHandler, appState, application, note }) => {
|
|
||||||
const dropzoneRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles);
|
|
||||||
const dragCounter = useRef(0);
|
|
||||||
|
|
||||||
const handleDrag = (event: DragEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragIn = (event: DragEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
dragCounter.current = dragCounter.current + 1;
|
|
||||||
|
|
||||||
if (event.dataTransfer?.items.length) {
|
|
||||||
setIsDragging(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOut = (event: DragEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
dragCounter.current = dragCounter.current - 1;
|
|
||||||
|
|
||||||
if (dragCounter.current > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDragging(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
|
||||||
(event: DragEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
setIsDragging(false);
|
|
||||||
|
|
||||||
if (event.dataTransfer?.items.length) {
|
|
||||||
Array.from(event.dataTransfer.items).forEach(async (item) => {
|
|
||||||
let fileOrHandle;
|
|
||||||
if (StreamingFileReader.available()) {
|
|
||||||
fileOrHandle =
|
|
||||||
(await item.getAsFileSystemHandle()) as FileSystemFileHandle;
|
|
||||||
} else {
|
|
||||||
fileOrHandle = item.getAsFile();
|
|
||||||
}
|
|
||||||
if (fileOrHandle) {
|
|
||||||
const uploadedFiles = await appState.files.uploadNewFile(
|
|
||||||
fileOrHandle
|
|
||||||
);
|
|
||||||
if (!uploadedFiles) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
|
||||||
uploadedFiles.forEach((file) => {
|
|
||||||
fileActionHandler({
|
|
||||||
type: PopoverFileItemActionType.AttachFileToNote,
|
|
||||||
payload: file,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
event.dataTransfer.clearData();
|
|
||||||
dragCounter.current = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[appState.files, currentTab, fileActionHandler]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const dropzoneElement = dropzoneRef.current;
|
|
||||||
|
|
||||||
if (dropzoneElement) {
|
|
||||||
dropzoneElement.addEventListener('dragenter', handleDragIn);
|
|
||||||
dropzoneElement.addEventListener('dragleave', handleDragOut);
|
|
||||||
dropzoneElement.addEventListener('dragover', handleDrag);
|
|
||||||
dropzoneElement.addEventListener('drop', handleDrop);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
dropzoneElement?.removeEventListener('dragenter', handleDragIn);
|
|
||||||
dropzoneElement?.removeEventListener('dragleave', handleDragOut);
|
|
||||||
dropzoneElement?.removeEventListener('dragover', handleDrag);
|
|
||||||
dropzoneElement?.removeEventListener('drop', handleDrop);
|
|
||||||
};
|
|
||||||
}, [handleDrop]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={dropzoneRef}
|
|
||||||
className="focus:shadow-none"
|
|
||||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
|
||||||
style={{
|
|
||||||
border: isDragging ? '2px dashed var(--sn-stylekit-info-color)' : '',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AttachedFilesPopover
|
|
||||||
application={application}
|
|
||||||
appState={appState}
|
|
||||||
note={note}
|
|
||||||
fileActionHandler={fileActionHandler}
|
|
||||||
currentTab={currentTab}
|
|
||||||
setCurrentTab={setCurrentTab}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,68 +1,40 @@
|
|||||||
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants';
|
||||||
import { KeyboardKey } from '@/services/ioService';
|
import { KeyboardKey } from '@/services/ioService';
|
||||||
import { formatSizeToReadableString } from '@standardnotes/filepicker';
|
import { formatSizeToReadableString } from '@standardnotes/filepicker';
|
||||||
import { SNFile } from '@standardnotes/snjs';
|
import { IconType, SNFile } from '@standardnotes/snjs';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { ICONS } from '../Icon';
|
import { Icon, ICONS } from '../Icon';
|
||||||
import {
|
import {
|
||||||
PopoverFileItemAction,
|
PopoverFileItemAction,
|
||||||
PopoverFileItemActionType,
|
PopoverFileItemActionType,
|
||||||
} from './PopoverFileItemAction';
|
} from './PopoverFileItemAction';
|
||||||
import { PopoverFileSubmenu } from './PopoverFileSubmenu';
|
import { PopoverFileSubmenu } from './PopoverFileSubmenu';
|
||||||
|
|
||||||
const getIconForFileType = (fileType: string) => {
|
export const getFileIconComponent = (iconType: string, className: string) => {
|
||||||
let iconType = 'file-other';
|
|
||||||
|
|
||||||
if (fileType === 'pdf') {
|
|
||||||
iconType = 'file-pdf';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^(docx?|odt)/.test(fileType)) {
|
|
||||||
iconType = 'file-doc';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^pptx?/.test(fileType)) {
|
|
||||||
iconType = 'file-ppt';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^(xlsx?|ods)/.test(fileType)) {
|
|
||||||
iconType = 'file-xls';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^(jpe?g|a?png|webp|gif)/.test(fileType)) {
|
|
||||||
iconType = 'file-image';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^(mov|mp4|mkv)/.test(fileType)) {
|
|
||||||
iconType = 'file-mov';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^(wav|mp3|flac|ogg)/.test(fileType)) {
|
|
||||||
iconType = 'file-music';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^(zip|rar|7z)/.test(fileType)) {
|
|
||||||
iconType = 'file-zip';
|
|
||||||
}
|
|
||||||
|
|
||||||
const IconComponent = ICONS[iconType as keyof typeof ICONS];
|
const IconComponent = ICONS[iconType as keyof typeof ICONS];
|
||||||
|
|
||||||
return <IconComponent className="flex-shrink-0" />;
|
return <IconComponent className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PopoverFileItemProps = {
|
export type PopoverFileItemProps = {
|
||||||
file: SNFile;
|
file: SNFile;
|
||||||
isAttachedToNote: boolean;
|
isAttachedToNote: boolean;
|
||||||
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>;
|
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>;
|
||||||
|
getIconType(type: string): IconType;
|
||||||
|
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
||||||
file,
|
file,
|
||||||
isAttachedToNote,
|
isAttachedToNote,
|
||||||
handleFileAction,
|
handleFileAction,
|
||||||
|
getIconType,
|
||||||
|
closeOnBlur,
|
||||||
}) => {
|
}) => {
|
||||||
const [fileName, setFileName] = useState(file.nameWithExt);
|
const [fileName, setFileName] = useState(file.name);
|
||||||
const [isRenamingFile, setIsRenamingFile] = useState(false);
|
const [isRenamingFile, setIsRenamingFile] = useState(false);
|
||||||
|
const itemRef = useRef<HTMLDivElement>(null);
|
||||||
const fileNameInputRef = useRef<HTMLInputElement>(null);
|
const fileNameInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -72,16 +44,14 @@ export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
|||||||
}, [isRenamingFile]);
|
}, [isRenamingFile]);
|
||||||
|
|
||||||
const renameFile = async (file: SNFile, name: string) => {
|
const renameFile = async (file: SNFile, name: string) => {
|
||||||
const didRename = await handleFileAction({
|
await handleFileAction({
|
||||||
type: PopoverFileItemActionType.RenameFile,
|
type: PopoverFileItemActionType.RenameFile,
|
||||||
payload: {
|
payload: {
|
||||||
file,
|
file,
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (didRename) {
|
setIsRenamingFile(false);
|
||||||
setIsRenamingFile(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileNameInput = (event: Event) => {
|
const handleFileNameInput = (event: Event) => {
|
||||||
@@ -90,15 +60,25 @@ export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
|||||||
|
|
||||||
const handleFileNameInputKeyDown = (event: KeyboardEvent) => {
|
const handleFileNameInputKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === KeyboardKey.Enter) {
|
if (event.key === KeyboardKey.Enter) {
|
||||||
renameFile(file, fileName);
|
itemRef.current?.focus();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileNameInputBlur = () => {
|
||||||
|
renameFile(file, fileName);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between p-3">
|
<div
|
||||||
|
ref={itemRef}
|
||||||
|
className="flex items-center justify-between p-3 focus:shadow-none"
|
||||||
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{getIconForFileType(file.ext ?? '')}
|
{getFileIconComponent(
|
||||||
|
getIconType(file.mimeType),
|
||||||
|
'w-8 h-8 flex-shrink-0'
|
||||||
|
)}
|
||||||
<div className="flex flex-col mx-4">
|
<div className="flex flex-col mx-4">
|
||||||
{isRenamingFile ? (
|
{isRenamingFile ? (
|
||||||
<input
|
<input
|
||||||
@@ -108,9 +88,18 @@ export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
|||||||
ref={fileNameInputRef}
|
ref={fileNameInputRef}
|
||||||
onInput={handleFileNameInput}
|
onInput={handleFileNameInput}
|
||||||
onKeyDown={handleFileNameInputKeyDown}
|
onKeyDown={handleFileNameInputKeyDown}
|
||||||
|
onBlur={handleFileNameInputBlur}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm mb-1">{file.nameWithExt}</div>
|
<div className="text-sm mb-1 break-word">
|
||||||
|
<span className="vertical-middle">{file.name}</span>
|
||||||
|
{file.protected && (
|
||||||
|
<Icon
|
||||||
|
type="lock-filled"
|
||||||
|
className="sn-icon--small ml-2 color-neutral vertical-middle"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-xs color-grey-0">
|
<div className="text-xs color-grey-0">
|
||||||
{file.created_at.toLocaleString()} ·{' '}
|
{file.created_at.toLocaleString()} ·{' '}
|
||||||
@@ -123,6 +112,7 @@ export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
|||||||
isAttachedToNote={isAttachedToNote}
|
isAttachedToNote={isAttachedToNote}
|
||||||
handleFileAction={handleFileAction}
|
handleFileAction={handleFileAction}
|
||||||
setIsRenamingFile={setIsRenamingFile}
|
setIsRenamingFile={setIsRenamingFile}
|
||||||
|
closeOnBlur={closeOnBlur}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ import {
|
|||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { Switch } from '../Switch';
|
import { Switch } from '../Switch';
|
||||||
import { useCloseOnBlur } from '../utils';
|
import { useCloseOnBlur } from '../utils';
|
||||||
|
import { useFilePreviewModal } from '../Files/FilePreviewModalProvider';
|
||||||
import { PopoverFileItemProps } from './PopoverFileItem';
|
import { PopoverFileItemProps } from './PopoverFileItem';
|
||||||
import { PopoverFileItemActionType } from './PopoverFileItemAction';
|
import { PopoverFileItemActionType } from './PopoverFileItemAction';
|
||||||
|
|
||||||
type Props = Omit<PopoverFileItemProps, 'renameFile'> & {
|
type Props = Omit<PopoverFileItemProps, 'renameFile' | 'getIconType'> & {
|
||||||
setIsRenamingFile: StateUpdater<boolean>;
|
setIsRenamingFile: StateUpdater<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +33,8 @@ export const PopoverFileSubmenu: FunctionComponent<Props> = ({
|
|||||||
handleFileAction,
|
handleFileAction,
|
||||||
setIsRenamingFile,
|
setIsRenamingFile,
|
||||||
}) => {
|
}) => {
|
||||||
|
const filePreviewModal = useFilePreviewModal();
|
||||||
|
|
||||||
const menuContainerRef = useRef<HTMLDivElement>(null);
|
const menuContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -99,6 +102,17 @@ export const PopoverFileSubmenu: FunctionComponent<Props> = ({
|
|||||||
>
|
>
|
||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<>
|
<>
|
||||||
|
<button
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
className="sn-dropdown-item focus:bg-info-backdrop"
|
||||||
|
onClick={() => {
|
||||||
|
filePreviewModal.activate(file);
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="file" className="mr-2 color-neutral" />
|
||||||
|
Preview file
|
||||||
|
</button>
|
||||||
{isAttachedToNote ? (
|
{isAttachedToNote ? (
|
||||||
<button
|
<button
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
@@ -179,6 +193,20 @@ export const PopoverFileSubmenu: FunctionComponent<Props> = ({
|
|||||||
<Icon type="pencil" className="mr-2 color-neutral" />
|
<Icon type="pencil" className="mr-2 color-neutral" />
|
||||||
Rename
|
Rename
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
className="sn-dropdown-item focus:bg-info-backdrop"
|
||||||
|
onClick={() => {
|
||||||
|
handleFileAction({
|
||||||
|
type: PopoverFileItemActionType.DeleteFile,
|
||||||
|
payload: file,
|
||||||
|
});
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="trash" className="mr-2 color-danger" />
|
||||||
|
<span className="color-danger">Delete permanently</span>
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
|
|||||||
@@ -22,22 +22,25 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
|
|||||||
} = appState.noteTags;
|
} = appState.noteTags;
|
||||||
|
|
||||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||||
const [dropdownMaxHeight, setDropdownMaxHeight] =
|
const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>(
|
||||||
useState<number | 'auto'>('auto');
|
'auto'
|
||||||
|
);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [closeOnBlur] = useCloseOnBlur(containerRef as any, (visible: boolean) => {
|
const [closeOnBlur] = useCloseOnBlur(containerRef, (visible: boolean) => {
|
||||||
setDropdownVisible(visible);
|
setDropdownVisible(visible);
|
||||||
appState.noteTags.clearAutocompleteSearch();
|
appState.noteTags.clearAutocompleteSearch();
|
||||||
});
|
});
|
||||||
|
|
||||||
const showDropdown = () => {
|
const showDropdown = () => {
|
||||||
const { clientHeight } = document.documentElement;
|
const { clientHeight } = document.documentElement;
|
||||||
const inputRect = inputRef.current!.getBoundingClientRect();
|
const inputRect = inputRef.current?.getBoundingClientRect();
|
||||||
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2);
|
if (inputRect) {
|
||||||
setDropdownVisible(true);
|
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2);
|
||||||
|
setDropdownVisible(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSearchQueryChange = (event: Event) => {
|
const onSearchQueryChange = (event: Event) => {
|
||||||
@@ -93,7 +96,7 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autocompleteInputFocused) {
|
if (autocompleteInputFocused) {
|
||||||
inputRef.current!.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, [appState.noteTags, autocompleteInputFocused]);
|
}, [appState.noteTags, autocompleteInputFocused]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { splitQueryInString } from '@/utils/stringUtils';
|
||||||
import { SNTag } from '@standardnotes/snjs';
|
import { SNTag } from '@standardnotes/snjs';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useEffect, useRef } from 'preact/hooks';
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
@@ -71,7 +72,7 @@ export const AutocompleteTagResult = observer(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focusedTagResultUuid === tagResult.uuid) {
|
if (focusedTagResultUuid === tagResult.uuid) {
|
||||||
tagResultRef.current!.focus();
|
tagResultRef.current?.focus();
|
||||||
appState.noteTags.setFocusedTagResultUuid(undefined);
|
appState.noteTags.setFocusedTagResultUuid(undefined);
|
||||||
}
|
}
|
||||||
}, [appState.noteTags, focusedTagResultUuid, tagResult]);
|
}, [appState.noteTags, focusedTagResultUuid, tagResult]);
|
||||||
@@ -92,9 +93,8 @@ export const AutocompleteTagResult = observer(
|
|||||||
{prefixTitle && <span className="grey-2">{prefixTitle}</span>}
|
{prefixTitle && <span className="grey-2">{prefixTitle}</span>}
|
||||||
{autocompleteSearchQuery === ''
|
{autocompleteSearchQuery === ''
|
||||||
? title
|
? title
|
||||||
: title
|
: splitQueryInString(title, autocompleteSearchQuery).map(
|
||||||
.split(new RegExp(`(${autocompleteSearchQuery})`, 'gi'))
|
(substring, index) => (
|
||||||
.map((substring, index) => (
|
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className={`${
|
className={`${
|
||||||
@@ -106,7 +106,8 @@ export const AutocompleteTagResult = observer(
|
|||||||
>
|
>
|
||||||
{substring}
|
{substring}
|
||||||
</span>
|
</span>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type ButtonProps = {
|
|||||||
| TargetedEvent<HTMLFormElement>
|
| TargetedEvent<HTMLFormElement>
|
||||||
| TargetedMouseEvent<HTMLButtonElement>
|
| TargetedMouseEvent<HTMLButtonElement>
|
||||||
) => void;
|
) => void;
|
||||||
|
onBlur?: (event: FocusEvent) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export const Button: FunctionComponent<ButtonProps> = forwardRef(
|
|||||||
type,
|
type,
|
||||||
label,
|
label,
|
||||||
className = '',
|
className = '',
|
||||||
|
onBlur,
|
||||||
onClick,
|
onClick,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
children,
|
children,
|
||||||
@@ -46,6 +48,7 @@ export const Button: FunctionComponent<ButtonProps> = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`${buttonClass} ${cursorClass} ${className}`}
|
className={`${buttonClass} ${cursorClass} ${className}`}
|
||||||
|
onBlur={onBlur}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
onClick(e);
|
onClick(e);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -123,13 +123,13 @@ export class ChallengeModal extends PureComponent<Props, State> {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
this.application.signOut();
|
this.application.user.signOut();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
cancel = () => {
|
cancel = () => {
|
||||||
if (this.props.challenge.cancelable) {
|
if (this.props.challenge.cancelable) {
|
||||||
this.application!.cancelChallenge(this.props.challenge);
|
this.application.cancelChallenge(this.props.challenge);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { FunctionComponent } from 'preact';
|
|
||||||
|
|
||||||
export const CircleProgress: FunctionComponent<{
|
|
||||||
percent: number;
|
|
||||||
className?: string;
|
|
||||||
}> = ({ percent, className = '' }) => {
|
|
||||||
const size = 16;
|
|
||||||
const ratioStrokeRadius = 0.25;
|
|
||||||
const outerRadius = size / 2;
|
|
||||||
|
|
||||||
const radius = outerRadius * (1 - ratioStrokeRadius);
|
|
||||||
const stroke = outerRadius - radius;
|
|
||||||
|
|
||||||
const circumference = radius * 2 * Math.PI;
|
|
||||||
const offset = circumference - (percent / 100) * circumference;
|
|
||||||
|
|
||||||
const transition = `transition: 0.35s stroke-dashoffset;`;
|
|
||||||
const transform = `transform: rotate(-90deg);`;
|
|
||||||
const transformOrigin = `transform-origin: 50% 50%;`;
|
|
||||||
const dasharray = `stroke-dasharray: ${circumference} ${circumference};`;
|
|
||||||
const dashoffset = `stroke-dashoffset: ${offset};`;
|
|
||||||
const style = `${transition} ${transform} ${transformOrigin} ${dasharray} ${dashoffset}`;
|
|
||||||
return (
|
|
||||||
<div className="h-5 w-5 min-w-5 min-h-5">
|
|
||||||
<svg viewBox={`0 0 ${size} ${size}`}>
|
|
||||||
<circle
|
|
||||||
stroke="#086DD6"
|
|
||||||
stroke-width={stroke}
|
|
||||||
fill="transparent"
|
|
||||||
r={radius}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
style={style}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { FunctionalComponent } from 'preact';
|
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
|
||||||
import { CircleProgress } from './CircleProgress';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Circular progress bar which runs in a specified time interval
|
|
||||||
* @param time - time interval in ms
|
|
||||||
*/
|
|
||||||
export const CircleProgressTime: FunctionalComponent<{ time: number }> = ({
|
|
||||||
time,
|
|
||||||
}) => {
|
|
||||||
const [percent, setPercent] = useState(0);
|
|
||||||
const interval = time / 100;
|
|
||||||
useEffect(() => {
|
|
||||||
const tick = setInterval(() => {
|
|
||||||
if (percent === 100) {
|
|
||||||
setPercent(0);
|
|
||||||
} else {
|
|
||||||
setPercent(percent + 1);
|
|
||||||
}
|
|
||||||
}, interval);
|
|
||||||
return () => {
|
|
||||||
clearInterval(tick);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return <CircleProgress percent={percent} />;
|
|
||||||
};
|
|
||||||
@@ -43,8 +43,8 @@ export const ConfirmSignoutModal = observer(
|
|||||||
<div className="sk-panel">
|
<div className="sk-panel">
|
||||||
<div className="sk-panel-content">
|
<div className="sk-panel-content">
|
||||||
<div className="sk-panel-section">
|
<div className="sk-panel-section">
|
||||||
<AlertDialogLabel className="sk-h3 sk-panel-section-title capitalize">
|
<AlertDialogLabel className="sk-h3 sk-panel-section-title">
|
||||||
End your session?
|
Sign out workspace?
|
||||||
</AlertDialogLabel>
|
</AlertDialogLabel>
|
||||||
<AlertDialogDescription className="sk-panel-row">
|
<AlertDialogDescription className="sk-panel-row">
|
||||||
<p className="color-foreground">
|
<p className="color-foreground">
|
||||||
@@ -93,7 +93,7 @@ export const ConfirmSignoutModal = observer(
|
|||||||
if (deleteLocalBackups) {
|
if (deleteLocalBackups) {
|
||||||
application.signOutAndDeleteLocalBackups();
|
application.signOutAndDeleteLocalBackups();
|
||||||
} else {
|
} else {
|
||||||
application.signOut();
|
application.user.signOut();
|
||||||
}
|
}
|
||||||
closeDialog();
|
closeDialog();
|
||||||
}}
|
}}
|
||||||
|
|||||||
194
app/assets/javascripts/components/Files/FilePreviewModal.tsx
Normal file
194
app/assets/javascripts/components/Files/FilePreviewModal.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { concatenateUint8Arrays } from '@/utils/concatenateUint8Arrays';
|
||||||
|
import { DialogContent, DialogOverlay } from '@reach/dialog';
|
||||||
|
import { SNFile } from '@standardnotes/snjs';
|
||||||
|
import { NoPreviewIllustration } from '@standardnotes/stylekit';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { getFileIconComponent } from '../AttachedFilesPopover/PopoverFileItem';
|
||||||
|
import { Button } from '../Button';
|
||||||
|
import { Icon } from '../Icon';
|
||||||
|
import { isFileTypePreviewable } from './isFilePreviewable';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
file: SNFile;
|
||||||
|
onDismiss: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviewComponentForFile = (file: SNFile, objectUrl: string) => {
|
||||||
|
if (file.mimeType.startsWith('image/')) {
|
||||||
|
return <img src={objectUrl} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.mimeType.startsWith('video/')) {
|
||||||
|
return <video className="w-full h-full" src={objectUrl} controls />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.mimeType.startsWith('audio/')) {
|
||||||
|
return <audio src={objectUrl} controls />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <object className="w-full h-full" data={objectUrl} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilePreviewModal: FunctionComponent<Props> = ({
|
||||||
|
application,
|
||||||
|
file,
|
||||||
|
onDismiss,
|
||||||
|
}) => {
|
||||||
|
const [objectUrl, setObjectUrl] = useState<string>();
|
||||||
|
const [isFilePreviewable, setIsFilePreviewable] = useState(false);
|
||||||
|
const [isLoadingFile, setIsLoadingFile] = useState(false);
|
||||||
|
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const getObjectUrl = useCallback(async () => {
|
||||||
|
setIsLoadingFile(true);
|
||||||
|
try {
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
await application.files.downloadFile(
|
||||||
|
file,
|
||||||
|
async (decryptedChunk: Uint8Array) => {
|
||||||
|
chunks.push(decryptedChunk);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const finalDecryptedBytes = concatenateUint8Arrays(chunks);
|
||||||
|
setObjectUrl(
|
||||||
|
URL.createObjectURL(
|
||||||
|
new Blob([finalDecryptedBytes], {
|
||||||
|
type: file.mimeType,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingFile(false);
|
||||||
|
}
|
||||||
|
}, [application.files, file]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isPreviewable = isFileTypePreviewable(file.mimeType);
|
||||||
|
setIsFilePreviewable(isPreviewable);
|
||||||
|
|
||||||
|
if (!objectUrl && isPreviewable) {
|
||||||
|
getObjectUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (objectUrl) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [file.mimeType, getObjectUrl, objectUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogOverlay
|
||||||
|
className="sn-component"
|
||||||
|
aria-label="File preview modal"
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
initialFocusRef={closeButtonRef}
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
className="flex flex-col rounded shadow-overlay"
|
||||||
|
style={{
|
||||||
|
width: '90%',
|
||||||
|
maxWidth: '90%',
|
||||||
|
minHeight: '90%',
|
||||||
|
background: 'var(--sn-stylekit-background-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-shrink-0 justify-between items-center min-h-6 px-4 py-3 border-0 border-b-1 border-solid border-main">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-6 h-6">
|
||||||
|
{getFileIconComponent(
|
||||||
|
application.iconsController.getIconForFileType(file.mimeType),
|
||||||
|
'w-6 h-6 flex-shrink-0'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="ml-3 font-medium">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{objectUrl && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className="mr-4"
|
||||||
|
onClick={() => {
|
||||||
|
application
|
||||||
|
.getArchiveService()
|
||||||
|
.downloadData(objectUrl, file.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
ref={closeButtonRef}
|
||||||
|
onClick={onDismiss}
|
||||||
|
aria-label="Close modal"
|
||||||
|
className="flex p-1 bg-transparent border-0 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Icon type="close" className="color-neutral" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow items-center justify-center min-h-0 overflow-auto">
|
||||||
|
{objectUrl ? (
|
||||||
|
getPreviewComponentForFile(file, objectUrl)
|
||||||
|
) : isLoadingFile ? (
|
||||||
|
<div className="sk-spinner w-5 h-5 spinner-info"></div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<NoPreviewIllustration className="w-30 h-30 mb-4" />
|
||||||
|
<div className="font-bold text-base mb-2">
|
||||||
|
This file can't be previewed.
|
||||||
|
</div>
|
||||||
|
{isFilePreviewable ? (
|
||||||
|
<>
|
||||||
|
<div className="text-sm text-center color-grey-0 mb-4 max-w-35ch">
|
||||||
|
There was an error loading the file. Try again, or download
|
||||||
|
it and open it using another application.
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className="mr-3"
|
||||||
|
onClick={() => {
|
||||||
|
getObjectUrl();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="normal"
|
||||||
|
onClick={() => {
|
||||||
|
application.getAppState().files.downloadFile(file);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-sm text-center color-grey-0 mb-4 max-w-35ch">
|
||||||
|
To view this file, download it and open it using another
|
||||||
|
application.
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
application.getAppState().files.downloadFile(file);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { SNFile } from '@standardnotes/snjs';
|
||||||
|
import { createContext, FunctionComponent } from 'preact';
|
||||||
|
import { useContext, useState } from 'preact/hooks';
|
||||||
|
import { FilePreviewModal } from './FilePreviewModal';
|
||||||
|
|
||||||
|
type FilePreviewModalContextData = {
|
||||||
|
activate: (file: SNFile) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilePreviewModalContext =
|
||||||
|
createContext<FilePreviewModalContextData | null>(null);
|
||||||
|
|
||||||
|
export const useFilePreviewModal = (): FilePreviewModalContextData => {
|
||||||
|
const value = useContext(FilePreviewModalContext);
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
throw new Error('FilePreviewModalProvider not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilePreviewModalProvider: FunctionComponent<{
|
||||||
|
application: WebApplication;
|
||||||
|
}> = ({ application, children }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [file, setFile] = useState<SNFile>();
|
||||||
|
|
||||||
|
const activate = (file: SNFile) => {
|
||||||
|
setFile(file);
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isOpen && file && (
|
||||||
|
<FilePreviewModal
|
||||||
|
application={application}
|
||||||
|
file={file}
|
||||||
|
onDismiss={close}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FilePreviewModalContext.Provider value={{ activate }}>
|
||||||
|
{children}
|
||||||
|
</FilePreviewModalContext.Provider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
12
app/assets/javascripts/components/Files/isFilePreviewable.ts
Normal file
12
app/assets/javascripts/components/Files/isFilePreviewable.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const isFileTypePreviewable = (fileType: string) => {
|
||||||
|
const isImage = fileType.startsWith('image/');
|
||||||
|
const isVideo = fileType.startsWith('video/');
|
||||||
|
const isAudio = fileType.startsWith('audio/');
|
||||||
|
const isPdf = fileType === 'application/pdf';
|
||||||
|
|
||||||
|
if (isImage || isVideo || isAudio || isPdf) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -23,7 +23,6 @@ import { Icon } from './Icon';
|
|||||||
import { QuickSettingsMenu } from './QuickSettingsMenu/QuickSettingsMenu';
|
import { QuickSettingsMenu } from './QuickSettingsMenu/QuickSettingsMenu';
|
||||||
import { SyncResolutionMenu } from './SyncResolutionMenu';
|
import { SyncResolutionMenu } from './SyncResolutionMenu';
|
||||||
import { Fragment, render } from 'preact';
|
import { Fragment, render } from 'preact';
|
||||||
import { AccountSwitcher } from './AccountSwitcher';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable before production release.
|
* Disable before production release.
|
||||||
@@ -43,7 +42,6 @@ type State = {
|
|||||||
dataUpgradeAvailable: boolean;
|
dataUpgradeAvailable: boolean;
|
||||||
hasPasscode: boolean;
|
hasPasscode: boolean;
|
||||||
descriptors: ApplicationDescriptor[];
|
descriptors: ApplicationDescriptor[];
|
||||||
hasAccountSwitcher: boolean;
|
|
||||||
showBetaWarning: boolean;
|
showBetaWarning: boolean;
|
||||||
showSyncResolution: boolean;
|
showSyncResolution: boolean;
|
||||||
newUpdateAvailable: boolean;
|
newUpdateAvailable: boolean;
|
||||||
@@ -70,7 +68,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
dataUpgradeAvailable: false,
|
dataUpgradeAvailable: false,
|
||||||
hasPasscode: false,
|
hasPasscode: false,
|
||||||
descriptors: props.applicationGroup.getDescriptors(),
|
descriptors: props.applicationGroup.getDescriptors(),
|
||||||
hasAccountSwitcher: false,
|
|
||||||
showBetaWarning: false,
|
showBetaWarning: false,
|
||||||
showSyncResolution: false,
|
showSyncResolution: false,
|
||||||
newUpdateAvailable: false,
|
newUpdateAvailable: false,
|
||||||
@@ -100,7 +97,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
arbitraryStatusMessage: message,
|
arbitraryStatusMessage: message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.loadAccountSwitcherState();
|
|
||||||
this.autorun(() => {
|
this.autorun(() => {
|
||||||
const showBetaWarning = this.appState.showBetaWarning;
|
const showBetaWarning = this.appState.showBetaWarning;
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -111,18 +107,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAccountSwitcherState() {
|
|
||||||
const stringValue = localStorage.getItem(ACCOUNT_SWITCHER_FEATURE_KEY);
|
|
||||||
if (!stringValue && ACCOUNT_SWITCHER_ENABLED) {
|
|
||||||
/** Enable permanently for this user so they don't lose the feature after its disabled */
|
|
||||||
localStorage.setItem(ACCOUNT_SWITCHER_FEATURE_KEY, JSON.stringify(true));
|
|
||||||
}
|
|
||||||
const hasAccountSwitcher = stringValue
|
|
||||||
? JSON.parse(stringValue)
|
|
||||||
: ACCOUNT_SWITCHER_ENABLED;
|
|
||||||
this.setState({ hasAccountSwitcher });
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadUpgradeStatus() {
|
reloadUpgradeStatus() {
|
||||||
this.application.checkForSecurityUpdate().then((available) => {
|
this.application.checkForSecurityUpdate().then((available) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -212,7 +196,10 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
if (!this.didCheckForOffline) {
|
if (!this.didCheckForOffline) {
|
||||||
this.didCheckForOffline = true;
|
this.didCheckForOffline = true;
|
||||||
if (this.state.offline && this.application.getNoteCount() === 0) {
|
if (
|
||||||
|
this.state.offline &&
|
||||||
|
this.application.items.getNoteCount() === 0
|
||||||
|
) {
|
||||||
this.appState.accountMenu.setShow(true);
|
this.appState.accountMenu.setShow(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +231,7 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
streamItems() {
|
streamItems() {
|
||||||
this.application.setDisplayOptions(
|
this.application.items.setDisplayOptions(
|
||||||
ContentType.Theme,
|
ContentType.Theme,
|
||||||
CollectionSort.Title,
|
CollectionSort.Title,
|
||||||
'asc',
|
'asc',
|
||||||
@@ -330,16 +317,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
accountSwitcherClickHandler = () => {
|
|
||||||
render(
|
|
||||||
<AccountSwitcher
|
|
||||||
application={this.application}
|
|
||||||
mainApplicationGroup={this.props.applicationGroup}
|
|
||||||
/>,
|
|
||||||
document.body.appendChild(document.createElement('div'))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
accountMenuClickHandler = () => {
|
accountMenuClickHandler = () => {
|
||||||
this.appState.quickSettingsMenu.closeQuickSettingsMenu();
|
this.appState.quickSettingsMenu.closeQuickSettingsMenu();
|
||||||
this.appState.accountMenu.toggleShow();
|
this.appState.accountMenu.toggleShow();
|
||||||
@@ -426,6 +403,7 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
onClickOutside={this.clickOutsideAccountMenu}
|
onClickOutside={this.clickOutsideAccountMenu}
|
||||||
appState={this.appState}
|
appState={this.appState}
|
||||||
application={this.application}
|
application={this.application}
|
||||||
|
mainApplicationGroup={this.props.applicationGroup}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -519,24 +497,6 @@ export class Footer extends PureComponent<Props, State> {
|
|||||||
<div className="sk-label">Offline</div>
|
<div className="sk-label">Offline</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{this.state.hasAccountSwitcher && (
|
|
||||||
<Fragment>
|
|
||||||
<div className="sk-app-bar-item border" />
|
|
||||||
<div
|
|
||||||
onClick={this.accountSwitcherClickHandler}
|
|
||||||
className="sk-app-bar-item"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
(this.state.hasPasscode ? 'alone' : '') +
|
|
||||||
' flex items-center'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon type="user-switch" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
{this.state.hasPasscode && (
|
{this.state.hasPasscode && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="sk-app-bar-item border" />
|
<div className="sk-app-bar-item border" />
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import {
|
|||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
ClearCircleFilledIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
CloudOffIcon,
|
CloudOffIcon,
|
||||||
ClearCircleFilledIcon,
|
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DashboardIcon,
|
DashboardIcon,
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
EyeIcon,
|
EyeIcon,
|
||||||
EyeOffIcon,
|
EyeOffIcon,
|
||||||
FileDocIcon,
|
FileDocIcon,
|
||||||
|
FileIcon,
|
||||||
FileImageIcon,
|
FileImageIcon,
|
||||||
FileMovIcon,
|
FileMovIcon,
|
||||||
FileMusicIcon,
|
FileMusicIcon,
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
FilePptIcon,
|
FilePptIcon,
|
||||||
FileXlsIcon,
|
FileXlsIcon,
|
||||||
FileZipIcon,
|
FileZipIcon,
|
||||||
|
FolderIcon,
|
||||||
HashtagIcon,
|
HashtagIcon,
|
||||||
HashtagOffIcon,
|
HashtagOffIcon,
|
||||||
HelpIcon,
|
HelpIcon,
|
||||||
@@ -81,6 +83,7 @@ import {
|
|||||||
TuneIcon,
|
TuneIcon,
|
||||||
UnarchiveIcon,
|
UnarchiveIcon,
|
||||||
UnpinIcon,
|
UnpinIcon,
|
||||||
|
UserAddIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
UserSwitch,
|
UserSwitch,
|
||||||
WarningIcon,
|
WarningIcon,
|
||||||
@@ -97,8 +100,8 @@ export const ICONS = {
|
|||||||
'check-circle': CheckCircleIcon,
|
'check-circle': CheckCircleIcon,
|
||||||
'chevron-down': ChevronDownIcon,
|
'chevron-down': ChevronDownIcon,
|
||||||
'chevron-right': ChevronRightIcon,
|
'chevron-right': ChevronRightIcon,
|
||||||
'cloud-off': CloudOffIcon,
|
|
||||||
'clear-circle-filled': ClearCircleFilledIcon,
|
'clear-circle-filled': ClearCircleFilledIcon,
|
||||||
|
'cloud-off': CloudOffIcon,
|
||||||
'eye-off': EyeOffIcon,
|
'eye-off': EyeOffIcon,
|
||||||
'file-doc': FileDocIcon,
|
'file-doc': FileDocIcon,
|
||||||
'file-image': FileImageIcon,
|
'file-image': FileImageIcon,
|
||||||
@@ -125,6 +128,7 @@ export const ICONS = {
|
|||||||
'rich-text': RichTextIcon,
|
'rich-text': RichTextIcon,
|
||||||
'trash-filled': TrashFilledIcon,
|
'trash-filled': TrashFilledIcon,
|
||||||
'trash-sweep': TrashSweepIcon,
|
'trash-sweep': TrashSweepIcon,
|
||||||
|
'user-add': UserAddIcon,
|
||||||
'user-switch': UserSwitch,
|
'user-switch': UserSwitch,
|
||||||
accessibility: AccessibilityIcon,
|
accessibility: AccessibilityIcon,
|
||||||
add: AddIcon,
|
add: AddIcon,
|
||||||
@@ -139,6 +143,8 @@ export const ICONS = {
|
|||||||
editor: EditorIcon,
|
editor: EditorIcon,
|
||||||
email: EmailIcon,
|
email: EmailIcon,
|
||||||
eye: EyeIcon,
|
eye: EyeIcon,
|
||||||
|
file: FileIcon,
|
||||||
|
folder: FolderIcon,
|
||||||
hashtag: HashtagIcon,
|
hashtag: HashtagIcon,
|
||||||
help: HelpIcon,
|
help: HelpIcon,
|
||||||
history: HistoryIcon,
|
history: HistoryIcon,
|
||||||
@@ -159,7 +165,6 @@ export const ICONS = {
|
|||||||
settings: SettingsIcon,
|
settings: SettingsIcon,
|
||||||
signIn: SignInIcon,
|
signIn: SignInIcon,
|
||||||
signOut: SignOutIcon,
|
signOut: SignOutIcon,
|
||||||
spellcheck: NotesIcon,
|
|
||||||
spreadsheets: SpreadsheetsIcon,
|
spreadsheets: SpreadsheetsIcon,
|
||||||
star: StarIcon,
|
star: StarIcon,
|
||||||
sync: SyncIcon,
|
sync: SyncIcon,
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const InputWithIcon: FunctionComponent<Props> = forwardRef(
|
|||||||
className={`pr-2 w-full border-0 focus:shadow-none ${
|
className={`pr-2 w-full border-0 focus:shadow-none ${
|
||||||
disabled ? DISABLED_CLASSNAME : ''
|
disabled ? DISABLED_CLASSNAME : ''
|
||||||
}`}
|
}`}
|
||||||
|
spellcheck={false}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type MenuProps = {
|
|||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
closeMenu?: () => void;
|
closeMenu?: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
initialFocus?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Menu: FunctionComponent<MenuProps> = ({
|
export const Menu: FunctionComponent<MenuProps> = ({
|
||||||
@@ -28,6 +29,7 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
|||||||
a11yLabel,
|
a11yLabel,
|
||||||
closeMenu,
|
closeMenu,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
initialFocus,
|
||||||
}: MenuProps) => {
|
}: MenuProps) => {
|
||||||
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useListKeyboardNavigation(menuElementRef);
|
useListKeyboardNavigation(menuElementRef, initialFocus);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && menuItemRefs.current.length > 0) {
|
if (isOpen && menuItemRefs.current.length > 0) {
|
||||||
@@ -73,8 +75,12 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
|||||||
child: ComponentChild,
|
child: ComponentChild,
|
||||||
index: number,
|
index: number,
|
||||||
array: ComponentChild[]
|
array: ComponentChild[]
|
||||||
) => {
|
): ComponentChild => {
|
||||||
if (!child) return;
|
if (!child || (Array.isArray(child) && child.length < 1)) return;
|
||||||
|
|
||||||
|
if (Array.isArray(child)) {
|
||||||
|
return child.map(mapMenuItems);
|
||||||
|
}
|
||||||
|
|
||||||
const _child = child as VNode<unknown>;
|
const _child = child as VNode<unknown>;
|
||||||
const isFirstMenuItem =
|
const isFirstMenuItem =
|
||||||
@@ -79,7 +79,7 @@ export const MenuItem: FunctionComponent<MenuItemProps> = forwardRef(
|
|||||||
<div
|
<div
|
||||||
className={`pseudo-radio-btn ${
|
className={`pseudo-radio-btn ${
|
||||||
checked ? 'pseudo-radio-btn--checked' : ''
|
checked ? 'pseudo-radio-btn--checked' : ''
|
||||||
} mr-2`}
|
} mr-2 flex-shrink-0`}
|
||||||
></div>
|
></div>
|
||||||
) : null}
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { SNTag } from '@standardnotes/snjs/dist/@types';
|
import { SNTag } from '@standardnotes/snjs';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -12,13 +12,10 @@ import {
|
|||||||
ComponentMutator,
|
ComponentMutator,
|
||||||
PayloadSource,
|
PayloadSource,
|
||||||
ComponentViewer,
|
ComponentViewer,
|
||||||
ComponentManagerEvent,
|
|
||||||
TransactionalMutation,
|
TransactionalMutation,
|
||||||
ItemMutator,
|
ItemMutator,
|
||||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||||
NoteViewController,
|
NoteViewController,
|
||||||
FeatureIdentifier,
|
|
||||||
FeatureStatus,
|
|
||||||
} from '@standardnotes/snjs';
|
} from '@standardnotes/snjs';
|
||||||
import { debounce, isDesktopApplication } from '@/utils';
|
import { debounce, isDesktopApplication } from '@/utils';
|
||||||
import { KeyboardModifier, KeyboardKey } from '@/services/ioService';
|
import { KeyboardModifier, KeyboardKey } from '@/services/ioService';
|
||||||
@@ -103,7 +100,6 @@ type State = {
|
|||||||
editorTitle: string;
|
editorTitle: string;
|
||||||
editorText: string;
|
editorText: string;
|
||||||
isDesktop?: boolean;
|
isDesktop?: boolean;
|
||||||
isEntitledToFiles: boolean;
|
|
||||||
lockText: string;
|
lockText: string;
|
||||||
marginResizersEnabled?: boolean;
|
marginResizersEnabled?: boolean;
|
||||||
monospaceFont?: boolean;
|
monospaceFont?: boolean;
|
||||||
@@ -172,9 +168,6 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
editorText: '',
|
editorText: '',
|
||||||
editorTitle: '',
|
editorTitle: '',
|
||||||
isDesktop: isDesktopApplication(),
|
isDesktop: isDesktopApplication(),
|
||||||
isEntitledToFiles:
|
|
||||||
this.application.features.getFeatureStatus(FeatureIdentifier.Files) ===
|
|
||||||
FeatureStatus.Entitled,
|
|
||||||
lockText: 'Note Editing Disabled',
|
lockText: 'Note Editing Disabled',
|
||||||
noteStatus: undefined,
|
noteStatus: undefined,
|
||||||
noteLocked: this.controller.note.locked,
|
noteLocked: this.controller.note.locked,
|
||||||
@@ -251,6 +244,15 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(_prevProps: Props, prevState: State): void {
|
||||||
|
if (
|
||||||
|
this.state.showProtectedWarning != undefined &&
|
||||||
|
prevState.showProtectedWarning !== this.state.showProtectedWarning
|
||||||
|
) {
|
||||||
|
this.reloadEditorComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onNoteInnerChange(note: SNNote, source: PayloadSource): void {
|
private onNoteInnerChange(note: SNNote, source: PayloadSource): void {
|
||||||
if (note.uuid !== this.note.uuid) {
|
if (note.uuid !== this.note.uuid) {
|
||||||
throw Error('Editor received changes for non-current note');
|
throw Error('Editor received changes for non-current note');
|
||||||
@@ -328,15 +330,6 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
/** @override */
|
/** @override */
|
||||||
async onAppEvent(eventName: ApplicationEvent) {
|
async onAppEvent(eventName: ApplicationEvent) {
|
||||||
switch (eventName) {
|
switch (eventName) {
|
||||||
case ApplicationEvent.FeaturesUpdated:
|
|
||||||
case ApplicationEvent.UserRolesChanged:
|
|
||||||
this.setState({
|
|
||||||
isEntitledToFiles:
|
|
||||||
this.application.features.getFeatureStatus(
|
|
||||||
FeatureIdentifier.Files
|
|
||||||
) === FeatureStatus.Entitled,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case ApplicationEvent.PreferencesChanged:
|
case ApplicationEvent.PreferencesChanged:
|
||||||
this.reloadPreferences();
|
this.reloadPreferences();
|
||||||
break;
|
break;
|
||||||
@@ -481,7 +474,24 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
this.reloadEditorComponent();
|
this.reloadEditorComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private destroyCurrentEditorComponent() {
|
||||||
|
const currentComponentViewer = this.state.editorComponentViewer;
|
||||||
|
if (currentComponentViewer) {
|
||||||
|
this.application.componentManager.destroyComponentViewer(
|
||||||
|
currentComponentViewer
|
||||||
|
);
|
||||||
|
this.setState({
|
||||||
|
editorComponentViewer: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async reloadEditorComponent() {
|
private async reloadEditorComponent() {
|
||||||
|
if (this.state.showProtectedWarning) {
|
||||||
|
this.destroyCurrentEditorComponent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newEditor = this.application.componentManager.editorForNote(
|
const newEditor = this.application.componentManager.editorForNote(
|
||||||
this.note
|
this.note
|
||||||
);
|
);
|
||||||
@@ -494,15 +504,9 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
if (currentComponentViewer?.componentUuid !== newEditor?.uuid) {
|
if (currentComponentViewer?.componentUuid !== newEditor?.uuid) {
|
||||||
if (currentComponentViewer) {
|
if (currentComponentViewer) {
|
||||||
this.application.componentManager.destroyComponentViewer(
|
this.destroyCurrentEditorComponent();
|
||||||
currentComponentViewer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (currentComponentViewer) {
|
|
||||||
this.setState({
|
|
||||||
editorComponentViewer: undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newEditor) {
|
if (newEditor) {
|
||||||
this.setState({
|
this.setState({
|
||||||
editorComponentViewer: this.createComponentViewer(newEditor),
|
editorComponentViewer: this.createComponentViewer(newEditor),
|
||||||
@@ -679,7 +683,7 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
performNoteDeletion(note: SNNote) {
|
performNoteDeletion(note: SNNote) {
|
||||||
this.application.deleteItem(note);
|
this.application.mutator.deleteItem(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
onPanelResizeFinish = async (
|
onPanelResizeFinish = async (
|
||||||
@@ -817,13 +821,13 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async disassociateComponentWithCurrentNote(component: SNComponent) {
|
async disassociateComponentWithCurrentNote(component: SNComponent) {
|
||||||
return this.application.runTransactionalMutation(
|
return this.application.mutator.runTransactionalMutation(
|
||||||
transactionForDisassociateComponentWithCurrentNote(component, this.note)
|
transactionForDisassociateComponentWithCurrentNote(component, this.note)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async associateComponentWithCurrentNote(component: SNComponent) {
|
async associateComponentWithCurrentNote(component: SNComponent) {
|
||||||
return this.application.runTransactionalMutation(
|
return this.application.mutator.runTransactionalMutation(
|
||||||
transactionForAssociateComponentWithCurrentNote(component, this.note)
|
transactionForAssociateComponentWithCurrentNote(component, this.note)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1043,18 +1047,17 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.isEntitledToFiles &&
|
{window.enabledUnfinishedFeatures && (
|
||||||
window.enabledUnfinishedFeatures && (
|
<div className="mr-3">
|
||||||
<div className="mr-3">
|
<AttachedFilesButton
|
||||||
<AttachedFilesButton
|
application={this.application}
|
||||||
application={this.application}
|
appState={this.appState}
|
||||||
appState={this.appState}
|
onClickPreprocessing={
|
||||||
onClickPreprocessing={
|
this.ensureNoteIsInsertedBeforeUIAction
|
||||||
this.ensureNoteIsInsertedBeforeUIAction
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
<div className="mr-3">
|
<div className="mr-3">
|
||||||
<ChangeEditorButton
|
<ChangeEditorButton
|
||||||
application={this.application}
|
application={this.application}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { WebApplication } from '@/ui_models/application';
|
|||||||
import { CollectionSort, PrefKey } from '@standardnotes/snjs';
|
import { CollectionSort, PrefKey } from '@standardnotes/snjs';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { useRef, useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Menu } from './menu/Menu';
|
import { Menu } from './Menu/Menu';
|
||||||
import { MenuItem, MenuItemSeparator, MenuItemType } from './menu/MenuItem';
|
import { MenuItem, MenuItemSeparator, MenuItemType } from './Menu/MenuItem';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ChangeEditorOption } from './ChangeEditorOption';
|
|||||||
import { BYTES_IN_ONE_MEGABYTE } from '@/constants';
|
import { BYTES_IN_ONE_MEGABYTE } from '@/constants';
|
||||||
import { ListedActionsOption } from './ListedActionsOption';
|
import { ListedActionsOption } from './ListedActionsOption';
|
||||||
import { AddTagOption } from './AddTagOption';
|
import { AddTagOption } from './AddTagOption';
|
||||||
|
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit';
|
||||||
|
|
||||||
export type NotesOptionsProps = {
|
export type NotesOptionsProps = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
@@ -160,7 +161,7 @@ const SpellcheckOptions: FunctionComponent<{
|
|||||||
disabled={!spellcheckControllable}
|
disabled={!spellcheckControllable}
|
||||||
>
|
>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Icon type="spellcheck" className={iconClass} />
|
<Icon type="notes" className={iconClass} />
|
||||||
Spellcheck
|
Spellcheck
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -236,23 +237,44 @@ export const NotesOptions = observer(
|
|||||||
};
|
};
|
||||||
}, [application]);
|
}, [application]);
|
||||||
|
|
||||||
const downloadSelectedItems = () => {
|
const getNoteFileName = (note: SNNote): string => {
|
||||||
notes.forEach((note) => {
|
const editor = application.componentManager.editorForNote(note);
|
||||||
const editor = application.componentManager.editorForNote(note);
|
const format = editor?.package_info?.file_type || 'txt';
|
||||||
const format = editor?.package_info?.file_type || 'txt';
|
return `${note.title}.${format}`;
|
||||||
const downloadAnchor = document.createElement('a');
|
};
|
||||||
downloadAnchor.setAttribute(
|
|
||||||
'href',
|
const downloadSelectedItems = async () => {
|
||||||
'data:text/plain;charset=utf-8,' + encodeURIComponent(note.text)
|
if (notes.length === 1) {
|
||||||
|
application
|
||||||
|
.getArchiveService()
|
||||||
|
.downloadData(new Blob([notes[0].text]), getNoteFileName(notes[0]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.length > 1) {
|
||||||
|
const loadingToastId = addToast({
|
||||||
|
type: ToastType.Loading,
|
||||||
|
message: `Exporting ${notes.length} notes...`,
|
||||||
|
});
|
||||||
|
await application.getArchiveService().downloadDataAsZip(
|
||||||
|
notes.map((note) => {
|
||||||
|
return {
|
||||||
|
filename: getNoteFileName(note),
|
||||||
|
content: new Blob([note.text]),
|
||||||
|
};
|
||||||
|
})
|
||||||
);
|
);
|
||||||
downloadAnchor.setAttribute('download', `${note.title}.${format}`);
|
dismissToast(loadingToastId);
|
||||||
downloadAnchor.click();
|
addToast({
|
||||||
});
|
type: ToastType.Success,
|
||||||
|
message: `Exported ${notes.length} notes`,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateSelectedItems = () => {
|
const duplicateSelectedItems = () => {
|
||||||
notes.forEach((note) => {
|
notes.forEach((note) => {
|
||||||
application.duplicateItem(note);
|
application.mutator.duplicateItem(note);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Icon } from '@/components/Icon';
|
import { Icon } from '@/components/Icon';
|
||||||
import { Menu } from '@/components/menu/Menu';
|
import { Menu } from '@/components/Menu/Menu';
|
||||||
import { MenuItem, MenuItemType } from '@/components/menu/MenuItem';
|
import { MenuItem, MenuItemType } from '@/components/Menu/MenuItem';
|
||||||
import {
|
import {
|
||||||
reloadFont,
|
reloadFont,
|
||||||
transactionForAssociateComponentWithCurrentNote,
|
transactionForAssociateComponentWithCurrentNote,
|
||||||
@@ -86,7 +86,7 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
) => {
|
) => {
|
||||||
if (component) {
|
if (component) {
|
||||||
if (component.conflictOf) {
|
if (component.conflictOf) {
|
||||||
application.changeAndSaveItem(component.uuid, (mutator) => {
|
application.mutator.changeAndSaveItem(component.uuid, (mutator) => {
|
||||||
mutator.conflictOf = undefined;
|
mutator.conflictOf = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await application.runTransactionalMutations(transactions);
|
await application.mutator.runTransactionalMutations(transactions);
|
||||||
/** Dirtying can happen above */
|
/** Dirtying can happen above */
|
||||||
application.sync.sync();
|
application.sync.sync();
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export const NotesView: FunctionComponent<Props> = observer(
|
|||||||
onKeyUp={onNoteFilterKeyUp}
|
onKeyUp={onNoteFilterKeyUp}
|
||||||
onFocus={onSearchFocused}
|
onFocus={onSearchFocused}
|
||||||
onBlur={onSearchBlurred}
|
onBlur={onSearchBlurred}
|
||||||
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
{noteFilterText && (
|
{noteFilterText && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from 'preact/hooks';
|
import { useRef } from 'preact/hooks';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { action, makeAutoObservable, observable } from 'mobx';
|
import { action, makeAutoObservable, observable } from 'mobx';
|
||||||
import { ExtensionsLatestVersions } from '@/preferences/panes/extensions-segments';
|
import { ExtensionsLatestVersions } from '@/components/Preferences/panes/extensions-segments';
|
||||||
import {
|
import {
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
ContentType,
|
ContentType,
|
||||||
@@ -95,7 +95,9 @@ export class PreferencesMenu {
|
|||||||
|
|
||||||
private loadLatestVersions(): void {
|
private loadLatestVersions(): void {
|
||||||
ExtensionsLatestVersions.load(this.application).then((versions) => {
|
ExtensionsLatestVersions.load(this.application).then((versions) => {
|
||||||
this._extensionLatestVersions = versions;
|
if (versions) {
|
||||||
|
this._extensionLatestVersions = versions;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@ export class PreferencesMenu {
|
|||||||
FeatureIdentifier.CloudLink,
|
FeatureIdentifier.CloudLink,
|
||||||
];
|
];
|
||||||
this._extensionPanes = (
|
this._extensionPanes = (
|
||||||
this.application.getItems([
|
this.application.items.getItems([
|
||||||
ContentType.ActionsExtension,
|
ContentType.ActionsExtension,
|
||||||
ContentType.Component,
|
ContentType.Component,
|
||||||
ContentType.Theme,
|
ContentType.Theme,
|
||||||
@@ -17,7 +17,7 @@ import { MfaProps } from './panes/two-factor-auth/MfaProps';
|
|||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { useEffect, useMemo } from 'preact/hooks';
|
import { useEffect, useMemo } from 'preact/hooks';
|
||||||
import { ExtensionPane } from './panes/ExtensionPane';
|
import { ExtensionPane } from './panes/ExtensionPane';
|
||||||
import { Backups } from '@/preferences/panes/Backups';
|
import { Backups } from '@/components/Preferences/panes/Backups';
|
||||||
import { Appearance } from './panes/Appearance';
|
import { Appearance } from './panes/Appearance';
|
||||||
|
|
||||||
interface PreferencesProps extends MfaProps {
|
interface PreferencesProps extends MfaProps {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
|
|
||||||
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
|
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
|
||||||
index,
|
index,
|
||||||
@@ -5,8 +5,7 @@ export const PreferencesPane: FunctionComponent = ({ children }) => (
|
|||||||
<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
|
? children.filter((child) => child != undefined)
|
||||||
.filter((child) => child != undefined)
|
|
||||||
: children}
|
: children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes?: string;
|
||||||
|
};
|
||||||
|
export const PreferencesSegment: FunctionComponent<Props> = ({
|
||||||
|
children,
|
||||||
|
classes = '',
|
||||||
|
}) => <div className={`flex flex-col ${classes}`}>{children}</div>;
|
||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
Credentials,
|
Credentials,
|
||||||
SignOutWrapper,
|
SignOutWrapper,
|
||||||
Authentication,
|
Authentication,
|
||||||
} from '@/preferences/panes/account';
|
} from '@/components/Preferences/panes/account';
|
||||||
import { PreferencesPane } from '@/preferences/components';
|
import { PreferencesPane } from '@/components/Preferences/components';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Dropdown, DropdownItem } from '@/components/Dropdown';
|
import { Dropdown, DropdownItem } from '@/components/Dropdown';
|
||||||
import { usePremiumModal } from '@/components/Premium';
|
import { usePremiumModal } from '@/components/Premium';
|
||||||
import { sortThemes } from '@/components/QuickSettingsMenu/QuickSettingsMenu';
|
import { sortThemes } from '@/components/QuickSettingsMenu/QuickSettingsMenu';
|
||||||
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 '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { GetFeatures } from '@standardnotes/features';
|
import { GetFeatures } from '@standardnotes/features';
|
||||||
@@ -62,7 +62,7 @@ export const Appearance: FunctionComponent<Props> = observer(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const themesAsItems: DropdownItem[] = (
|
const themesAsItems: DropdownItem[] = (
|
||||||
application.getDisplayableItems(ContentType.Theme) as SNTheme[]
|
application.items.getDisplayableItems(ContentType.Theme) as SNTheme[]
|
||||||
)
|
)
|
||||||
.filter((theme) => !theme.isLayerable())
|
.filter((theme) => !theme.isLayerable())
|
||||||
.sort(sortThemes)
|
.sort(sortThemes)
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { PreferencesGroup, PreferencesSegment } from '@/preferences/components';
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
} from '@/components/Preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { ComponentViewer, SNComponent } from '@standardnotes/snjs';
|
import { ComponentViewer, SNComponent } from '@standardnotes/snjs';
|
||||||
import { FeatureIdentifier } from '@standardnotes/features';
|
import { FeatureIdentifier } from '@standardnotes/features';
|
||||||
@@ -7,7 +10,7 @@ import { FunctionComponent } from 'preact';
|
|||||||
import { ExtensionItem } from './extensions-segments';
|
import { ExtensionItem } from './extensions-segments';
|
||||||
import { ComponentView } from '@/components/ComponentView';
|
import { ComponentView } from '@/components/ComponentView';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { PreferencesMenu } from '@/preferences/PreferencesMenu';
|
import { PreferencesMenu } from '@/components/Preferences/PreferencesMenu';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -54,7 +57,7 @@ export const ExtensionPane: FunctionComponent<IProps> = observer(
|
|||||||
extension={extension}
|
extension={extension}
|
||||||
first={false}
|
first={false}
|
||||||
uninstall={() =>
|
uninstall={() =>
|
||||||
application
|
application.mutator
|
||||||
.deleteItem(extension)
|
.deleteItem(extension)
|
||||||
.then(() => preferencesMenu.loadExtensionsPanes())
|
.then(() => preferencesMenu.loadExtensionsPanes())
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
|||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
const loadExtensions = (application: WebApplication) =>
|
const loadExtensions = (application: WebApplication) =>
|
||||||
application.getItems(
|
application.items.getItems(
|
||||||
[ContentType.ActionsExtension, ContentType.Component, ContentType.Theme],
|
[ContentType.ActionsExtension, ContentType.Component, ContentType.Theme],
|
||||||
true
|
true
|
||||||
) as SNComponent[];
|
) as SNComponent[];
|
||||||
@@ -48,7 +48,7 @@ export const Extensions: FunctionComponent<{
|
|||||||
)
|
)
|
||||||
.then(async (shouldRemove: boolean) => {
|
.then(async (shouldRemove: boolean) => {
|
||||||
if (shouldRemove) {
|
if (shouldRemove) {
|
||||||
await application.deleteItem(extension);
|
await application.mutator.deleteItem(extension);
|
||||||
setExtensions(loadExtensions(application));
|
setExtensions(loadExtensions(application));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -73,7 +73,7 @@ export const Extensions: FunctionComponent<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmExtension = async () => {
|
const confirmExtension = async () => {
|
||||||
await application.insertItem(confirmableExtension as SNComponent);
|
await application.mutator.insertItem(confirmableExtension as SNComponent);
|
||||||
application.sync.sync();
|
application.sync.sync();
|
||||||
setExtensions(loadExtensions(application));
|
setExtensions(loadExtensions(application));
|
||||||
};
|
};
|
||||||
@@ -2,9 +2,9 @@ import { WebApplication } from '@/ui_models/application';
|
|||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { PreferencesPane } from '../components';
|
import { PreferencesPane } from '../components';
|
||||||
import { ErrorReporting, Tools, Defaults, LabsPane } from './general-segments';
|
import { Tools, Defaults, LabsPane } from './general-segments';
|
||||||
import { ExtensionsLatestVersions } from '@/preferences/panes/extensions-segments';
|
import { ExtensionsLatestVersions } from '@/components/Preferences/panes/extensions-segments';
|
||||||
import { Advanced } from '@/preferences/panes/account';
|
import { Advanced } from '@/components/Preferences/panes/account';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
interface GeneralProps {
|
interface GeneralProps {
|
||||||
@@ -18,7 +18,6 @@ export const General: FunctionComponent<GeneralProps> = observer(
|
|||||||
<PreferencesPane>
|
<PreferencesPane>
|
||||||
<Tools application={application} />
|
<Tools application={application} />
|
||||||
<Defaults application={application} />
|
<Defaults application={application} />
|
||||||
<ErrorReporting appState={appState} />
|
|
||||||
<LabsPane application={application} />
|
<LabsPane application={application} />
|
||||||
<Advanced
|
<Advanced
|
||||||
application={application}
|
application={application}
|
||||||
@@ -104,7 +104,11 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
|||||||
<Text>
|
<Text>
|
||||||
Send an email to help@standardnotes.com and we’ll sort it out.
|
Send an email to help@standardnotes.com and we’ll sort it out.
|
||||||
</Text>
|
</Text>
|
||||||
<LinkButton className="mt-3" link="mailto: help@standardnotes.com" label="Email us" />
|
<LinkButton
|
||||||
|
className="mt-3"
|
||||||
|
link="mailto: help@standardnotes.com"
|
||||||
|
label="Email us"
|
||||||
|
/>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
</PreferencesPane>
|
</PreferencesPane>
|
||||||
@@ -105,7 +105,7 @@ export const Listed = observer(({ application }: Props) => {
|
|||||||
type="normal"
|
type="normal"
|
||||||
disabled={requestingAccount}
|
disabled={requestingAccount}
|
||||||
label={
|
label={
|
||||||
requestingAccount ? 'Creating account...' : 'Create New Author'
|
requestingAccount ? 'Creating account...' : 'Create new author'
|
||||||
}
|
}
|
||||||
onClick={registerNewAccount}
|
onClick={registerNewAccount}
|
||||||
/>
|
/>
|
||||||
@@ -3,6 +3,7 @@ import { AppState } from '@/ui_models/app_state';
|
|||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { PreferencesPane } from '../components';
|
import { PreferencesPane } from '../components';
|
||||||
import { Encryption, PasscodeLock, Protections } from './security-segments';
|
import { Encryption, PasscodeLock, Protections } from './security-segments';
|
||||||
|
import { Privacy } from './security-segments/Privacy';
|
||||||
import { TwoFactorAuthWrapper } from './two-factor-auth';
|
import { TwoFactorAuthWrapper } from './two-factor-auth';
|
||||||
import { MfaProps } from './two-factor-auth/MfaProps';
|
import { MfaProps } from './two-factor-auth/MfaProps';
|
||||||
|
|
||||||
@@ -20,5 +21,6 @@ export const Security: FunctionComponent<SecurityProps> = (props) => (
|
|||||||
userProvider={props.userProvider}
|
userProvider={props.userProvider}
|
||||||
/>
|
/>
|
||||||
<PasscodeLock appState={props.appState} application={props.application} />
|
<PasscodeLock appState={props.appState} application={props.application} />
|
||||||
|
<Privacy application={props.application} />
|
||||||
</PreferencesPane>
|
</PreferencesPane>
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { FunctionalComponent } from 'preact';
|
||||||
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
} from '@/components/Preferences/components';
|
||||||
|
import { OfflineSubscription } from '@/components/Preferences/panes/account/offlineSubscription';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { Extensions } from '@/components/Preferences/panes/Extensions';
|
||||||
|
import { ExtensionsLatestVersions } from '@/components/Preferences/panes/extensions-segments';
|
||||||
|
import { AccordionItem } from '@/components/Shared/AccordionItem';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
application: WebApplication;
|
||||||
|
appState: AppState;
|
||||||
|
extensionsLatestVersions: ExtensionsLatestVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Advanced: FunctionalComponent<IProps> = observer(
|
||||||
|
({ application, appState, extensionsLatestVersions }) => {
|
||||||
|
return (
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<AccordionItem title={'Advanced Settings'}>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex-grow flex flex-col">
|
||||||
|
<OfflineSubscription
|
||||||
|
application={application}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
<Extensions
|
||||||
|
className={'mt-3'}
|
||||||
|
application={application}
|
||||||
|
extensionsLatestVersions={extensionsLatestVersions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
PreferencesSegment,
|
PreferencesSegment,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
@@ -4,14 +4,14 @@ import {
|
|||||||
Subtitle,
|
Subtitle,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { observer } from '@node_modules/mobx-react-lite';
|
import { observer } from '@node_modules/mobx-react-lite';
|
||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
import { dateToLocalizedString } from '@standardnotes/snjs';
|
import { dateToLocalizedString } from '@standardnotes/snjs';
|
||||||
import { useCallback, useState } from 'preact/hooks';
|
import { useCallback, useState } from 'preact/hooks';
|
||||||
import { ChangeEmail } from '@/preferences/panes/account/changeEmail';
|
import { ChangeEmail } from '@/components/Preferences/panes/account/changeEmail';
|
||||||
import { FunctionComponent, render } from 'preact';
|
import { FunctionComponent, render } from 'preact';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { PasswordWizard } from '@/components/PasswordWizard';
|
import { PasswordWizard } from '@/components/PasswordWizard';
|
||||||
@@ -45,7 +45,8 @@ export const Credentials: FunctionComponent<Props> = observer(
|
|||||||
<Title>Credentials</Title>
|
<Title>Credentials</Title>
|
||||||
<Subtitle>Email</Subtitle>
|
<Subtitle>Email</Subtitle>
|
||||||
<Text>
|
<Text>
|
||||||
You're signed in as <span className="font-bold">{user?.email}</span>
|
You're signed in as{' '}
|
||||||
|
<span className="font-bold wrap">{user?.email}</span>
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
className="min-w-20 mt-3"
|
className="min-w-20 mt-3"
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Subtitle,
|
Subtitle,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
@@ -41,12 +41,15 @@ const SignOutView: FunctionComponent<{
|
|||||||
</div>
|
</div>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Subtitle>This device</Subtitle>
|
<Subtitle>This workspace</Subtitle>
|
||||||
<Text>This will delete all local items and preferences.</Text>
|
<Text>
|
||||||
|
Remove all data related to the current workspace from the
|
||||||
|
application.
|
||||||
|
</Text>
|
||||||
<div className="min-h-3" />
|
<div className="min-h-3" />
|
||||||
<Button
|
<Button
|
||||||
type="danger"
|
type="danger"
|
||||||
label="Sign out and clear local data"
|
label="Sign out workspace"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
appState.accountMenu.setSigningOut(true);
|
appState.accountMenu.setSigningOut(true);
|
||||||
}}
|
}}
|
||||||
@@ -67,12 +70,14 @@ const ClearSessionDataView: FunctionComponent<{
|
|||||||
return (
|
return (
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>Clear session data</Title>
|
<Title>Clear workspace</Title>
|
||||||
<Text>This will delete all local items and preferences.</Text>
|
<Text>
|
||||||
|
Remove all data related to the current workspace from the application.
|
||||||
|
</Text>
|
||||||
<div className="min-h-3" />
|
<div className="min-h-3" />
|
||||||
<Button
|
<Button
|
||||||
type="danger"
|
type="danger"
|
||||||
label="Clear Session Data"
|
label="Clear workspace"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
appState.accountMenu.setSigningOut(true);
|
appState.accountMenu.setSigningOut(true);
|
||||||
}}
|
}}
|
||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
PreferencesSegment,
|
PreferencesSegment,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/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';
|
||||||
@@ -3,9 +3,12 @@ import { FunctionalComponent } from 'preact';
|
|||||||
export const ChangeEmailSuccess: FunctionalComponent = () => {
|
export const ChangeEmailSuccess: FunctionalComponent = () => {
|
||||||
return (
|
return (
|
||||||
<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 ensure maximum compatibility.
|
Please ensure you are running the latest version of Standard Notes on
|
||||||
|
all platforms to ensure maximum compatibility.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
ModalDialogButtons,
|
ModalDialogButtons,
|
||||||
ModalDialogDescription,
|
ModalDialogDescription,
|
||||||
ModalDialogLabel,
|
ModalDialogLabel,
|
||||||
} from '@/components/shared/ModalDialog';
|
} from '@/components/Shared/ModalDialog';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { FunctionalComponent } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FunctionalComponent } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
import { Subtitle } from '@/preferences/components';
|
import { Subtitle } from '@/components/Preferences/components';
|
||||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { JSXInternal } from '@node_modules/preact/src/jsx';
|
import { JSXInternal } from '@node_modules/preact/src/jsx';
|
||||||
@@ -9,8 +9,8 @@ import { WebApplication } from '@/ui_models/application';
|
|||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { STRING_REMOVE_OFFLINE_KEY_CONFIRMATION } from '@/strings';
|
import { STRING_REMOVE_OFFLINE_KEY_CONFIRMATION } from '@/strings';
|
||||||
import { ButtonType } from '@standardnotes/snjs';
|
import { ButtonType, ClientDisplayableError } from '@standardnotes/snjs';
|
||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
@@ -49,8 +49,8 @@ export const OfflineSubscription: FunctionalComponent<IProps> = observer(
|
|||||||
activationCode
|
activationCode
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result?.error) {
|
if (result instanceof ClientDisplayableError) {
|
||||||
await application.alertService.alert(result.error);
|
await application.alertService.alert(result.text);
|
||||||
} else {
|
} else {
|
||||||
setIsSuccessfullyActivated(true);
|
setIsSuccessfullyActivated(true);
|
||||||
setHasUserPreviouslyStoredCode(true);
|
setHasUserPreviouslyStoredCode(true);
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { FunctionalComponent } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
import { LinkButton, Text } from '@/preferences/components';
|
import { LinkButton, Text } from '@/components/Preferences/components';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { loadPurchaseFlowUrl } from '@/purchaseFlow/PurchaseFlowWrapper';
|
import { loadPurchaseFlowUrl } from '@/components/PurchaseFlow/PurchaseFlowWrapper';
|
||||||
|
|
||||||
export const NoSubscription: FunctionalComponent<{
|
export const NoSubscription: FunctionalComponent<{
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
PreferencesGroup,
|
PreferencesGroup,
|
||||||
PreferencesSegment,
|
PreferencesSegment,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { SubscriptionInformation } from './SubscriptionInformation';
|
import { SubscriptionInformation } from './SubscriptionInformation';
|
||||||
import { NoSubscription } from './NoSubscription';
|
import { NoSubscription } from './NoSubscription';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { SubscriptionState } from '../../../../ui_models/app_state/subscription_state';
|
import { SubscriptionState } from '../../../../../ui_models/app_state/subscription_state';
|
||||||
import { Text } from '@/preferences/components';
|
import { Text } from '@/components/Preferences/components';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { openSubscriptionDashboard } from '@/hooks/manageSubscription';
|
import { openSubscriptionDashboard } from '@/hooks/manageSubscription';
|
||||||
@@ -95,7 +95,7 @@ export const DataBackups = observer(({ application, appState }: Props) => {
|
|||||||
const performImport = async (data: BackupFile) => {
|
const performImport = async (data: BackupFile) => {
|
||||||
setIsImportDataLoading(true);
|
setIsImportDataLoading(true);
|
||||||
|
|
||||||
const result = await application.importData(data);
|
const result = await application.mutator.importData(data);
|
||||||
|
|
||||||
setIsImportDataLoading(false);
|
setIsImportDataLoading(false);
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ export const DataBackups = observer(({ application, appState }: Props) => {
|
|||||||
|
|
||||||
let statusText = STRING_IMPORT_SUCCESS;
|
let statusText = STRING_IMPORT_SUCCESS;
|
||||||
if ('error' in result) {
|
if ('error' in result) {
|
||||||
statusText = result.error;
|
statusText = result.error.text;
|
||||||
} else if (result.errorCount) {
|
} else if (result.errorCount) {
|
||||||
statusText = StringImportError(result.errorCount);
|
statusText = StringImportError(result.errorCount);
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ export const DataBackups = observer(({ application, appState }: Props) => {
|
|||||||
<div class="flex flex-row items-center mt-3">
|
<div class="flex flex-row items-center mt-3">
|
||||||
<Button
|
<Button
|
||||||
type="normal"
|
type="normal"
|
||||||
label="Import Backup"
|
label="Import backup"
|
||||||
onClick={handleImportFile}
|
onClick={handleImportFile}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -13,10 +13,14 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
import { EmailBackupFrequency, SettingName } from '@standardnotes/settings';
|
import {
|
||||||
|
EmailBackupFrequency,
|
||||||
|
MuteFailedBackupsEmailsOption,
|
||||||
|
SettingName,
|
||||||
|
} from '@standardnotes/settings';
|
||||||
import { Dropdown, DropdownItem } from '@/components/Dropdown';
|
import { Dropdown, DropdownItem } from '@/components/Dropdown';
|
||||||
import { Switch } from '@/components/Switch';
|
import { Switch } from '@/components/Switch';
|
||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
import { FeatureIdentifier } from '@standardnotes/features';
|
import { FeatureIdentifier } from '@standardnotes/features';
|
||||||
import { FeatureStatus } from '@standardnotes/snjs';
|
import { FeatureStatus } from '@standardnotes/snjs';
|
||||||
|
|
||||||
@@ -44,14 +48,19 @@ export const EmailBackups = observer(({ application }: Props) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userSettings = await application.listSettings();
|
const userSettings = await application.settings.listSettings();
|
||||||
setEmailFrequency(
|
setEmailFrequency(
|
||||||
(userSettings.EMAIL_BACKUP_FREQUENCY ||
|
userSettings.getSettingValue<EmailBackupFrequency>(
|
||||||
EmailBackupFrequency.Disabled) as EmailBackupFrequency
|
SettingName.EmailBackupFrequency,
|
||||||
|
EmailBackupFrequency.Disabled
|
||||||
|
)
|
||||||
);
|
);
|
||||||
setIsFailedBackupEmailMuted(
|
setIsFailedBackupEmailMuted(
|
||||||
convertStringifiedBooleanToBoolean(
|
convertStringifiedBooleanToBoolean(
|
||||||
userSettings[SettingName.MuteFailedBackupsEmails] as string
|
userSettings.getSettingValue<MuteFailedBackupsEmailsOption>(
|
||||||
|
SettingName.MuteFailedBackupsEmails,
|
||||||
|
MuteFailedBackupsEmailsOption.NotMuted
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -75,7 +84,10 @@ export const EmailBackups = observer(({ application }: Props) => {
|
|||||||
EmailBackupFrequency[frequency as keyof typeof EmailBackupFrequency];
|
EmailBackupFrequency[frequency as keyof typeof EmailBackupFrequency];
|
||||||
frequencyOptions.push({
|
frequencyOptions.push({
|
||||||
value: frequencyValue,
|
value: frequencyValue,
|
||||||
label: application.getEmailBackupFrequencyOptionLabel(frequencyValue),
|
label:
|
||||||
|
application.settings.getEmailBackupFrequencyOptionLabel(
|
||||||
|
frequencyValue
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setEmailFrequencyOptions(frequencyOptions);
|
setEmailFrequencyOptions(frequencyOptions);
|
||||||
@@ -88,7 +100,7 @@ export const EmailBackups = observer(({ application }: Props) => {
|
|||||||
payload: string
|
payload: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
await application.updateSetting(settingName, payload);
|
await application.settings.updateSetting(settingName, payload, false);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
application.alertService.alert(STRING_FAILED_TO_UPDATE_USER_SETTING);
|
application.alertService.alert(STRING_FAILED_TO_UPDATE_USER_SETTING);
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
import { ButtonType, SettingName } from '@standardnotes/snjs';
|
import { ButtonType, SettingName } from '@standardnotes/snjs';
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +9,7 @@ import {
|
|||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { isDev, openInNewTab } from '@/utils';
|
import { isDev, openInNewTab } from '@/utils';
|
||||||
import { Subtitle } from '@/preferences/components';
|
import { Subtitle } from '@/components/Preferences/components';
|
||||||
import { KeyboardKey } from '@Services/ioService';
|
import { KeyboardKey } from '@Services/ioService';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
|
|
||||||
@@ -27,7 +26,9 @@ export const CloudBackupProvider: FunctionComponent<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [authBegan, setAuthBegan] = useState(false);
|
const [authBegan, setAuthBegan] = useState(false);
|
||||||
const [successfullyInstalled, setSuccessfullyInstalled] = useState(false);
|
const [successfullyInstalled, setSuccessfullyInstalled] = useState(false);
|
||||||
const [backupFrequency, setBackupFrequency] = useState<string | null>(null);
|
const [backupFrequency, setBackupFrequency] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
const [confirmation, setConfirmation] = useState('');
|
const [confirmation, setConfirmation] = useState('');
|
||||||
|
|
||||||
const disable = async (event: Event) => {
|
const disable = async (event: Event) => {
|
||||||
@@ -42,10 +43,10 @@ export const CloudBackupProvider: FunctionComponent<Props> = ({
|
|||||||
'Cancel'
|
'Cancel'
|
||||||
);
|
);
|
||||||
if (shouldDisable) {
|
if (shouldDisable) {
|
||||||
await application.deleteSetting(backupFrequencySettingName);
|
await application.settings.deleteSetting(backupFrequencySettingName);
|
||||||
await application.deleteSetting(backupTokenSettingName);
|
await application.settings.deleteSetting(backupTokenSettingName);
|
||||||
|
|
||||||
setBackupFrequency(null);
|
setBackupFrequency(undefined);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
application.alertService.alert(error as string);
|
application.alertService.alert(error as string);
|
||||||
@@ -66,7 +67,7 @@ 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.updateSetting(
|
await application.settings.updateSetting(
|
||||||
backupFrequencySettingName,
|
backupFrequencySettingName,
|
||||||
backupFrequency as string
|
backupFrequency as string
|
||||||
);
|
);
|
||||||
@@ -134,11 +135,11 @@ export const CloudBackupProvider: FunctionComponent<Props> = ({
|
|||||||
if (!cloudProviderToken) {
|
if (!cloudProviderToken) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
await application.updateSetting(
|
await application.settings.updateSetting(
|
||||||
backupTokenSettingName,
|
backupTokenSettingName,
|
||||||
cloudProviderToken
|
cloudProviderToken
|
||||||
);
|
);
|
||||||
await application.updateSetting(
|
await application.settings.updateSetting(
|
||||||
backupFrequencySettingName,
|
backupFrequencySettingName,
|
||||||
defaultBackupFrequency
|
defaultBackupFrequency
|
||||||
);
|
);
|
||||||
@@ -166,7 +167,9 @@ export const CloudBackupProvider: FunctionComponent<Props> = ({
|
|||||||
if (!application.getUser()) {
|
if (!application.getUser()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const frequency = await application.getSetting(backupFrequencySettingName);
|
const frequency = await application.settings.getSetting(
|
||||||
|
backupFrequencySettingName
|
||||||
|
);
|
||||||
setBackupFrequency(frequency);
|
setBackupFrequency(frequency);
|
||||||
}, [application, backupFrequencySettingName]);
|
}, [application, backupFrequencySettingName]);
|
||||||
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { CloudBackupProvider } from './CloudBackupProvider';
|
import { CloudBackupProvider } from './CloudBackupProvider';
|
||||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
@@ -8,26 +7,24 @@ import {
|
|||||||
Subtitle,
|
Subtitle,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
import { FeatureIdentifier } from '@standardnotes/features';
|
import { FeatureIdentifier } from '@standardnotes/features';
|
||||||
import { FeatureStatus } from '@standardnotes/snjs';
|
import { FeatureStatus } from '@standardnotes/snjs';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { CloudProvider, SettingName } from '@standardnotes/settings';
|
import {
|
||||||
|
CloudProvider,
|
||||||
|
MuteFailedCloudBackupsEmailsOption,
|
||||||
|
SettingName,
|
||||||
|
} from '@standardnotes/settings';
|
||||||
import { Switch } from '@/components/Switch';
|
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.Dropbox,
|
{ name: CloudProvider.Google },
|
||||||
},
|
{ name: CloudProvider.OneDrive },
|
||||||
{
|
|
||||||
name: CloudProvider.Google,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: CloudProvider.OneDrive,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -51,10 +48,13 @@ export const CloudLink: FunctionComponent<Props> = ({ application }) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userSettings = await application.listSettings();
|
const userSettings = await application.settings.listSettings();
|
||||||
setIsFailedCloudBackupEmailMuted(
|
setIsFailedCloudBackupEmailMuted(
|
||||||
convertStringifiedBooleanToBoolean(
|
convertStringifiedBooleanToBoolean(
|
||||||
userSettings[SettingName.MuteFailedCloudBackupsEmails] as string
|
userSettings.getSettingValue(
|
||||||
|
SettingName.MuteFailedCloudBackupsEmails,
|
||||||
|
MuteFailedCloudBackupsEmailsOption.NotMuted
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -89,7 +89,7 @@ export const CloudLink: FunctionComponent<Props> = ({ application }) => {
|
|||||||
payload: string
|
payload: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
await application.updateSetting(settingName, payload);
|
await application.settings.updateSetting(settingName, payload);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
application.alertService.alert(STRING_FAILED_TO_UPDATE_USER_SETTING);
|
application.alertService.alert(STRING_FAILED_TO_UPDATE_USER_SETTING);
|
||||||
@@ -1,42 +1,36 @@
|
|||||||
import { displayStringForContentType, SNComponent } from '@standardnotes/snjs';
|
import { displayStringForContentType, SNComponent } from '@standardnotes/snjs';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import {
|
import { Title, Text, Subtitle, PreferencesSegment } from '../../components';
|
||||||
Title,
|
|
||||||
Text,
|
|
||||||
Subtitle,
|
|
||||||
PreferencesSegment,
|
|
||||||
} from '../../components';
|
|
||||||
|
|
||||||
export const ConfirmCustomExtension: FunctionComponent<{
|
export const ConfirmCustomExtension: FunctionComponent<{
|
||||||
component: SNComponent,
|
component: SNComponent;
|
||||||
callback: (confirmed: boolean) => void
|
callback: (confirmed: boolean) => void;
|
||||||
}> = ({ component, callback }) => {
|
}> = ({ component, callback }) => {
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
{
|
{
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
value: component.package_info.name
|
value: component.package_info.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Description',
|
label: 'Description',
|
||||||
value: component.package_info.description
|
value: component.package_info.description,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Version',
|
label: 'Version',
|
||||||
value: component.package_info.version
|
value: component.package_info.version,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Hosted URL',
|
label: 'Hosted URL',
|
||||||
value: component.thirdPartyPackageInfo.url
|
value: component.thirdPartyPackageInfo.url,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Download URL',
|
label: 'Download URL',
|
||||||
value: component.package_info.download_url
|
value: component.package_info.download_url,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Extension Type',
|
label: 'Extension Type',
|
||||||
value: displayStringForContentType(component.content_type)
|
value: displayStringForContentType(component.content_type),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -45,7 +39,9 @@ export const ConfirmCustomExtension: FunctionComponent<{
|
|||||||
<Title>Confirm Extension</Title>
|
<Title>Confirm Extension</Title>
|
||||||
|
|
||||||
{fields.map((field) => {
|
{fields.map((field) => {
|
||||||
if (!field.value) { return undefined; }
|
if (!field.value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Subtitle>{field.label}</Subtitle>
|
<Subtitle>{field.label}</Subtitle>
|
||||||
@@ -74,7 +70,6 @@ export const ConfirmCustomExtension: FunctionComponent<{
|
|||||||
onClick={() => callback(true)}
|
onClick={() => callback(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
PreferencesSegment,
|
PreferencesSegment,
|
||||||
SubtitleLight,
|
SubtitleLight,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { Switch } from '@/components/Switch';
|
import { Switch } from '@/components/Switch';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
@@ -46,7 +46,7 @@ export const ExtensionItem: FunctionComponent<ExtensionItemProps> = ({
|
|||||||
const toggleOffllineOnly = () => {
|
const toggleOffllineOnly = () => {
|
||||||
const newOfflineOnly = !offlineOnly;
|
const newOfflineOnly = !offlineOnly;
|
||||||
setOfflineOnly(newOfflineOnly);
|
setOfflineOnly(newOfflineOnly);
|
||||||
application
|
application.mutator
|
||||||
.changeAndSaveItem(extension.uuid, (m: any) => {
|
.changeAndSaveItem(extension.uuid, (m: any) => {
|
||||||
if (m.content == undefined) m.content = {};
|
if (m.content == undefined) m.content = {};
|
||||||
m.content.offlineOnly = newOfflineOnly;
|
m.content.offlineOnly = newOfflineOnly;
|
||||||
@@ -62,7 +62,7 @@ export const ExtensionItem: FunctionComponent<ExtensionItemProps> = ({
|
|||||||
|
|
||||||
const changeExtensionName = (newName: string) => {
|
const changeExtensionName = (newName: string) => {
|
||||||
setExtensionName(newName);
|
setExtensionName(newName);
|
||||||
application
|
application.mutator
|
||||||
.changeAndSaveItem(extension.uuid, (m: any) => {
|
.changeAndSaveItem(extension.uuid, (m: any) => {
|
||||||
if (m.content == undefined) m.content = {};
|
if (m.content == undefined) m.content = {};
|
||||||
m.content.name = newName;
|
m.content.name = newName;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { FeatureDescription } from '@standardnotes/features';
|
||||||
|
import { SNComponent, ClientDisplayableError } from '@standardnotes/snjs';
|
||||||
|
import { makeAutoObservable, observable } from 'mobx';
|
||||||
|
|
||||||
|
export class ExtensionsLatestVersions {
|
||||||
|
static async load(
|
||||||
|
application: WebApplication
|
||||||
|
): Promise<ExtensionsLatestVersions | undefined> {
|
||||||
|
const response = await application.getAvailableSubscriptions();
|
||||||
|
|
||||||
|
if (response instanceof ClientDisplayableError) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionMap: Map<string, string> = new Map();
|
||||||
|
collectFeatures(
|
||||||
|
response.CORE_PLAN?.features as FeatureDescription[],
|
||||||
|
versionMap
|
||||||
|
);
|
||||||
|
collectFeatures(
|
||||||
|
response.PLUS_PLAN?.features as FeatureDescription[],
|
||||||
|
versionMap
|
||||||
|
);
|
||||||
|
collectFeatures(
|
||||||
|
response.PRO_PLAN?.features as FeatureDescription[],
|
||||||
|
versionMap
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ExtensionsLatestVersions(versionMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly latestVersionsMap: Map<string, string>) {
|
||||||
|
makeAutoObservable<ExtensionsLatestVersions, 'latestVersionsMap'>(this, {
|
||||||
|
latestVersionsMap: observable.ref,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersion(extension: SNComponent): string | undefined {
|
||||||
|
return this.latestVersionsMap.get(extension.package_info.identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFeatures(
|
||||||
|
features: FeatureDescription[] | undefined,
|
||||||
|
versionMap: Map<string, string>
|
||||||
|
) {
|
||||||
|
if (features == undefined) return;
|
||||||
|
for (const feature of features) {
|
||||||
|
versionMap.set(feature.identifier, feature.version!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { FunctionComponent } from "preact";
|
import { FunctionComponent } from 'preact';
|
||||||
import { useState, useRef, useEffect } from "preact/hooks";
|
import { useState, useRef, useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
export const RenameExtension: FunctionComponent<{
|
export const RenameExtension: FunctionComponent<{
|
||||||
extensionName: string, changeName: (newName: string) => void
|
extensionName: string;
|
||||||
|
changeName: (newName: string) => void;
|
||||||
}> = ({ extensionName, changeName }) => {
|
}> = ({ extensionName, changeName }) => {
|
||||||
const [isRenaming, setIsRenaming] = useState(false);
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
const [newExtensionName, setNewExtensionName] = useState<string>(extensionName);
|
const [newExtensionName, setNewExtensionName] =
|
||||||
|
useState<string>(extensionName);
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -38,21 +40,30 @@ export const RenameExtension: FunctionComponent<{
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
disabled={!isRenaming}
|
disabled={!isRenaming}
|
||||||
autocomplete='off'
|
autocomplete="off"
|
||||||
className="flex-grow text-base font-bold no-border bg-default px-0 color-text"
|
className="flex-grow text-base font-bold no-border bg-default px-0 color-text"
|
||||||
type="text"
|
type="text"
|
||||||
value={newExtensionName}
|
value={newExtensionName}
|
||||||
onChange={({ target: input }) => setNewExtensionName((input as HTMLInputElement)?.value)}
|
onChange={({ target: input }) =>
|
||||||
|
setNewExtensionName((input as HTMLInputElement)?.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="min-w-3" />
|
<div className="min-w-3" />
|
||||||
{isRenaming ?
|
{isRenaming ? (
|
||||||
<>
|
<>
|
||||||
<a className="pt-1 cursor-pointer" onClick={confirmRename}>Confirm</a>
|
<a className="pt-1 cursor-pointer" onClick={confirmRename}>
|
||||||
|
Confirm
|
||||||
|
</a>
|
||||||
<div className="min-w-3" />
|
<div className="min-w-3" />
|
||||||
<a className="pt-1 cursor-pointer" onClick={cancelRename}>Cancel</a>
|
<a className="pt-1 cursor-pointer" onClick={cancelRename}>
|
||||||
</> :
|
Cancel
|
||||||
<a className="pt-1 cursor-pointer" onClick={startRenaming}>Rename</a>
|
</a>
|
||||||
}
|
</>
|
||||||
|
) : (
|
||||||
|
<a className="pt-1 cursor-pointer" onClick={startRenaming}>
|
||||||
|
Rename
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Subtitle,
|
Subtitle,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import {
|
import {
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '@standardnotes/snjs';
|
} from '@standardnotes/snjs';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
import { Switch } from '@/components/Switch';
|
import { Switch } from '@/components/Switch';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -34,7 +34,7 @@ const makeEditorDefault = (
|
|||||||
if (currentDefault) {
|
if (currentDefault) {
|
||||||
removeEditorDefault(application, currentDefault);
|
removeEditorDefault(application, currentDefault);
|
||||||
}
|
}
|
||||||
application.changeAndSaveItem(component.uuid, (m) => {
|
application.mutator.changeAndSaveItem(component.uuid, (m) => {
|
||||||
const mutator = m as ComponentMutator;
|
const mutator = m as ComponentMutator;
|
||||||
mutator.defaultEditor = true;
|
mutator.defaultEditor = true;
|
||||||
});
|
});
|
||||||
@@ -44,7 +44,7 @@ const removeEditorDefault = (
|
|||||||
application: WebApplication,
|
application: WebApplication,
|
||||||
component: SNComponent
|
component: SNComponent
|
||||||
) => {
|
) => {
|
||||||
application.changeAndSaveItem(component.uuid, (m) => {
|
application.mutator.changeAndSaveItem(component.uuid, (m) => {
|
||||||
const mutator = m as ComponentMutator;
|
const mutator = m as ComponentMutator;
|
||||||
mutator.defaultEditor = false;
|
mutator.defaultEditor = false;
|
||||||
});
|
});
|
||||||
@@ -67,6 +67,10 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
|||||||
application.getPreference(PrefKey.EditorSpellcheck, true)
|
application.getPreference(PrefKey.EditorSpellcheck, true)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [addNoteToParentFolders, setAddNoteToParentFolders] = useState(() =>
|
||||||
|
application.getPreference(PrefKey.NoteAddToParentFolders, true)
|
||||||
|
);
|
||||||
|
|
||||||
const toggleSpellcheck = () => {
|
const toggleSpellcheck = () => {
|
||||||
setSpellcheck(!spellcheck);
|
setSpellcheck(!spellcheck);
|
||||||
application.getAppState().toggleGlobalSpellcheck();
|
application.getAppState().toggleGlobalSpellcheck();
|
||||||
@@ -148,6 +152,28 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
|||||||
</div>
|
</div>
|
||||||
<Switch onChange={toggleSpellcheck} checked={spellcheck} />
|
<Switch onChange={toggleSpellcheck} checked={spellcheck} />
|
||||||
</div>
|
</div>
|
||||||
|
<HorizontalSeparator classes="mt-5 mb-3" />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Subtitle>
|
||||||
|
Add all parent tags when adding a nested tag to a note
|
||||||
|
</Subtitle>
|
||||||
|
<Text>
|
||||||
|
When enabled, adding a nested tag to a note will automatically add
|
||||||
|
all associated parent tags.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={() => {
|
||||||
|
application.setPreference(
|
||||||
|
PrefKey.NoteAddToParentFolders,
|
||||||
|
!addNoteToParentFolders
|
||||||
|
);
|
||||||
|
setAddNoteToParentFolders(!addNoteToParentFolders);
|
||||||
|
}}
|
||||||
|
checked={addNoteToParentFolders}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
);
|
);
|
||||||
@@ -6,13 +6,13 @@ import {
|
|||||||
Subtitle,
|
Subtitle,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs';
|
import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
import { usePremiumModal } from '@/components/Premium';
|
import { usePremiumModal } from '@/components/Premium';
|
||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
|
|
||||||
type ExperimentalFeatureItem = {
|
type ExperimentalFeatureItem = {
|
||||||
identifier: FeatureIdentifier;
|
identifier: FeatureIdentifier;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
import { Switch } from '@/components/Switch';
|
import { Switch } from '@/components/Switch';
|
||||||
import {
|
import {
|
||||||
PreferencesGroup,
|
PreferencesGroup,
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Subtitle,
|
Subtitle,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { PrefKey } from '@standardnotes/snjs';
|
import { PrefKey } from '@standardnotes/snjs';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './ErrorReporting';
|
|
||||||
export * from './Tools';
|
export * from './Tools';
|
||||||
export * from './Defaults';
|
export * from './Defaults';
|
||||||
export * from './Labs';
|
export * from './Labs';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
import { LinkButton, Subtitle } from '@/preferences/components';
|
import { LinkButton, Subtitle } from '@/components/Preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { ListedAccount, ListedAccountInfo } from '@standardnotes/snjs';
|
import { ListedAccount, ListedAccountInfo } from '@standardnotes/snjs';
|
||||||
import { FunctionalComponent } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||||
|
import { Icon } from '@/components/Icon';
|
||||||
|
import {
|
||||||
|
STRING_E2E_ENABLED,
|
||||||
|
STRING_ENC_NOT_ENABLED,
|
||||||
|
STRING_LOCAL_ENC_ENABLED,
|
||||||
|
} from '@/strings';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '../../components';
|
||||||
|
|
||||||
|
const formatCount = (count: number, itemType: string) =>
|
||||||
|
`${count} / ${count} ${itemType}`;
|
||||||
|
|
||||||
|
const EncryptionEnabled: FunctionComponent<{ appState: AppState }> = observer(
|
||||||
|
({ appState }) => {
|
||||||
|
const count = appState.accountMenu.structuredNotesAndTagsCount;
|
||||||
|
const notes = formatCount(count.notes, 'notes');
|
||||||
|
const tags = formatCount(count.tags, 'tags');
|
||||||
|
const archived = formatCount(count.archived, 'archived notes');
|
||||||
|
const deleted = formatCount(count.deleted, 'trashed notes');
|
||||||
|
|
||||||
|
const checkIcon = (
|
||||||
|
<Icon className="success min-w-4 min-h-4" type="check-bold" />
|
||||||
|
);
|
||||||
|
const noteIcon = <Icon type="rich-text" className="min-w-5 min-h-5" />;
|
||||||
|
const tagIcon = <Icon type="hashtag" className="min-w-5 min-h-5" />;
|
||||||
|
const archiveIcon = <Icon type="archive" className="min-w-5 min-h-5" />;
|
||||||
|
const trashIcon = <Icon type="trash" className="min-w-5 min-h-5" />;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row pb-1 pt-1.5">
|
||||||
|
<DecoratedInput
|
||||||
|
disabled={true}
|
||||||
|
text={notes}
|
||||||
|
right={[checkIcon]}
|
||||||
|
left={[noteIcon]}
|
||||||
|
/>
|
||||||
|
<div className="min-w-3" />
|
||||||
|
<DecoratedInput
|
||||||
|
disabled={true}
|
||||||
|
text={tags}
|
||||||
|
right={[checkIcon]}
|
||||||
|
left={[tagIcon]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<DecoratedInput
|
||||||
|
disabled={true}
|
||||||
|
text={archived}
|
||||||
|
right={[checkIcon]}
|
||||||
|
left={[archiveIcon]}
|
||||||
|
/>
|
||||||
|
<div className="min-w-3" />
|
||||||
|
<DecoratedInput
|
||||||
|
disabled={true}
|
||||||
|
text={deleted}
|
||||||
|
right={[checkIcon]}
|
||||||
|
left={[trashIcon]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Encryption: FunctionComponent<{ appState: AppState }> = observer(
|
||||||
|
({ appState }) => {
|
||||||
|
const app = appState.application;
|
||||||
|
const hasUser = app.hasAccount();
|
||||||
|
const hasPasscode = app.hasPasscode();
|
||||||
|
const isEncryptionEnabled = app.isEncryptionAvailable();
|
||||||
|
|
||||||
|
const encryptionStatusString = hasUser
|
||||||
|
? STRING_E2E_ENABLED
|
||||||
|
: hasPasscode
|
||||||
|
? STRING_LOCAL_ENC_ENABLED
|
||||||
|
: STRING_ENC_NOT_ENABLED;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>Encryption</Title>
|
||||||
|
<Text>{encryptionStatusString}</Text>
|
||||||
|
|
||||||
|
{isEncryptionEnabled && <EncryptionEnabled appState={appState} />}
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
|
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
|
||||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_LOCAL_ENC_ENABLED,
|
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
|
||||||
|
STRING_E2E_ENABLED,
|
||||||
|
STRING_ENC_NOT_ENABLED,
|
||||||
|
STRING_LOCAL_ENC_ENABLED,
|
||||||
STRING_NON_MATCHING_PASSCODES,
|
STRING_NON_MATCHING_PASSCODES,
|
||||||
StringUtils,
|
StringUtils,
|
||||||
Strings
|
Strings,
|
||||||
} from '@/strings';
|
} from '@/strings';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { preventRefreshing } from '@/utils';
|
import { preventRefreshing } from '@/utils';
|
||||||
@@ -15,7 +18,12 @@ 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 '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { PreferencesSegment, Title, Text, PreferencesGroup } from '@/preferences/components';
|
import {
|
||||||
|
PreferencesSegment,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
PreferencesGroup,
|
||||||
|
} from '@/components/Preferences/components';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -23,23 +31,31 @@ type Props = {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PasscodeLock = observer(({
|
export const PasscodeLock = observer(({ application, appState }: Props) => {
|
||||||
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 } = appState.accountMenu;
|
const {
|
||||||
|
setIsEncryptionEnabled,
|
||||||
|
setIsBackupEncrypted,
|
||||||
|
setEncryptionStatusString,
|
||||||
|
} = appState.accountMenu;
|
||||||
|
|
||||||
const passcodeInputRef = useRef<HTMLInputElement>(null);
|
const passcodeInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [passcode, setPasscode] = useState<string | undefined>(undefined);
|
const [passcode, setPasscode] = useState<string | undefined>(undefined);
|
||||||
const [passcodeConfirmation, setPasscodeConfirmation] = useState<string | undefined>(undefined);
|
const [passcodeConfirmation, setPasscodeConfirmation] = useState<
|
||||||
const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState<unknown>(null);
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [selectedAutoLockInterval, setSelectedAutoLockInterval] =
|
||||||
|
useState<unknown>(null);
|
||||||
const [isPasscodeFocused, setIsPasscodeFocused] = useState(false);
|
const [isPasscodeFocused, setIsPasscodeFocused] = useState(false);
|
||||||
const [showPasscodeForm, setShowPasscodeForm] = useState(false);
|
const [showPasscodeForm, setShowPasscodeForm] = useState(false);
|
||||||
const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession());
|
const [canAddPasscode, setCanAddPasscode] = useState(
|
||||||
|
!application.isEphemeralSession()
|
||||||
|
);
|
||||||
const [hasPasscode, setHasPasscode] = useState(application.hasPasscode());
|
const [hasPasscode, setHasPasscode] = useState(application.hasPasscode());
|
||||||
|
|
||||||
const handleAddPassCode = () => {
|
const handleAddPassCode = () => {
|
||||||
@@ -52,7 +68,9 @@ export const PasscodeLock = observer(({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const reloadAutoLockInterval = useCallback(async () => {
|
const reloadAutoLockInterval = useCallback(async () => {
|
||||||
const interval = await application.getAutolockService().getAutoLockInterval();
|
const interval = await application
|
||||||
|
.getAutolockService()
|
||||||
|
.getAutoLockInterval();
|
||||||
setSelectedAutoLockInterval(interval);
|
setSelectedAutoLockInterval(interval);
|
||||||
}, [application]);
|
}, [application]);
|
||||||
|
|
||||||
@@ -67,13 +85,18 @@ export const PasscodeLock = observer(({
|
|||||||
const encryptionStatusString = hasUser
|
const encryptionStatusString = hasUser
|
||||||
? STRING_E2E_ENABLED
|
? STRING_E2E_ENABLED
|
||||||
: hasPasscode
|
: hasPasscode
|
||||||
? STRING_LOCAL_ENC_ENABLED
|
? STRING_LOCAL_ENC_ENABLED
|
||||||
: STRING_ENC_NOT_ENABLED;
|
: STRING_ENC_NOT_ENABLED;
|
||||||
|
|
||||||
setEncryptionStatusString(encryptionStatusString);
|
setEncryptionStatusString(encryptionStatusString);
|
||||||
setIsEncryptionEnabled(encryptionEnabled);
|
setIsEncryptionEnabled(encryptionEnabled);
|
||||||
setIsBackupEncrypted(encryptionEnabled);
|
setIsBackupEncrypted(encryptionEnabled);
|
||||||
}, [application, setEncryptionStatusString, setIsBackupEncrypted, setIsEncryptionEnabled]);
|
}, [
|
||||||
|
application,
|
||||||
|
setEncryptionStatusString,
|
||||||
|
setIsBackupEncrypted,
|
||||||
|
setIsEncryptionEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
const selectAutoLockInterval = async (interval: number) => {
|
const selectAutoLockInterval = async (interval: number) => {
|
||||||
if (!(await application.authorizeAutolockIntervalChange())) {
|
if (!(await application.authorizeAutolockIntervalChange())) {
|
||||||
@@ -88,9 +111,7 @@ export const PasscodeLock = observer(({
|
|||||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
|
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
|
||||||
async () => {
|
async () => {
|
||||||
if (await application.removePasscode()) {
|
if (await application.removePasscode()) {
|
||||||
await application
|
await application.getAutolockService().deleteAutolockPreference();
|
||||||
.getAutolockService()
|
|
||||||
.deleteAutolockPreference();
|
|
||||||
await reloadAutoLockInterval();
|
await reloadAutoLockInterval();
|
||||||
refreshEncryptionStatus();
|
refreshEncryptionStatus();
|
||||||
}
|
}
|
||||||
@@ -103,12 +124,18 @@ export const PasscodeLock = observer(({
|
|||||||
setPasscode(value);
|
setPasscode(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmPasscodeChange = (event: TargetedEvent<HTMLInputElement>) => {
|
const handleConfirmPasscodeChange = (
|
||||||
|
event: TargetedEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
const { value } = event.target as HTMLInputElement;
|
const { value } = event.target as HTMLInputElement;
|
||||||
setPasscodeConfirmation(value);
|
setPasscodeConfirmation(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitPasscodeForm = async (event: TargetedEvent<HTMLFormElement> | TargetedMouseEvent<HTMLButtonElement>) => {
|
const submitPasscodeForm = async (
|
||||||
|
event:
|
||||||
|
| TargetedEvent<HTMLFormElement>
|
||||||
|
| TargetedMouseEvent<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!passcode || passcode.length === 0) {
|
if (!passcode || passcode.length === 0) {
|
||||||
@@ -119,7 +146,7 @@ export const PasscodeLock = observer(({
|
|||||||
|
|
||||||
if (passcode !== passcodeConfirmation) {
|
if (passcode !== passcodeConfirmation) {
|
||||||
await alertDialog({
|
await alertDialog({
|
||||||
text: STRING_NON_MATCHING_PASSCODES
|
text: STRING_NON_MATCHING_PASSCODES,
|
||||||
});
|
});
|
||||||
setIsPasscodeFocused(true);
|
setIsPasscodeFocused(true);
|
||||||
return;
|
return;
|
||||||
@@ -186,27 +213,28 @@ export const PasscodeLock = observer(({
|
|||||||
|
|
||||||
{!hasPasscode && canAddPasscode && (
|
{!hasPasscode && canAddPasscode && (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<Text className="mb-3">
|
<Text className="mb-3">
|
||||||
Add a passcode to lock the application and
|
Add a passcode to lock the application and encrypt on-device key
|
||||||
encrypt on-device key storage.
|
storage.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{keyStorageInfo && (
|
{keyStorageInfo && <Text className="mb-3">{keyStorageInfo}</Text>}
|
||||||
<Text className="mb-3">{keyStorageInfo}</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!showPasscodeForm && (
|
{!showPasscodeForm && (
|
||||||
<Button label="Add Passcode" onClick={handleAddPassCode} type="primary" />
|
<Button
|
||||||
|
label="Add passcode"
|
||||||
|
onClick={handleAddPassCode}
|
||||||
|
type="primary"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasPasscode && !canAddPasscode && (
|
{!hasPasscode && !canAddPasscode && (
|
||||||
<Text>
|
<Text>
|
||||||
Adding a passcode is not supported in temporary sessions. Please sign
|
Adding a passcode is not supported in temporary sessions. Please
|
||||||
out, then sign back in with the "Stay signed in" option checked.
|
sign out, then sign back in with the "Stay signed in" option
|
||||||
|
checked.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -229,8 +257,17 @@ export const PasscodeLock = observer(({
|
|||||||
placeholder="Confirm Passcode"
|
placeholder="Confirm Passcode"
|
||||||
/>
|
/>
|
||||||
<div className="min-h-2" />
|
<div className="min-h-2" />
|
||||||
<Button type="primary" onClick={submitPasscodeForm} label="Set Passcode" className="mr-3" />
|
<Button
|
||||||
<Button type="normal" onClick={() => setShowPasscodeForm(false)} label="Cancel" />
|
type="primary"
|
||||||
|
onClick={submitPasscodeForm}
|
||||||
|
label="Set Passcode"
|
||||||
|
className="mr-3"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="normal"
|
||||||
|
onClick={() => setShowPasscodeForm(false)}
|
||||||
|
label="Cancel"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -238,11 +275,20 @@ export const PasscodeLock = observer(({
|
|||||||
<>
|
<>
|
||||||
<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 type="normal" label="Change Passcode" onClick={changePasscodePressed} className="mr-3" />
|
<Button
|
||||||
<Button type="danger" label="Remove Passcode" onClick={removePasscodePressed} />
|
type="normal"
|
||||||
|
label="Change Passcode"
|
||||||
|
onClick={changePasscodePressed}
|
||||||
|
className="mr-3"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="danger"
|
||||||
|
label="Remove Passcode"
|
||||||
|
onClick={removePasscodePressed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>)}
|
</>
|
||||||
|
)}
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
|
|
||||||
@@ -252,19 +298,23 @@ export const PasscodeLock = observer(({
|
|||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>Autolock</Title>
|
<Title>Autolock</Title>
|
||||||
<Text className="mb-3">The autolock timer begins when the window or tab loses focus.</Text>
|
<Text className="mb-3">
|
||||||
|
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 ${option.value === selectedAutoLockInterval ? 'boxed' : ''}`}
|
className={`sk-a info mr-3 ${
|
||||||
onClick={() => selectAutoLockInterval(option.value)}>
|
option.value === selectedAutoLockInterval ? 'boxed' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => selectAutoLockInterval(option.value)}
|
||||||
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
</>
|
</>
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator';
|
||||||
|
import { Switch } from '@/components/Switch';
|
||||||
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesSegment,
|
||||||
|
Subtitle,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@/components/Preferences/components';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import {
|
||||||
|
MuteSignInEmailsOption,
|
||||||
|
LogSessionUserAgentOption,
|
||||||
|
SettingName,
|
||||||
|
} from '@standardnotes/settings';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionalComponent } from 'preact';
|
||||||
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
|
import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/strings';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Privacy: FunctionalComponent<Props> = observer(
|
||||||
|
({ application }: Props) => {
|
||||||
|
const [signInEmailsMutedValue, setSignInEmailsMutedValue] =
|
||||||
|
useState<MuteSignInEmailsOption>(MuteSignInEmailsOption.NotMuted);
|
||||||
|
const [sessionUaLoggingValue, setSessionUaLoggingValue] =
|
||||||
|
useState<LogSessionUserAgentOption>(LogSessionUserAgentOption.Enabled);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const updateSetting = async (
|
||||||
|
settingName: SettingName,
|
||||||
|
payload: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await application.settings.updateSetting(settingName, payload, false);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
application.alertService.alert(STRING_FAILED_TO_UPDATE_USER_SETTING);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSettings = useCallback(async () => {
|
||||||
|
if (!application.getUser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userSettings = await application.settings.listSettings();
|
||||||
|
setSignInEmailsMutedValue(
|
||||||
|
userSettings.getSettingValue<MuteSignInEmailsOption>(
|
||||||
|
SettingName.MuteSignInEmails,
|
||||||
|
MuteSignInEmailsOption.NotMuted
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setSessionUaLoggingValue(
|
||||||
|
userSettings.getSettingValue<LogSessionUserAgentOption>(
|
||||||
|
SettingName.LogSessionUserAgent,
|
||||||
|
LogSessionUserAgentOption.Enabled
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [application]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadSettings();
|
||||||
|
}, [loadSettings]);
|
||||||
|
|
||||||
|
const toggleMuteSignInEmails = async () => {
|
||||||
|
const previousValue = signInEmailsMutedValue;
|
||||||
|
const newValue =
|
||||||
|
previousValue === MuteSignInEmailsOption.Muted
|
||||||
|
? MuteSignInEmailsOption.NotMuted
|
||||||
|
: MuteSignInEmailsOption.Muted;
|
||||||
|
setSignInEmailsMutedValue(newValue);
|
||||||
|
|
||||||
|
const updateResult = await updateSetting(
|
||||||
|
SettingName.MuteSignInEmails,
|
||||||
|
newValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateResult) {
|
||||||
|
setSignInEmailsMutedValue(previousValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSessionLogging = async () => {
|
||||||
|
const previousValue = sessionUaLoggingValue;
|
||||||
|
const newValue =
|
||||||
|
previousValue === LogSessionUserAgentOption.Enabled
|
||||||
|
? LogSessionUserAgentOption.Disabled
|
||||||
|
: LogSessionUserAgentOption.Enabled;
|
||||||
|
setSessionUaLoggingValue(newValue);
|
||||||
|
|
||||||
|
const updateResult = await updateSetting(
|
||||||
|
SettingName.LogSessionUserAgent,
|
||||||
|
newValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateResult) {
|
||||||
|
setSessionUaLoggingValue(previousValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>Privacy</Title>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Subtitle>Disable sign-in notification emails</Subtitle>
|
||||||
|
<Text>
|
||||||
|
Disables email notifications when a new sign-in occurs on your
|
||||||
|
account. (Email notifications are available to paid
|
||||||
|
subscribers).
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className={'sk-spinner info small'} />
|
||||||
|
) : (
|
||||||
|
<Switch
|
||||||
|
onChange={toggleMuteSignInEmails}
|
||||||
|
checked={
|
||||||
|
signInEmailsMutedValue === MuteSignInEmailsOption.Muted
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<HorizontalSeparator classes="mt-5 mb-3" />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Subtitle>Session user agent logging</Subtitle>
|
||||||
|
<Text>
|
||||||
|
User agent logging allows you to identify the devices or
|
||||||
|
browsers signed into your account. For increased privacy, you
|
||||||
|
can disable this feature, which will remove all saved user
|
||||||
|
agent values from our server, and disable future logging of
|
||||||
|
this value.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className={'sk-spinner info small'} />
|
||||||
|
) : (
|
||||||
|
<Switch
|
||||||
|
onChange={toggleSessionLogging}
|
||||||
|
checked={
|
||||||
|
sessionUaLoggingValue === LogSessionUserAgentOption.Enabled
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
PreferencesSegment,
|
PreferencesSegment,
|
||||||
Title,
|
Title,
|
||||||
Text,
|
Text,
|
||||||
} from '@/preferences/components';
|
} from '@/components/Preferences/components';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -3,5 +3,7 @@ import { FunctionComponent } from 'preact';
|
|||||||
export const Bullet: FunctionComponent<{ className?: string }> = ({
|
export const Bullet: FunctionComponent<{ className?: string }> = ({
|
||||||
className = '',
|
className = '',
|
||||||
}) => (
|
}) => (
|
||||||
<div className={`min-w-1 min-h-1 rounded-full bg-inverted-default ${className} mr-2`} />
|
<div
|
||||||
|
className={`min-w-1 min-h-1 rounded-full bg-inverted-default ${className} mr-2`}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user