Merge branch 'release/3.8.21' into main
This commit is contained in:
@@ -19,7 +19,10 @@
|
||||
"camelcase": "warn",
|
||||
"sort-imports": "off",
|
||||
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
|
||||
"react-hooks/exhaustive-deps": "error" // Checks effect dependencies
|
||||
"react-hooks/exhaustive-deps": "error", // Checks effect dependencies
|
||||
"eol-last": "error",
|
||||
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
|
||||
"no-trailing-spaces": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
|
||||
# OS & IDE
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
@@ -213,7 +213,7 @@ DEPENDENCIES
|
||||
dotenv-rails
|
||||
haml
|
||||
lograge (~> 0.11.2)
|
||||
newrelic_rpm
|
||||
newrelic_rpm (~> 7.0)
|
||||
non-stupid-digest-assets
|
||||
puma
|
||||
rack-cors
|
||||
|
||||
3
app/assets/icons/ic-copy.svg
Normal file
3
app/assets/icons/ic-copy.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66724 3.66626C1.66724 2.56169 2.56267 1.66626 3.66724 1.66626H11.3339C12.4385 1.66626 13.3339 2.56169 13.3339 3.66626V13.3329H3.66724C2.56267 13.3329 1.66724 12.4375 1.66724 11.3329V3.66626ZM16.3339 6.66626C17.4385 6.66626 18.3339 7.56169 18.3339 8.66626V16.3329C18.3339 17.4375 17.4385 18.3329 16.3339 18.3329H8.66724C7.56267 18.3329 6.66724 17.4375 6.66724 16.3329V14.9996H15.0006V6.66626H16.3339ZM3.3339 3.33293V11.6663H11.6672V3.33293H3.3339Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 580 B |
3
app/assets/icons/ic-download.svg
Normal file
3
app/assets/icons/ic-download.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 3.5V9.5H12.17L10 11.67L7.83 9.5H9V3.5H11ZM13 1.5H7V7.5H3L10 14.5L17 7.5H13V1.5ZM17 16.5H3V18.5H17V16.5Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 238 B |
3
app/assets/icons/ic-info.svg
Normal file
3
app/assets/icons/ic-info.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.16675 7.50008H10.8334V5.83342H9.16675V7.50008ZM10.0001 16.6667C6.32508 16.6667 3.33341 13.6751 3.33341 10.0001C3.33341 6.32508 6.32508 3.33341 10.0001 3.33341C13.6751 3.33341 16.6667 6.32508 16.6667 10.0001C16.6667 13.6751 13.6751 16.6667 10.0001 16.6667ZM10.0001 1.66675C8.90573 1.66675 7.8221 1.8823 6.81105 2.30109C5.80001 2.71987 4.88135 3.3337 4.10752 4.10752C2.54472 5.67033 1.66675 7.78994 1.66675 10.0001C1.66675 12.2102 2.54472 14.3298 4.10752 15.8926C4.88135 16.6665 5.80001 17.2803 6.81105 17.6991C7.8221 18.1179 8.90573 18.3334 10.0001 18.3334C12.2102 18.3334 14.3298 17.4554 15.8926 15.8926C17.4554 14.3298 18.3334 12.2102 18.3334 10.0001C18.3334 8.90573 18.1179 7.8221 17.6991 6.81105C17.2803 5.80001 16.6665 4.88135 15.8926 4.10752C15.1188 3.3337 14.2002 2.71987 13.1891 2.30109C12.1781 1.8823 11.0944 1.66675 10.0001 1.66675ZM9.16675 14.1667H10.8334V9.16675H9.16675V14.1667Z" fill="#72767E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -65,7 +65,7 @@ import { NotesContextMenuDirective } from './components/NotesContextMenu';
|
||||
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
|
||||
import { IconDirective } from './components/Icon';
|
||||
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
|
||||
import { PreferencesDirective } from './components/preferences';
|
||||
import { PreferencesDirective } from './preferences';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
/**
|
||||
|
||||
32
app/assets/javascripts/components/Button.tsx
Normal file
32
app/assets/javascripts/components/Button.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`;
|
||||
|
||||
const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \
|
||||
focus:bg-contrast hover:bg-contrast`;
|
||||
const primaryClass = `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 \
|
||||
focus:brightness-130`;
|
||||
|
||||
export const Button: FunctionComponent<{
|
||||
className?: string;
|
||||
type: 'normal' | 'primary';
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}> = ({ type, label, className = '', onClick, disabled = false }) => {
|
||||
const buttonClass = type === 'primary' ? primaryClass : normalClass;
|
||||
const cursorClass = disabled ? 'cursor-default' : 'cursor-pointer';
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${buttonClass} ${cursorClass} ${className}`}
|
||||
onClick={(e) => {
|
||||
onClick();
|
||||
e.preventDefault();
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
38
app/assets/javascripts/components/CircleProgress.tsx
Normal file
38
app/assets/javascripts/components/CircleProgress.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
27
app/assets/javascripts/components/CircleProgressTime.tsx
Normal file
27
app/assets/javascripts/components/CircleProgressTime.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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} />;
|
||||
};
|
||||
42
app/assets/javascripts/components/DecoratedInput.tsx
Normal file
42
app/assets/javascripts/components/DecoratedInput.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FunctionalComponent, ComponentChild } from 'preact';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
left?: ComponentChild[];
|
||||
right?: ComponentChild[];
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input that can be decorated on the left and right side
|
||||
*/
|
||||
export const DecoratedInput: FunctionalComponent<Props> = ({
|
||||
className = '',
|
||||
disabled = false,
|
||||
left,
|
||||
right,
|
||||
text,
|
||||
}) => {
|
||||
const base =
|
||||
'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center gap-4';
|
||||
const stateClasses = disabled
|
||||
? 'no-border bg-grey-5'
|
||||
: 'border-solid border-1 border-gray-300';
|
||||
const classes = `${base} ${stateClasses} ${className}`;
|
||||
|
||||
return (
|
||||
<div className={`${classes} focus-within:ring-info`}>
|
||||
{left}
|
||||
<div className="flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full no-border color-black focus:shadow-none"
|
||||
disabled={disabled}
|
||||
value={text}
|
||||
/>
|
||||
</div>
|
||||
{right}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -23,6 +23,9 @@ import SettingsIcon from '../../icons/ic-settings.svg';
|
||||
import StarIcon from '../../icons/ic-star.svg';
|
||||
import ThemesIcon from '../../icons/ic-themes.svg';
|
||||
import UserIcon from '../../icons/ic-user.svg';
|
||||
import CopyIcon from '../../icons/ic-copy.svg';
|
||||
import DownloadIcon from '../../icons/ic-download.svg';
|
||||
import InfoIcon from '../../icons/ic-info.svg';
|
||||
|
||||
import { toDirective } from './utils';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
@@ -52,6 +55,9 @@ const ICONS = {
|
||||
star: StarIcon,
|
||||
themes: ThemesIcon,
|
||||
user: UserIcon,
|
||||
copy: CopyIcon,
|
||||
download: DownloadIcon,
|
||||
info: InfoIcon,
|
||||
};
|
||||
|
||||
export type IconType = keyof typeof ICONS;
|
||||
@@ -61,7 +67,7 @@ type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Icon: FunctionalComponent<Props> = ({ type, className }) => {
|
||||
export const Icon: FunctionalComponent<Props> = ({ type, className = '' }) => {
|
||||
const IconComponent = ICONS[type];
|
||||
return <IconComponent className={`sn-icon ${className}`} />;
|
||||
};
|
||||
|
||||
@@ -1,53 +1,38 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { Icon, IconType } from './Icon';
|
||||
|
||||
const ICON_BUTTON_TYPES: {
|
||||
[type: string]: { className: string };
|
||||
} = {
|
||||
normal: {
|
||||
className: '',
|
||||
},
|
||||
primary: {
|
||||
className: 'info',
|
||||
},
|
||||
};
|
||||
|
||||
export type IconButtonType = keyof typeof ICON_BUTTON_TYPES;
|
||||
|
||||
interface IconButtonProps {
|
||||
interface Props {
|
||||
/**
|
||||
* onClick - preventDefault is handled within the component
|
||||
*/
|
||||
onClick: () => void;
|
||||
|
||||
type: IconButtonType;
|
||||
|
||||
className?: string;
|
||||
|
||||
iconType: IconType;
|
||||
icon: IconType;
|
||||
}
|
||||
|
||||
/**
|
||||
* CircleButton component with an icon for SPA
|
||||
* IconButton component with an icon
|
||||
* preventDefault is already handled within the component
|
||||
*/
|
||||
export const IconButton: FunctionComponent<IconButtonProps> = ({
|
||||
export const IconButton: FunctionComponent<Props> = ({
|
||||
onClick,
|
||||
type,
|
||||
className,
|
||||
iconType,
|
||||
icon,
|
||||
}) => {
|
||||
const click = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
};
|
||||
const typeProps = ICON_BUTTON_TYPES[type];
|
||||
return (
|
||||
<button
|
||||
className={`sn-icon-button ${typeProps.className} ${className ?? ''}`}
|
||||
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${
|
||||
className ?? ''
|
||||
}`}
|
||||
onClick={click}
|
||||
>
|
||||
<Icon type={iconType} />
|
||||
<Icon type={icon} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
22
app/assets/javascripts/components/Input.tsx
Normal file
22
app/assets/javascripts/components/Input.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
interface Props {
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Input: FunctionalComponent<Props> = ({
|
||||
className = '',
|
||||
disabled = false,
|
||||
text,
|
||||
}) => {
|
||||
const base = `rounded py-1.5 px-3 text-input my-1 h-8`;
|
||||
const stateClasses = disabled
|
||||
? 'no-border bg-grey-5'
|
||||
: 'border-solid border-1 border-gray-300';
|
||||
const classes = `${base} ${stateClasses} ${className}`;
|
||||
return (
|
||||
<input type="text" className={classes} disabled={disabled} value={text} />
|
||||
);
|
||||
};
|
||||
@@ -3,19 +3,21 @@ import { toDirective } from './utils';
|
||||
import NotesIcon from '../../icons/il-notes.svg';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptionsPanel } from './NotesOptionsPanel';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
const MultipleSelectedNotes = observer(({ appState }: Props) => {
|
||||
const MultipleSelectedNotes = observer(({ application, appState }: Props) => {
|
||||
const count = appState.notes.selectedNotesCount;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full items-center">
|
||||
<div className="flex items-center justify-between p-4 w-full">
|
||||
<h1 className="sk-h1 font-bold m-0">{count} selected notes</h1>
|
||||
<NotesOptionsPanel appState={appState} />
|
||||
<NotesOptionsPanel application={application} appState={appState} />
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col justify-center items-center w-full max-w-md">
|
||||
<NotesIcon className="block" />
|
||||
|
||||
@@ -3,12 +3,14 @@ import { toDirective, useCloseOnBlur, useCloseOnClickOutside } from './utils';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptions } from './NotesOptions';
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
const NotesContextMenu = observer(({ appState }: Props) => {
|
||||
const NotesContextMenu = observer(({ application, appState }: Props) => {
|
||||
const {
|
||||
contextMenuOpen,
|
||||
contextMenuPosition,
|
||||
@@ -46,7 +48,11 @@ const NotesContextMenu = observer(({ appState }: Props) => {
|
||||
maxHeight: contextMenuMaxHeight,
|
||||
}}
|
||||
>
|
||||
<NotesOptions appState={appState} closeOnBlur={closeOnBlur} />
|
||||
<NotesOptions
|
||||
application={application}
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
});
|
||||
|
||||
@@ -9,15 +9,33 @@ import {
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import { SNNote } from '@standardnotes/snjs/dist/@types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { KeyboardModifier } from '@/services/ioService';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
onSubmenuChange?: (submenuOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type DeletePermanentlyButtonProps = {
|
||||
closeOnBlur: Props["closeOnBlur"];
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const DeletePermanentlyButton = ({
|
||||
closeOnBlur,
|
||||
onClick,
|
||||
}: DeletePermanentlyButtonProps) => (
|
||||
<button onBlur={closeOnBlur} className="sn-dropdown-item" onClick={onClick}>
|
||||
<Icon type="close" className="color-danger mr-2" />
|
||||
<span className="color-danger">Delete permanently</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export const NotesOptions = observer(
|
||||
({ appState, closeOnBlur, onSubmenuChange }: Props) => {
|
||||
({ application, appState, closeOnBlur, onSubmenuChange }: Props) => {
|
||||
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
||||
const [tagsMenuPosition, setTagsMenuPosition] = useState<{
|
||||
top: number;
|
||||
@@ -29,6 +47,7 @@ export const NotesOptions = observer(
|
||||
});
|
||||
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] =
|
||||
useState<number | 'auto'>('auto');
|
||||
const [altKeyDown, setAltKeyDown] = useState(false);
|
||||
|
||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
||||
const notesMatchingAttribute = notes.filter(condition);
|
||||
@@ -59,6 +78,22 @@ export const NotesOptions = observer(
|
||||
}
|
||||
}, [tagsMenuOpen, onSubmenuChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const removeAltKeyObserver = application.io.addKeyObserver({
|
||||
modifiers: [KeyboardModifier.Alt],
|
||||
onKeyDown: () => {
|
||||
setAltKeyDown(true);
|
||||
},
|
||||
onKeyUp: () => {
|
||||
setAltKeyDown(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeAltKeyObserver();
|
||||
};
|
||||
}, [application]);
|
||||
|
||||
const openTagsMenu = () => {
|
||||
const defaultFontSize = window.getComputedStyle(
|
||||
document.documentElement
|
||||
@@ -235,18 +270,26 @@ export const NotesOptions = observer(
|
||||
Unarchive
|
||||
</button>
|
||||
)}
|
||||
{notTrashed && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.setTrashSelectedNotes(true);
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className={iconClass} />
|
||||
Move to Trash
|
||||
</button>
|
||||
)}
|
||||
{notTrashed &&
|
||||
(altKeyDown ? (
|
||||
<DeletePermanentlyButton
|
||||
closeOnBlur={closeOnBlur}
|
||||
onClick={async () => {
|
||||
await appState.notes.deleteNotesPermanently();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.setTrashSelectedNotes(true);
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className={iconClass} />
|
||||
Move to Trash
|
||||
</button>
|
||||
))}
|
||||
{trashed && (
|
||||
<>
|
||||
<button
|
||||
@@ -259,16 +302,12 @@ export const NotesOptions = observer(
|
||||
<Icon type="restore" className={iconClass} />
|
||||
Restore
|
||||
</button>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item"
|
||||
<DeletePermanentlyButton
|
||||
closeOnBlur={closeOnBlur}
|
||||
onClick={async () => {
|
||||
await appState.notes.deleteNotesPermanently();
|
||||
}}
|
||||
>
|
||||
<Icon type="close" className="color-danger mr-2" />
|
||||
<span className="color-danger">Delete permanently</span>
|
||||
</button>
|
||||
/>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item"
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptions } from './NotesOptions';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
export const NotesOptionsPanel = observer(({ application, appState }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
@@ -76,6 +78,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
>
|
||||
{open && (
|
||||
<NotesOptions
|
||||
application={application}
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
onSubmenuChange={onSubmenuChange}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Icon, IconType } from '@/components/Icon';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
interface PreferencesMenuItemProps {
|
||||
iconType: IconType;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const PreferencesMenuItem: FunctionComponent<PreferencesMenuItemProps> =
|
||||
({ iconType, label, selected, onClick }) => (
|
||||
<div
|
||||
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Icon className="icon" type={iconType} />
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
42
app/assets/javascripts/components/RoundIconButton.tsx
Normal file
42
app/assets/javascripts/components/RoundIconButton.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { Icon, IconType } from './Icon';
|
||||
|
||||
type ButtonType = 'normal' | 'primary';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* onClick - preventDefault is handled within the component
|
||||
*/
|
||||
onClick: () => void;
|
||||
|
||||
type: ButtonType;
|
||||
|
||||
className?: string;
|
||||
|
||||
icon: IconType;
|
||||
}
|
||||
|
||||
/**
|
||||
* IconButton component with an icon
|
||||
* preventDefault is already handled within the component
|
||||
*/
|
||||
export const RoundIconButton: FunctionComponent<Props> = ({
|
||||
onClick,
|
||||
type,
|
||||
className,
|
||||
icon: iconType,
|
||||
}) => {
|
||||
const click = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
};
|
||||
const classes = type === 'primary' ? 'info ' : '';
|
||||
return (
|
||||
<button
|
||||
className={`sn-icon-button ${classes} ${className ?? ''}`}
|
||||
onClick={click}
|
||||
>
|
||||
<Icon type={iconType} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,7 @@ export type SwitchProps = HTMLProps<HTMLInputElement> & {
|
||||
checked?: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
className?: string;
|
||||
children: ComponentChildren;
|
||||
children?: ComponentChildren;
|
||||
};
|
||||
|
||||
export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
@@ -22,7 +22,9 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
const checked = props.checked ?? checkedState;
|
||||
const className = props.className ?? '';
|
||||
return (
|
||||
<label className={`sn-component flex justify-between items-center cursor-pointer hover:bg-contrast px-3 ${className}`}>
|
||||
<label
|
||||
className={`sn-component flex justify-between items-center cursor-pointer hover:bg-contrast px-3 ${className}`}
|
||||
>
|
||||
{props.children}
|
||||
<CustomCheckboxContainer
|
||||
checked={checked}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
export const Title: FunctionalComponent = ({ children }) => (
|
||||
<h2 className="text-base m-0 mb-3">{children}</h2>
|
||||
);
|
||||
|
||||
export const Subtitle: FunctionalComponent = ({ children }) => (
|
||||
<h4 className="font-medium text-sm m-0 mb-1">{children}</h4>
|
||||
);
|
||||
|
||||
export const Text: FunctionalComponent = ({ children }) => (
|
||||
<p className="text-xs">{children}</p>
|
||||
);
|
||||
|
||||
export const Button: FunctionalComponent<{ label: string; link: string }> = ({
|
||||
label,
|
||||
link,
|
||||
}) => (
|
||||
<a
|
||||
target="_blank"
|
||||
className="block bg-default color-text rounded border-solid border-1
|
||||
border-gray-300 px-4 py-2 font-bold text-sm fit-content mt-3
|
||||
focus:bg-contrast hover:bg-contrast "
|
||||
href={link}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
@@ -1,22 +0,0 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
import { toDirective } from '../utils';
|
||||
import { PreferencesView } from './view';
|
||||
|
||||
interface WrapperProps {
|
||||
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
|
||||
}
|
||||
|
||||
const PreferencesViewWrapper: FunctionComponent<WrapperProps> = observer(
|
||||
({ appState }) => {
|
||||
if (!appState.preferences.isOpen) return null;
|
||||
return (
|
||||
<PreferencesView close={() => appState.preferences.closePreferences()} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const PreferencesDirective = toDirective<WrapperProps>(
|
||||
PreferencesViewWrapper
|
||||
);
|
||||
@@ -1,23 +0,0 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesMenuItem } from '../PreferencesMenuItem';
|
||||
import { Preferences } from './preferences';
|
||||
|
||||
interface PreferencesMenuProps {
|
||||
preferences: Preferences;
|
||||
}
|
||||
|
||||
export const PreferencesMenu: FunctionComponent<PreferencesMenuProps> =
|
||||
observer(({ preferences }) => (
|
||||
<div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6">
|
||||
{preferences.items.map((pref) => (
|
||||
<PreferencesMenuItem
|
||||
key={pref.id}
|
||||
iconType={pref.icon}
|
||||
label={pref.label}
|
||||
selected={pref.selected}
|
||||
onClick={() => preferences.selectItem(pref.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
@@ -1,33 +0,0 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
const HorizontalLine: FunctionalComponent<{ index: number; length: number }> =
|
||||
({ index, length }) =>
|
||||
index < length - 1 ? (
|
||||
<hr className="h-1px w-full bg-border no-border" />
|
||||
) : null;
|
||||
|
||||
export const PreferencesSegment: FunctionalComponent = ({ children }) => (
|
||||
<div>{children}</div>
|
||||
);
|
||||
|
||||
export const PreferencesGroup: FunctionalComponent = ({ children }) => (
|
||||
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
|
||||
{!Array.isArray(children)
|
||||
? children
|
||||
: children.map((c, i, arr) => (
|
||||
<>
|
||||
{c}
|
||||
<HorizontalLine index={i} length={arr.length} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PreferencesPane: FunctionalComponent = ({ children }) => (
|
||||
<div className="preferences-pane flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||
<div className="flex-grow flex flex-col py-6 items-center">
|
||||
<div className="max-w-124 flex flex-col gap-3">{children}</div>
|
||||
</div>
|
||||
<div className="flex-basis-55 flex-shrink-max" />
|
||||
</div>
|
||||
);
|
||||
@@ -1,55 +0,0 @@
|
||||
import { IconType } from '@/components/Icon';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
|
||||
interface PreferenceItem {
|
||||
icon: IconType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface PreferenceListItem extends PreferenceItem {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const predefinedItems: PreferenceItem[] = [
|
||||
{ label: 'General', icon: 'settings' },
|
||||
{ label: 'Account', icon: 'user' },
|
||||
{ label: 'Appearance', icon: 'themes' },
|
||||
{ label: 'Security', icon: 'security' },
|
||||
{ label: 'Listed', icon: 'listed' },
|
||||
{ label: 'Shortcuts', icon: 'keyboard' },
|
||||
{ label: 'Accessibility', icon: 'accessibility' },
|
||||
{ label: 'Get a free month', icon: 'star' },
|
||||
{ label: 'Help & feedback', icon: 'help' },
|
||||
];
|
||||
|
||||
export class Preferences {
|
||||
private readonly _items: PreferenceListItem[];
|
||||
private _selectedId = 0;
|
||||
|
||||
constructor(items: PreferenceItem[] = predefinedItems) {
|
||||
makeObservable<Preferences, '_selectedId'>(this, {
|
||||
_selectedId: observable,
|
||||
selectedItem: computed,
|
||||
items: computed,
|
||||
selectItem: action,
|
||||
});
|
||||
|
||||
this._items = items.map((p, idx) => ({ ...p, id: idx }));
|
||||
this._selectedId = this._items[0].id;
|
||||
}
|
||||
|
||||
selectItem(id: number) {
|
||||
this._selectedId = id;
|
||||
}
|
||||
|
||||
get items(): (PreferenceListItem & { selected: boolean })[] {
|
||||
return this._items.map((p) => ({
|
||||
...p,
|
||||
selected: p.id === this._selectedId,
|
||||
}));
|
||||
}
|
||||
|
||||
get selectedItem(): PreferenceListItem {
|
||||
return this._items.find((item) => item.id === this._selectedId)!;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import { TitleBar, Title } from '@/components/TitleBar';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { Preferences } from './preferences';
|
||||
import { PreferencesMenu } from './menu';
|
||||
import { HelpAndFeedback } from './help-feedback';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface PreferencesViewProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const PreferencesCanvas: FunctionComponent<{
|
||||
preferences: Preferences;
|
||||
}> = observer(({ preferences: prefs }) => (
|
||||
<div className="flex flex-row flex-grow min-h-0 justify-between">
|
||||
<PreferencesMenu preferences={prefs}></PreferencesMenu>
|
||||
{/* Temporary selector until a full solution is implemented */}
|
||||
{prefs.selectedItem.label === 'Help & feedback' ? (
|
||||
<HelpAndFeedback />
|
||||
) : null}
|
||||
</div>
|
||||
));
|
||||
|
||||
export const PreferencesView: FunctionComponent<PreferencesViewProps> =
|
||||
observer(({ close }) => {
|
||||
const prefs = new Preferences();
|
||||
return (
|
||||
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
|
||||
<TitleBar className="items-center justify-between">
|
||||
{/* div is added so flex justify-between can center the title */}
|
||||
<div className="h-8 w-8" />
|
||||
<Title className="text-lg">Your preferences for Standard Notes</Title>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
type="normal"
|
||||
iconType="close"
|
||||
/>
|
||||
</TitleBar>
|
||||
<PreferencesCanvas preferences={prefs} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { WebApplication } from '@/ui_models/application';
|
||||
import { WebDirective } from './../../types';
|
||||
import template from '%/directives/actions-menu.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { SNItem, Action, SNActionsExtension, UuidString } from '@standardnotes/snjs';
|
||||
import { SNItem, Action, SNActionsExtension, UuidString, CopyPayload } from '@standardnotes/snjs';
|
||||
import { ActionResponse } from '@standardnotes/snjs';
|
||||
import { ActionsExtensionMutator } from '@standardnotes/snjs';
|
||||
|
||||
@@ -27,7 +27,7 @@ type ActionsMenuState = {
|
||||
extensions: SNActionsExtension[]
|
||||
extensionsState: Record<UuidString, ExtensionState>
|
||||
selectedActionId?: number
|
||||
menu: {
|
||||
menuItems: {
|
||||
uuid: UuidString,
|
||||
name: string,
|
||||
loading: boolean,
|
||||
@@ -57,7 +57,7 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
});
|
||||
this.loadExtensions();
|
||||
this.autorun(() => {
|
||||
this.rebuildMenu({
|
||||
this.rebuildMenuState({
|
||||
hiddenExtensions: this.appState.actionsMenu.hiddenExtensions
|
||||
});
|
||||
});
|
||||
@@ -65,13 +65,20 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
|
||||
/** @override */
|
||||
getInitialState() {
|
||||
const extensions = this.application.actionsManager!.getExtensions().sort((a, b) => {
|
||||
const extensions = this.application.actionsManager.getExtensions().sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
}).map((extension) => {
|
||||
return new SNActionsExtension(CopyPayload(extension.payload, {
|
||||
content: {
|
||||
...extension.payload.safeContent,
|
||||
actions: []
|
||||
}
|
||||
}));
|
||||
});
|
||||
const extensionsState: Record<UuidString, ExtensionState> = {};
|
||||
extensions.map((extension) => {
|
||||
extensionsState[extension.uuid] = {
|
||||
loading: false,
|
||||
loading: true,
|
||||
error: false,
|
||||
};
|
||||
});
|
||||
@@ -79,11 +86,11 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
extensions,
|
||||
extensionsState,
|
||||
hiddenExtensions: {},
|
||||
menu: [],
|
||||
menuItems: [],
|
||||
};
|
||||
}
|
||||
|
||||
rebuildMenu({
|
||||
rebuildMenuState({
|
||||
extensions = this.state.extensions,
|
||||
extensionsState = this.state.extensionsState,
|
||||
selectedActionId = this.state.selectedActionId,
|
||||
@@ -93,7 +100,7 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
extensions,
|
||||
extensionsState,
|
||||
selectedActionId,
|
||||
menu: extensions.map(extension => {
|
||||
menuItems: extensions.map(extension => {
|
||||
const state = extensionsState[extension.uuid];
|
||||
const hidden = hiddenExtensions[extension.uuid];
|
||||
return {
|
||||
@@ -136,7 +143,7 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
|
||||
async executeAction(action: Action, extensionUuid: UuidString) {
|
||||
if (action.verb === 'nested') {
|
||||
this.rebuildMenu({
|
||||
this.rebuildMenuState({
|
||||
selectedActionId: action.id
|
||||
});
|
||||
return;
|
||||
@@ -220,7 +227,7 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
}
|
||||
return ext;
|
||||
});
|
||||
await this.rebuildMenu({
|
||||
await this.rebuildMenuState({
|
||||
extensions
|
||||
});
|
||||
}
|
||||
@@ -236,7 +243,7 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
}
|
||||
return ext;
|
||||
});
|
||||
this.rebuildMenu({
|
||||
this.rebuildMenuState({
|
||||
extensions
|
||||
});
|
||||
}
|
||||
@@ -248,7 +255,7 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
private setLoadingExtension(extensionUuid: UuidString, value = false) {
|
||||
const { extensionsState } = this.state;
|
||||
extensionsState[extensionUuid].loading = value;
|
||||
this.rebuildMenu({
|
||||
this.rebuildMenuState({
|
||||
extensionsState
|
||||
});
|
||||
}
|
||||
@@ -256,7 +263,7 @@ class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements
|
||||
private setErrorExtension(extensionUuid: UuidString, value = false) {
|
||||
const { extensionsState } = this.state;
|
||||
extensionsState[extensionUuid].error = value;
|
||||
this.rebuildMenu({
|
||||
this.rebuildMenuState({
|
||||
extensionsState
|
||||
});
|
||||
}
|
||||
|
||||
20
app/assets/javascripts/preferences/PreferencesMenuView.tsx
Normal file
20
app/assets/javascripts/preferences/PreferencesMenuView.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { MenuItem } from './components';
|
||||
import { PreferencesMenu } from './preferences-menu';
|
||||
|
||||
export const PreferencesMenuView: FunctionComponent<{
|
||||
menu: PreferencesMenu;
|
||||
}> = observer(({ menu }) => (
|
||||
<div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6">
|
||||
{menu.menuItems.map((pref) => (
|
||||
<MenuItem
|
||||
key={pref.id}
|
||||
iconType={pref.icon}
|
||||
label={pref.label}
|
||||
selected={pref.selected}
|
||||
onClick={() => menu.selectPane(pref.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
87
app/assets/javascripts/preferences/PreferencesView.tsx
Normal file
87
app/assets/javascripts/preferences/PreferencesView.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { RoundIconButton } from '@/components/RoundIconButton';
|
||||
import { TitleBar, Title } from '@/components/TitleBar';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { AccountPreferences, HelpAndFeedback, Security } from './panes';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PreferencesMenu } from './preferences-menu';
|
||||
import { PreferencesMenuView } from './PreferencesMenuView';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
const PaneSelector: FunctionComponent<{
|
||||
prefs: PreferencesMenu;
|
||||
application: WebApplication;
|
||||
}> = observer(({ prefs: menu, application }) => {
|
||||
switch (menu.selectedPaneId) {
|
||||
case 'general':
|
||||
return null;
|
||||
case 'account':
|
||||
return <AccountPreferences application={application} />;
|
||||
case 'appearance':
|
||||
return null;
|
||||
case 'security':
|
||||
return <Security />;
|
||||
case 'listed':
|
||||
return null;
|
||||
case 'shortcuts':
|
||||
return null;
|
||||
case 'accessibility':
|
||||
return null;
|
||||
case 'get-free-month':
|
||||
return null;
|
||||
case 'help-feedback':
|
||||
return <HelpAndFeedback />;
|
||||
}
|
||||
});
|
||||
|
||||
const PreferencesCanvas: FunctionComponent<{
|
||||
preferences: PreferencesMenu;
|
||||
application: WebApplication;
|
||||
}> = observer(({ preferences: prefs, application }) => (
|
||||
<div className="flex flex-row flex-grow min-h-0 justify-between">
|
||||
<PreferencesMenuView menu={prefs}></PreferencesMenuView>
|
||||
<PaneSelector prefs={prefs} application={application} />
|
||||
</div>
|
||||
));
|
||||
|
||||
const PreferencesView: FunctionComponent<{
|
||||
close: () => void;
|
||||
application: WebApplication;
|
||||
}> = observer(
|
||||
({ close, application }) => {
|
||||
const prefs = new PreferencesMenu();
|
||||
|
||||
return (
|
||||
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
|
||||
<TitleBar className="items-center justify-between">
|
||||
{/* div is added so flex justify-between can center the title */}
|
||||
<div className="h-8 w-8" />
|
||||
<Title className="text-lg">Your preferences for Standard Notes</Title>
|
||||
<RoundIconButton
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
type="normal"
|
||||
icon="close"
|
||||
/>
|
||||
</TitleBar>
|
||||
<PreferencesCanvas preferences={prefs} application={application} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export interface PreferencesWrapperProps {
|
||||
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
|
||||
application: WebApplication;
|
||||
}
|
||||
|
||||
export const PreferencesViewWrapper: FunctionComponent<PreferencesWrapperProps> =
|
||||
observer(({ appState, application }) => {
|
||||
if (!appState.preferences.isOpen) return null;
|
||||
return (
|
||||
<PreferencesView
|
||||
application={application}
|
||||
close={() => appState.preferences.closePreferences()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
26
app/assets/javascripts/preferences/components/Content.tsx
Normal file
26
app/assets/javascripts/preferences/components/Content.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const Title: FunctionComponent = ({ children }) => (
|
||||
<h2 className="text-base m-0 mb-1">{children}</h2>
|
||||
);
|
||||
|
||||
export const Subtitle: FunctionComponent = ({ children }) => (
|
||||
<h4 className="font-medium text-sm m-0 mb-1">{children}</h4>
|
||||
);
|
||||
|
||||
export const Text: FunctionComponent = ({ children }) => (
|
||||
<p className="text-xs">{children}</p>
|
||||
);
|
||||
|
||||
const buttonClasses = `block bg-default color-text rounded border-solid \
|
||||
border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content mt-3 \
|
||||
focus:bg-contrast hover:bg-contrast `;
|
||||
|
||||
export const LinkButton: FunctionComponent<{ label: string; link: string }> = ({
|
||||
label,
|
||||
link,
|
||||
}) => (
|
||||
<a target="_blank" className={buttonClasses} href={link}>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
27
app/assets/javascripts/preferences/components/MenuItem.tsx
Normal file
27
app/assets/javascripts/preferences/components/MenuItem.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Icon, IconType } from '@/components/Icon';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
interface Props {
|
||||
iconType: IconType;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const MenuItem: FunctionComponent<Props> = ({
|
||||
iconType,
|
||||
label,
|
||||
selected,
|
||||
onClick,
|
||||
}) => (
|
||||
<div
|
||||
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Icon className="icon" type={iconType} />
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
35
app/assets/javascripts/preferences/components/Pane.tsx
Normal file
35
app/assets/javascripts/preferences/components/Pane.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({
|
||||
index,
|
||||
length,
|
||||
}) =>
|
||||
index < length - 1 ? (
|
||||
<hr className="h-1px w-full bg-border no-border" />
|
||||
) : null;
|
||||
|
||||
export const PreferencesSegment: FunctionComponent = ({ children }) => (
|
||||
<div className="flex flex-col">{children}</div>
|
||||
);
|
||||
|
||||
export const PreferencesGroup: FunctionComponent = ({ children }) => (
|
||||
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
|
||||
{!Array.isArray(children)
|
||||
? children
|
||||
: children.map((c, i, arr) => (
|
||||
<>
|
||||
{c}
|
||||
<HorizontalLine index={i} length={arr.length} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PreferencesPane: FunctionComponent = ({ children }) => (
|
||||
<div className="preferences-pane flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||
<div className="flex-grow flex flex-col py-6 items-center">
|
||||
<div className="w-125 max-w-125 flex flex-col gap-3">{children}</div>
|
||||
</div>
|
||||
<div className="flex-basis-55 flex-shrink" />
|
||||
</div>
|
||||
);
|
||||
3
app/assets/javascripts/preferences/components/index.ts
Normal file
3
app/assets/javascripts/preferences/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './Content';
|
||||
export * from './MenuItem';
|
||||
export * from './Pane';
|
||||
9
app/assets/javascripts/preferences/index.ts
Normal file
9
app/assets/javascripts/preferences/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { toDirective } from '../components/utils';
|
||||
import {
|
||||
PreferencesViewWrapper,
|
||||
PreferencesWrapperProps,
|
||||
} from './PreferencesView';
|
||||
|
||||
export const PreferencesDirective = toDirective<PreferencesWrapperProps>(
|
||||
PreferencesViewWrapper
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Sync } from '@/preferences/panes/account';
|
||||
import { PreferencesPane } from '@/preferences/components';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
export const AccountPreferences = observer(({application}: {application: WebApplication}) => {
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Sync application={application} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
});
|
||||
@@ -1,12 +1,20 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { PreferencesGroup, PreferencesPane, PreferencesSegment } from './pane';
|
||||
import { Title, Subtitle, Text, Button } from './content';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import {
|
||||
Title,
|
||||
Subtitle,
|
||||
Text,
|
||||
LinkButton,
|
||||
PreferencesGroup,
|
||||
PreferencesPane,
|
||||
PreferencesSegment,
|
||||
} from '../components';
|
||||
|
||||
export const HelpAndFeedback: FunctionalComponent = () => (
|
||||
export const HelpAndFeedback: FunctionComponent = () => (
|
||||
<PreferencesPane>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Frequently asked questions</Title>
|
||||
<div className="h-2 w-full" />
|
||||
<Subtitle>Who can read my private notes?</Subtitle>
|
||||
<Text>
|
||||
Quite simply: no one but you. Not us, not your ISP, not a hacker, and
|
||||
@@ -44,7 +52,7 @@ export const HelpAndFeedback: FunctionalComponent = () => (
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
<Subtitle>Can’t find your question here?</Subtitle>
|
||||
<Button label="Open FAQ" link="https://standardnotes.com/help" />
|
||||
<LinkButton label="Open FAQ" link="https://standardnotes.com/help" />
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<PreferencesGroup>
|
||||
@@ -59,7 +67,7 @@ export const HelpAndFeedback: FunctionalComponent = () => (
|
||||
</a>{' '}
|
||||
before advocating for a feature request.
|
||||
</Text>
|
||||
<Button
|
||||
<LinkButton
|
||||
label="Go to the forum"
|
||||
link="https://forum.standardnotes.org/"
|
||||
/>
|
||||
@@ -73,7 +81,7 @@ export const HelpAndFeedback: FunctionalComponent = () => (
|
||||
Want to share your feedback with us? Join the Standard Notes Slack
|
||||
group for discussions on security, themes, editors and more.
|
||||
</Text>
|
||||
<Button
|
||||
<LinkButton
|
||||
link="https://standardnotes.com/slack"
|
||||
label="Join our Slack group"
|
||||
/>
|
||||
@@ -85,7 +93,7 @@ export const HelpAndFeedback: FunctionalComponent = () => (
|
||||
<Text>
|
||||
Send an email to help@standardnotes.org and we’ll sort it out.
|
||||
</Text>
|
||||
<Button link="mailto: help@standardnotes.org" label="Email us" />
|
||||
<LinkButton link="mailto: help@standardnotes.org" label="Email us" />
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
9
app/assets/javascripts/preferences/panes/Security.tsx
Normal file
9
app/assets/javascripts/preferences/panes/Security.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesPane } from '../components';
|
||||
import { TwoFactorAuthWrapper } from './two-factor-auth';
|
||||
|
||||
export const Security: FunctionComponent = () => (
|
||||
<PreferencesPane>
|
||||
<TwoFactorAuthWrapper />
|
||||
</PreferencesPane>
|
||||
);
|
||||
60
app/assets/javascripts/preferences/panes/account/Sync.tsx
Normal file
60
app/assets/javascripts/preferences/panes/account/Sync.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/preferences/components';
|
||||
import { Button } from '@/components/Button';
|
||||
import { SyncQueueStrategy } from '@node_modules/@standardnotes/snjs';
|
||||
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||
import { useState } from '@node_modules/preact/hooks';
|
||||
import { dateToLocalizedString } from '@/utils';
|
||||
import { observer } from '@node_modules/mobx-react-lite';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
const Sync = observer(({ application }: Props) => {
|
||||
const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate);
|
||||
};
|
||||
|
||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||
|
||||
const doSynchronization = async () => {
|
||||
setIsSyncingInProgress(true);
|
||||
|
||||
const response = await application.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true
|
||||
});
|
||||
setIsSyncingInProgress(false);
|
||||
if (response && response.error) {
|
||||
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
setLastSyncDate(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className='flex flex-row items-center'>
|
||||
<div className='flex-grow flex flex-col'>
|
||||
<Title>Sync</Title>
|
||||
<Text>
|
||||
Last synced <span className='font-bold'>on {lastSyncDate}</span>
|
||||
</Text>
|
||||
<Button
|
||||
className='min-w-20 mt-3'
|
||||
type='normal'
|
||||
label='Sync now'
|
||||
disabled={isSyncingInProgress}
|
||||
onClick={doSynchronization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
|
||||
export default Sync;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Sync } from './Sync';
|
||||
3
app/assets/javascripts/preferences/panes/index.ts
Normal file
3
app/assets/javascripts/preferences/panes/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './HelpFeedback';
|
||||
export * from './Security';
|
||||
export * from './AccountPreferences';
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Icon, IconType } from '@/components/Icon';
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from '@reach/disclosure';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
const DisclosureIconButton: FunctionComponent<{
|
||||
className?: string;
|
||||
icon: IconType;
|
||||
}> = ({ className = '', icon }) => (
|
||||
<DisclosureButton
|
||||
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${
|
||||
className ?? ''
|
||||
}`}
|
||||
>
|
||||
<Icon type={icon} />
|
||||
</DisclosureButton>
|
||||
);
|
||||
|
||||
/**
|
||||
* AuthAppInfoPopup is an info icon that shows a tooltip when clicked
|
||||
* Tooltip is dismissible by clicking outside
|
||||
*
|
||||
* Note: it can be generalized but more use cases are required
|
||||
* @returns
|
||||
*/
|
||||
export const AuthAppInfoTooltip: FunctionComponent = () => {
|
||||
const [isShown, setShown] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const dismiss = () => setShown(false);
|
||||
document.addEventListener('mousedown', dismiss);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', dismiss);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<Disclosure open={isShown} onChange={() => setShown(!isShown)}>
|
||||
<div className="relative">
|
||||
<DisclosureIconButton icon="info" className="mt-1" />
|
||||
<DisclosurePanel>
|
||||
<div
|
||||
className={`bg-black color-white text-center rounded shadow-overlay
|
||||
py-1.5 px-2 absolute w-103 -top-10 -left-51`}
|
||||
>
|
||||
Some apps, like Google Authenticator, do not back up and restore
|
||||
your secret keys if you lose your device or get a new one.
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { downloadSecretKey } from './download-secret-key';
|
||||
import { TwoFactorActivation } from './model';
|
||||
import {
|
||||
TwoFactorDialog,
|
||||
TwoFactorDialogLabel,
|
||||
TwoFactorDialogDescription,
|
||||
TwoFactorDialogButtons,
|
||||
} from './TwoFactorDialog';
|
||||
|
||||
export const SaveSecretKey: FunctionComponent<{
|
||||
activation: TwoFactorActivation;
|
||||
}> = observer(({ activation: act }) => {
|
||||
const download = (
|
||||
<IconButton
|
||||
icon="download"
|
||||
onClick={() => {
|
||||
downloadSecretKey(act.secretKey);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const copy = (
|
||||
<IconButton
|
||||
icon="copy"
|
||||
onClick={() => {
|
||||
navigator?.clipboard?.writeText(act.secretKey);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<TwoFactorDialog>
|
||||
<TwoFactorDialogLabel
|
||||
closeDialog={() => {
|
||||
act.cancelActivation();
|
||||
}}
|
||||
>
|
||||
Step 2 of 3 - Save secret key
|
||||
</TwoFactorDialogLabel>
|
||||
<TwoFactorDialogDescription>
|
||||
<div className="flex-grow flex flex-col gap-2">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div className="text-sm">
|
||||
・<b>Save your secret key</b>{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
|
||||
>
|
||||
somewhere safe
|
||||
</a>
|
||||
:
|
||||
</div>
|
||||
<DecoratedInput
|
||||
disabled={true}
|
||||
right={[copy, download]}
|
||||
text={act.secretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
・You can use this key to generate codes if you lose access to your
|
||||
authenticator app.{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://standardnotes.com/help/22/what-happens-if-i-lose-my-2fa-device-and-my-secret-key"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</TwoFactorDialogDescription>
|
||||
<TwoFactorDialogButtons>
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Back"
|
||||
onClick={() => act.openScanQRCode()}
|
||||
/>
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label="Next"
|
||||
onClick={() => act.openVerification()}
|
||||
/>
|
||||
</TwoFactorDialogButtons>
|
||||
</TwoFactorDialog>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { DecoratedInput } from '../../../components/DecoratedInput';
|
||||
import { IconButton } from '../../../components/IconButton';
|
||||
import { Button } from '@/components/Button';
|
||||
import { TwoFactorActivation } from './model';
|
||||
import {
|
||||
TwoFactorDialog,
|
||||
TwoFactorDialogLabel,
|
||||
TwoFactorDialogDescription,
|
||||
TwoFactorDialogButtons,
|
||||
} from './TwoFactorDialog';
|
||||
import { AuthAppInfoTooltip } from './AuthAppInfoPopup';
|
||||
|
||||
export const ScanQRCode: FunctionComponent<{
|
||||
activation: TwoFactorActivation;
|
||||
}> = observer(({ activation: act }) => {
|
||||
const copy = (
|
||||
<IconButton
|
||||
icon="copy"
|
||||
onClick={() => {
|
||||
navigator?.clipboard?.writeText(act.secretKey);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<TwoFactorDialog>
|
||||
<TwoFactorDialogLabel
|
||||
closeDialog={() => {
|
||||
act.cancelActivation();
|
||||
}}
|
||||
>
|
||||
Step 1 of 3 - Scan QR code
|
||||
</TwoFactorDialogLabel>
|
||||
<TwoFactorDialogDescription>
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
<div className="w-25 h-25 flex items-center justify-center bg-info">
|
||||
QR code
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<div className="text-sm">
|
||||
・Open your <b>authenticator app</b>.
|
||||
</div>
|
||||
<AuthAppInfoTooltip />
|
||||
</div>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="text-sm flex-grow">
|
||||
・<b>Scan this QR code</b> or <b>add this secret key</b>:
|
||||
</div>
|
||||
<div className="w-56">
|
||||
<DecoratedInput
|
||||
disabled={true}
|
||||
text={act.secretKey}
|
||||
right={[copy]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TwoFactorDialogDescription>
|
||||
<TwoFactorDialogButtons>
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Cancel"
|
||||
onClick={() => act.cancelActivation()}
|
||||
/>
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label="Next"
|
||||
onClick={() => act.openSaveSecretKey()}
|
||||
/>
|
||||
</TwoFactorDialogButtons>
|
||||
</TwoFactorDialog>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { TwoFactorActivation } from './model';
|
||||
import { SaveSecretKey } from './SaveSecretKey';
|
||||
import { ScanQRCode } from './ScanQRCode';
|
||||
import { Verification } from './Verification';
|
||||
|
||||
export const TwoFactorActivationView: FunctionComponent<{
|
||||
activation: TwoFactorActivation;
|
||||
}> = observer(({ activation: act }) => (
|
||||
<>
|
||||
{act.step === 'scan-qr-code' && <ScanQRCode activation={act} />}
|
||||
|
||||
{act.step === 'save-secret-key' && <SaveSecretKey activation={act} />}
|
||||
|
||||
{act.step === 'verification' && <Verification activation={act} />}
|
||||
</>
|
||||
));
|
||||
@@ -0,0 +1,53 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import {
|
||||
Title,
|
||||
Text,
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
} from '../../components';
|
||||
import { Switch } from '../../../components/Switch';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import {
|
||||
is2FAActivation,
|
||||
is2FADisabled,
|
||||
is2FAEnabled,
|
||||
TwoFactorAuth,
|
||||
} from './model';
|
||||
import { TwoFactorDisabledView } from './TwoFactorDisabledView';
|
||||
import { TwoFactorEnabledView } from './TwoFactorEnabledView';
|
||||
import { TwoFactorActivationView } from './TwoFactorActivationView';
|
||||
|
||||
export const TwoFactorAuthView: FunctionComponent<{
|
||||
auth: TwoFactorAuth;
|
||||
}> = observer(({ auth }) => (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Title>Two-factor authentication</Title>
|
||||
<Text>
|
||||
An extra layer of security when logging in to your account.
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={!is2FADisabled(auth.status)}
|
||||
onChange={() => auth.toggle2FA()}
|
||||
/>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
{is2FAEnabled(auth.status) && (
|
||||
<TwoFactorEnabledView
|
||||
secretKey={auth.status.secretKey}
|
||||
authCode={auth.status.authCode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{is2FAActivation(auth.status) && (
|
||||
<TwoFactorActivationView activation={auth.status} />
|
||||
)}
|
||||
|
||||
{!is2FAEnabled(auth.status) && <TwoFactorDisabledView />}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
));
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ComponentChildren, FunctionComponent } from 'preact';
|
||||
import { IconButton } from '../../../components/IconButton';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@reach/alert-dialog';
|
||||
import { useRef } from 'preact/hooks';
|
||||
|
||||
/**
|
||||
* TwoFactorDialog is AlertDialog styled for 2FA
|
||||
* Can be generalized but more use cases are needed
|
||||
*/
|
||||
export const TwoFactorDialog: FunctionComponent<{
|
||||
children: ComponentChildren;
|
||||
}> = ({ children }) => {
|
||||
const ldRef = useRef<HTMLButtonElement>();
|
||||
|
||||
return (
|
||||
<AlertDialog leastDestructiveRef={ldRef}>
|
||||
{/* sn-component is focusable by default, but doesn't stretch to child width
|
||||
resulting in a badly focused dialog. Utility classes are not available
|
||||
at the sn-component level, only below it. tabIndex -1 disables focus
|
||||
and enables it on the child component */}
|
||||
<div tabIndex={-1} className="sn-component">
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="w-160 bg-default rounded shadow-overlay focus:padded-ring-info"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const TwoFactorDialogLabel: FunctionComponent<{
|
||||
closeDialog: () => void;
|
||||
}> = ({ children, closeDialog }) => (
|
||||
<AlertDialogLabel className="">
|
||||
<div className="px-4 pt-4 pb-3 flex flex-row">
|
||||
<div className="flex-grow color-black text-lg font-bold">{children}</div>
|
||||
<IconButton
|
||||
className="color-grey-1 h-5 w-5"
|
||||
icon="close"
|
||||
onClick={() => closeDialog()}
|
||||
/>
|
||||
</div>
|
||||
<hr className="h-1px bg-border no-border m-0" />
|
||||
</AlertDialogLabel>
|
||||
);
|
||||
|
||||
export const TwoFactorDialogDescription: FunctionComponent = ({ children }) => (
|
||||
<AlertDialogDescription className="px-4 py-4">
|
||||
{children}
|
||||
</AlertDialogDescription>
|
||||
);
|
||||
|
||||
export const TwoFactorDialogButtons: FunctionComponent = ({ children }) => (
|
||||
<>
|
||||
<hr className="h-1px bg-border no-border m-0" />
|
||||
<div className="px-4 py-4 flex flex-row justify-end gap-3">{children}</div>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Text } from '../../components';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const TwoFactorDisabledView: FunctionComponent = () => (
|
||||
<Text>
|
||||
Enabling two-factor authentication will sign you out of all other sessions.{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Text>
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
import { CircleProgressTime } from '@/components/CircleProgressTime';
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { downloadSecretKey } from './download-secret-key';
|
||||
import { Text } from '../../components';
|
||||
|
||||
export const TwoFactorEnabledView: FunctionComponent<{
|
||||
secretKey: string;
|
||||
authCode: string;
|
||||
}> = ({ secretKey, authCode }) => {
|
||||
const download = (
|
||||
<IconButton
|
||||
icon="download"
|
||||
onClick={() => {
|
||||
downloadSecretKey(secretKey);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const copy = (
|
||||
<IconButton
|
||||
icon="copy"
|
||||
onClick={() => {
|
||||
navigator?.clipboard?.writeText(secretKey);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const progress = <CircleProgressTime time={30000} />;
|
||||
return (
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Text>Secret Key</Text>
|
||||
<DecoratedInput
|
||||
disabled={true}
|
||||
right={[copy, download]}
|
||||
text={secretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-30 flex flex-col">
|
||||
<Text>Authentication Code</Text>
|
||||
<DecoratedInput disabled={true} text={authCode} right={[progress]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { TwoFactorActivation } from './model';
|
||||
import {
|
||||
TwoFactorDialog,
|
||||
TwoFactorDialogLabel,
|
||||
TwoFactorDialogDescription,
|
||||
TwoFactorDialogButtons,
|
||||
} from './TwoFactorDialog';
|
||||
|
||||
export const Verification: FunctionComponent<{
|
||||
activation: TwoFactorActivation;
|
||||
}> = observer(({ activation: act }) => {
|
||||
const borderInv =
|
||||
act.verificationStatus === 'invalid' ? 'border-dark-red' : '';
|
||||
return (
|
||||
<TwoFactorDialog>
|
||||
<TwoFactorDialogLabel
|
||||
closeDialog={() => {
|
||||
act.cancelActivation();
|
||||
}}
|
||||
>
|
||||
Step 3 of 3 - Verification
|
||||
</TwoFactorDialogLabel>
|
||||
<TwoFactorDialogDescription>
|
||||
<div className="flex-grow flex flex-col gap-1">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="text-sm">
|
||||
・Enter your <b>secret key</b>:
|
||||
</div>
|
||||
<DecoratedInput className={borderInv} />
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="text-sm">
|
||||
・Verify the <b>authentication code</b> generated by your
|
||||
authenticator app:
|
||||
</div>
|
||||
<DecoratedInput className={`w-30 ${borderInv}`} />
|
||||
</div>
|
||||
</div>
|
||||
</TwoFactorDialogDescription>
|
||||
<TwoFactorDialogButtons>
|
||||
{act.verificationStatus === 'invalid' && (
|
||||
<div className="text-sm color-dark-red">
|
||||
Incorrect credentials, please try again.
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Back"
|
||||
onClick={() => act.openSaveSecretKey()}
|
||||
/>
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label="Next"
|
||||
onClick={() => act.enable2FA('X', 'X')}
|
||||
/>
|
||||
</TwoFactorDialogButtons>
|
||||
</TwoFactorDialog>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// Temporary implementation until integration
|
||||
export function downloadSecretKey(text: string) {
|
||||
const link = document.createElement('a');
|
||||
const blob = new Blob([text], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.setAttribute('download', 'standardnotes_2fa_key.txt');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(link.href);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { TwoFactorAuth } from './model';
|
||||
import { TwoFactorAuthView } from './TwoFactorAuthView';
|
||||
|
||||
export const TwoFactorAuthWrapper: FunctionComponent = () => {
|
||||
const [auth] = useState(() => new TwoFactorAuth());
|
||||
return <TwoFactorAuthView auth={auth} />;
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import { action, makeAutoObservable, observable, untracked } from 'mobx';
|
||||
|
||||
function getNewAuthCode() {
|
||||
const MIN = 100000;
|
||||
const MAX = 999999;
|
||||
const code = Math.floor(Math.random() * (MAX - MIN) + MIN);
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
const activationSteps = [
|
||||
'scan-qr-code',
|
||||
'save-secret-key',
|
||||
'verification',
|
||||
] as const;
|
||||
|
||||
type ActivationStep = typeof activationSteps[number];
|
||||
|
||||
export class TwoFactorActivation {
|
||||
public readonly type = 'two-factor-activation' as const;
|
||||
|
||||
private _step: ActivationStep;
|
||||
|
||||
private _secretKey: string;
|
||||
private _authCode: string;
|
||||
private _2FAVerification: 'none' | 'invalid' | 'valid' = 'none';
|
||||
|
||||
constructor(
|
||||
private _cancelActivation: () => void,
|
||||
private _enable2FA: (secretKey: string) => void
|
||||
) {
|
||||
this._secretKey = 'FHJJSAJKDASKW43KJS';
|
||||
this._authCode = getNewAuthCode();
|
||||
this._step = 'scan-qr-code';
|
||||
|
||||
makeAutoObservable<
|
||||
TwoFactorActivation,
|
||||
'_secretKey' | '_authCode' | '_step' | '_enable2FAVerification'
|
||||
>(
|
||||
this,
|
||||
{
|
||||
_secretKey: observable,
|
||||
_authCode: observable,
|
||||
_step: observable,
|
||||
_enable2FAVerification: observable,
|
||||
},
|
||||
{ autoBind: true }
|
||||
);
|
||||
}
|
||||
|
||||
get secretKey() {
|
||||
return this._secretKey;
|
||||
}
|
||||
|
||||
get authCode() {
|
||||
return this._authCode;
|
||||
}
|
||||
|
||||
get step() {
|
||||
return this._step;
|
||||
}
|
||||
|
||||
get verificationStatus() {
|
||||
return this._2FAVerification;
|
||||
}
|
||||
|
||||
cancelActivation() {
|
||||
this._cancelActivation();
|
||||
}
|
||||
|
||||
openScanQRCode() {
|
||||
this._step = 'scan-qr-code';
|
||||
}
|
||||
|
||||
openSaveSecretKey() {
|
||||
this._step = 'save-secret-key';
|
||||
}
|
||||
|
||||
openVerification() {
|
||||
this._step = 'verification';
|
||||
this._2FAVerification = 'none';
|
||||
}
|
||||
|
||||
enable2FA(secretKey: string, authCode: string) {
|
||||
if (secretKey === this._secretKey && authCode === this._authCode) {
|
||||
this._2FAVerification = 'valid';
|
||||
this._enable2FA(secretKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Change to invalid upon implementation
|
||||
this._2FAVerification = 'valid';
|
||||
// Remove after implementation
|
||||
this._enable2FA(secretKey);
|
||||
}
|
||||
}
|
||||
|
||||
export class TwoFactorEnabled {
|
||||
public readonly type = 'two-factor-enabled' as const;
|
||||
private _secretKey: string;
|
||||
private _authCode: string;
|
||||
|
||||
constructor(secretKey: string) {
|
||||
this._secretKey = secretKey;
|
||||
this._authCode = getNewAuthCode();
|
||||
|
||||
makeAutoObservable<TwoFactorEnabled, '_secretKey' | '_authCode'>(this, {
|
||||
_secretKey: observable,
|
||||
_authCode: observable,
|
||||
});
|
||||
}
|
||||
|
||||
get secretKey() {
|
||||
return this._secretKey;
|
||||
}
|
||||
|
||||
get authCode() {
|
||||
return this._authCode;
|
||||
}
|
||||
|
||||
refreshAuthCode() {
|
||||
this._authCode = getNewAuthCode();
|
||||
}
|
||||
}
|
||||
|
||||
type TwoFactorStatus =
|
||||
| TwoFactorEnabled
|
||||
| TwoFactorActivation
|
||||
| 'two-factor-disabled';
|
||||
|
||||
export const is2FADisabled = (s: TwoFactorStatus): s is 'two-factor-disabled' =>
|
||||
s === 'two-factor-disabled';
|
||||
|
||||
export const is2FAActivation = (s: TwoFactorStatus): s is TwoFactorActivation =>
|
||||
(s as any).type === 'two-factor-activation';
|
||||
|
||||
export const is2FAEnabled = (s: TwoFactorStatus): s is TwoFactorEnabled =>
|
||||
(s as any).type === 'two-factor-enabled';
|
||||
|
||||
export class TwoFactorAuth {
|
||||
private _status: TwoFactorStatus = 'two-factor-disabled';
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable<TwoFactorAuth, '_status'>(this, {
|
||||
_status: observable,
|
||||
});
|
||||
}
|
||||
|
||||
private startActivation() {
|
||||
const cancel = action(() => (this._status = 'two-factor-disabled'));
|
||||
const enable = action(
|
||||
(secretKey: string) => (this._status = new TwoFactorEnabled(secretKey))
|
||||
);
|
||||
this._status = new TwoFactorActivation(cancel, enable);
|
||||
}
|
||||
|
||||
private deactivate2FA() {
|
||||
this._status = 'two-factor-disabled';
|
||||
}
|
||||
|
||||
toggle2FA() {
|
||||
if (this._status === 'two-factor-disabled') this.startActivation();
|
||||
else this.deactivate2FA();
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this._status;
|
||||
}
|
||||
}
|
||||
71
app/assets/javascripts/preferences/preferences-menu.ts
Normal file
71
app/assets/javascripts/preferences/preferences-menu.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { IconType } from '@/components/Icon';
|
||||
import { makeAutoObservable, observable } from 'mobx';
|
||||
|
||||
const PREFERENCE_IDS = [
|
||||
'general',
|
||||
'account',
|
||||
'appearance',
|
||||
'security',
|
||||
'listed',
|
||||
'shortcuts',
|
||||
'accessibility',
|
||||
'get-free-month',
|
||||
'help-feedback',
|
||||
] as const;
|
||||
|
||||
export type PreferenceId = typeof PREFERENCE_IDS[number];
|
||||
interface PreferencesMenuItem {
|
||||
readonly id: PreferenceId;
|
||||
readonly icon: IconType;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Items are in order of appearance
|
||||
*/
|
||||
const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'general', label: 'General', icon: 'settings' },
|
||||
{ id: 'account', label: 'Account', icon: 'user' },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
|
||||
{ id: 'security', label: 'Security', icon: 'security' },
|
||||
{ id: 'listed', label: 'Listed', icon: 'listed' },
|
||||
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard' },
|
||||
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility' },
|
||||
{ id: 'get-free-month', label: 'Get a free month', icon: 'star' },
|
||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help' },
|
||||
];
|
||||
|
||||
export class PreferencesMenu {
|
||||
private _selectedPane: PreferenceId = 'general';
|
||||
|
||||
constructor(
|
||||
private readonly _menu: PreferencesMenuItem[] = PREFERENCES_MENU_ITEMS
|
||||
) {
|
||||
makeAutoObservable<PreferencesMenu, '_selectedPane' | '_twoFactorAuth'>(
|
||||
this,
|
||||
{
|
||||
_twoFactorAuth: observable,
|
||||
_selectedPane: observable,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get menuItems(): (PreferencesMenuItem & {
|
||||
selected: boolean;
|
||||
})[] {
|
||||
return this._menu.map((p) => ({
|
||||
...p,
|
||||
selected: p.id === this._selectedPane,
|
||||
}));
|
||||
}
|
||||
|
||||
get selectedPaneId(): PreferenceId {
|
||||
return (
|
||||
this._menu.find((item) => item.id === this._selectedPane)?.id ?? 'general'
|
||||
);
|
||||
}
|
||||
|
||||
selectPane(key: PreferenceId) {
|
||||
this._selectedPane = key;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,8 @@ export function StringEmptyTrash(count: number) {
|
||||
/** @account */
|
||||
export const STRING_ACCOUNT_MENU_UNCHECK_MERGE =
|
||||
'Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?';
|
||||
export const STRING_SIGN_OUT_CONFIRMATION = 'This will delete all local items and extensions.';
|
||||
export const STRING_SIGN_OUT_CONFIRMATION =
|
||||
'This will delete all local items and extensions.';
|
||||
export const STRING_ERROR_DECRYPTING_IMPORT =
|
||||
'There was an error decrypting your items. Make sure the password you entered is correct and try again.';
|
||||
export const STRING_E2E_ENABLED =
|
||||
@@ -111,7 +112,8 @@ export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT =
|
||||
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade';
|
||||
|
||||
export const Strings = {
|
||||
protectingNoteWithoutProtectionSources: 'Access to this note will not be restricted until you set up a passcode or account.',
|
||||
protectingNoteWithoutProtectionSources:
|
||||
'Access to this note will not be restricted until you set up a passcode or account.',
|
||||
openAccountMenu: 'Open Account Menu',
|
||||
trashNotesTitle: 'Move to Trash',
|
||||
trashNotesText: 'Are you sure you want to move these notes to the trash?',
|
||||
@@ -157,4 +159,4 @@ export const StringUtils = {
|
||||
? "This note has editing disabled. If you'd like to delete it, enable editing, and try again."
|
||||
: "One or more of these notes have editing disabled. If you'd like to delete them, make sure editing is enabled on all of them, and try again.";
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
)
|
||||
preferences(
|
||||
app-state='self.appState'
|
||||
application='self.application'
|
||||
)
|
||||
challenge-modal(
|
||||
ng-repeat="challenge in self.challenges track by challenge.id"
|
||||
@@ -37,6 +38,7 @@
|
||||
on-dismiss="self.removeChallenge(challenge)"
|
||||
)
|
||||
notes-context-menu(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#editor-column.section.editor.sn-component(aria-label='Note')
|
||||
protected-note-panel.h-full.flex.justify-center.items-center(
|
||||
ng-if='self.appState.notes.showProtectedWarning'
|
||||
ng-if='self.state.showProtectedWarning'
|
||||
app-state='self.appState'
|
||||
on-view-note='self.dismissProtectedWarning()'
|
||||
)
|
||||
@@ -49,6 +49,7 @@
|
||||
) {{self.state.noteStatus.message}}
|
||||
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
|
||||
notes-options-panel(
|
||||
application='self.application',
|
||||
app-state='self.appState',
|
||||
ng-if='self.appState.notes.selectedNotesCount > 0'
|
||||
)
|
||||
|
||||
@@ -64,7 +64,6 @@ type EditorState = {
|
||||
showOptionsMenu: boolean;
|
||||
showEditorMenu: boolean;
|
||||
showHistoryMenu: boolean;
|
||||
altKeyDown: boolean;
|
||||
spellcheck: boolean;
|
||||
/**
|
||||
* Setting to false then true will allow the current editor component-view to be destroyed
|
||||
@@ -74,6 +73,7 @@ type EditorState = {
|
||||
/** Setting to true then false will allow the main content textarea to be destroyed
|
||||
* then re-initialized. Used when reloading spellcheck status. */
|
||||
textareaUnloading: boolean;
|
||||
showProtectedWarning: boolean;
|
||||
};
|
||||
|
||||
type EditorValues = {
|
||||
@@ -106,7 +106,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
onEditorLoad?: () => void;
|
||||
|
||||
private scrollPosition = 0;
|
||||
private removeAltKeyObserver?: any;
|
||||
private removeTrashKeyObserver?: any;
|
||||
private removeTabObserver?: any;
|
||||
|
||||
@@ -143,8 +142,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.editor.clearNoteChangeListener();
|
||||
this.removeComponentsObserver();
|
||||
(this.removeComponentsObserver as any) = undefined;
|
||||
this.removeAltKeyObserver();
|
||||
this.removeAltKeyObserver = undefined;
|
||||
this.removeTrashKeyObserver();
|
||||
this.removeTrashKeyObserver = undefined;
|
||||
this.removeTabObserver && this.removeTabObserver();
|
||||
@@ -203,6 +200,11 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
}
|
||||
});
|
||||
this.autorun(() => {
|
||||
this.setState({
|
||||
showProtectedWarning: this.appState.notes.showProtectedWarning
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
@@ -217,10 +219,10 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
showOptionsMenu: false,
|
||||
showEditorMenu: false,
|
||||
showHistoryMenu: false,
|
||||
altKeyDown: false,
|
||||
noteStatus: undefined,
|
||||
editorUnloading: false,
|
||||
textareaUnloading: false,
|
||||
showProtectedWarning: false,
|
||||
} as EditorState;
|
||||
}
|
||||
|
||||
@@ -277,7 +279,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
showOptionsMenu: false,
|
||||
showEditorMenu: false,
|
||||
showHistoryMenu: false,
|
||||
altKeyDown: false,
|
||||
noteStatus: undefined,
|
||||
});
|
||||
this.editorValues.title = note.title;
|
||||
@@ -613,7 +614,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
setShowProtectedWarning(show: boolean) {
|
||||
this.application.getAppState().notes.setShowProtectedWarning(show);
|
||||
this.appState.notes.setShowProtectedWarning(show);
|
||||
}
|
||||
|
||||
async deleteNote(permanently: boolean) {
|
||||
@@ -843,22 +844,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
registerKeyboardShortcuts() {
|
||||
this.removeAltKeyObserver = this.application
|
||||
.io
|
||||
.addKeyObserver({
|
||||
modifiers: [KeyboardModifier.Alt],
|
||||
onKeyDown: () => {
|
||||
this.setState({
|
||||
altKeyDown: true,
|
||||
});
|
||||
},
|
||||
onKeyUp: () => {
|
||||
this.setState({
|
||||
altKeyDown: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
this.removeTrashKeyObserver = this.application
|
||||
.io
|
||||
.addKeyObserver({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.h-full
|
||||
multiple-selected-notes-panel.h-full(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
ng-if='self.state.showMultipleSelectedNotes'
|
||||
)
|
||||
|
||||
@@ -199,6 +199,10 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.fill-current {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
target='blank'
|
||||
)
|
||||
menu-row(label="'Download Actions'")
|
||||
div(ng-repeat='extension in self.state.menu track by extension.uuid')
|
||||
div(ng-repeat='extension in self.state.menuItems track by extension.uuid')
|
||||
.sk-menu-panel-header(
|
||||
ng-click='self.toggleExtensionVisibility(extension.uuid); $event.stopPropagation();'
|
||||
)
|
||||
@@ -33,11 +33,11 @@
|
||||
menu-row(
|
||||
faded='true',
|
||||
label="'No Actions Available'",
|
||||
ng-if='!extension.actions.length'
|
||||
ng-if='!extension.actions.length && !extension.hidden'
|
||||
)
|
||||
menu-row(
|
||||
faded='true',
|
||||
label="'Error loading actions'",
|
||||
subtitle="'Please try again later.'"
|
||||
ng-if='extension.error'
|
||||
ng-if='extension.error && !extension.hidden'
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "standard-notes-web",
|
||||
"version": "3.8.18",
|
||||
"version": "3.8.21",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -55,7 +55,7 @@
|
||||
"pug-loader": "^2.4.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"serve-static": "^1.14.1",
|
||||
"sn-stylekit": "5.2.5",
|
||||
"sn-stylekit": "5.2.8",
|
||||
"ts-loader": "^8.0.17",
|
||||
"typescript": "4.2.3",
|
||||
"typescript-eslint": "0.0.1-alpha.0",
|
||||
@@ -71,8 +71,8 @@
|
||||
"@reach/checkbox": "^0.13.2",
|
||||
"@reach/dialog": "^0.13.0",
|
||||
"@standardnotes/sncrypto-web": "1.2.10",
|
||||
"@standardnotes/snjs": "2.7.21",
|
||||
"mobx": "^6.1.6",
|
||||
"@standardnotes/snjs": "2.7.23",
|
||||
"mobx": "^6.3.2",
|
||||
"mobx-react-lite": "^3.2.0",
|
||||
"preact": "^10.5.12"
|
||||
}
|
||||
|
||||
30
yarn.lock
30
yarn.lock
@@ -2016,6 +2016,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.1.1.tgz#834701c2e14d31eb204bff90457fa05e9183464a"
|
||||
integrity sha512-E9zDYZ1gJkVZBEzd7a1L2haQ4GYeH1lUrY87UmDH1AMYUHW+c0SqZ71af1fBNqGzrx3EZSXk+Qzr7RyOa6N1Mw==
|
||||
|
||||
"@standardnotes/features@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.0.0.tgz#906af029b6e58241689ca37436982c37a888a418"
|
||||
integrity sha512-PEQyP/p/TQLVcNYcbu9jEIWNRqBrFFG1Qyy8QIcvNUt5o4lpLZGEY1T+PJUsPSisnuKKNpQrgVLc9LjhUKpuYw==
|
||||
|
||||
"@standardnotes/sncrypto-common@^1.2.7", "@standardnotes/sncrypto-common@^1.2.9":
|
||||
version "1.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.2.9.tgz#5212a959e4ec563584e42480bfd39ef129c3cbdf"
|
||||
@@ -2029,12 +2034,13 @@
|
||||
"@standardnotes/sncrypto-common" "^1.2.7"
|
||||
libsodium-wrappers "^0.7.8"
|
||||
|
||||
"@standardnotes/snjs@2.7.21":
|
||||
version "2.7.21"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.21.tgz#db451e5facaf5fa41fa509eb1f304723929c3541"
|
||||
integrity sha512-GhkGk1LJmD494COZkSOgyHaUnGnLWNLlSuCZMTwbw3dgkN5PjobbRhfDvEZaLqjwok+h9nkiQt3hugQ3h6Cy5w==
|
||||
"@standardnotes/snjs@2.7.23":
|
||||
version "2.7.23"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.23.tgz#fedc9c025301dbe20ed2d598fb378e36f90ff64e"
|
||||
integrity sha512-eoEwKlV2PZcJXFbCt6bgovu9nldVoT7DPoterTBo/NZ4odRILOwxLA1SAgL5H5FYPb9NHkwaaCt9uTdIqdNYhA==
|
||||
dependencies:
|
||||
"@standardnotes/auth" "3.1.1"
|
||||
"@standardnotes/features" "1.0.0"
|
||||
"@standardnotes/sncrypto-common" "^1.2.9"
|
||||
|
||||
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
|
||||
@@ -6192,10 +6198,10 @@ mobx-react-lite@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz#331d7365a6b053378dfe9c087315b4e41c5df69f"
|
||||
integrity sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==
|
||||
|
||||
mobx@^6.1.6:
|
||||
version "6.1.6"
|
||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.1.6.tgz#ae75e57ec07d190ed187273864002163fa357224"
|
||||
integrity sha512-3by0Avodad/3cdPAuJuj8jWXhe1YByHKaEkonD9yOdcMoMuot2jrjlSXmQPhR1bJpNHfSsOx122tM9Pv3IzFWA==
|
||||
mobx@^6.3.2:
|
||||
version "6.3.2"
|
||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.2.tgz#125590961f702a572c139ab69392bea416d2e51b"
|
||||
integrity sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg==
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -7908,10 +7914,10 @@ slice-ansi@^4.0.0:
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
sn-stylekit@5.2.5:
|
||||
version "5.2.5"
|
||||
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.5.tgz#85a28da395fedbaae9f7a91c48648042cdcc8052"
|
||||
integrity sha512-8J+8UtRvukyJOBp79RcD4IZrvJJbjYY6EdN4N125K0xW84nDjgURuPuCjwm4lnp6vcXODU6r5d3JMDJoXYq8wA==
|
||||
sn-stylekit@5.2.8:
|
||||
version "5.2.8"
|
||||
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.8.tgz#320d7f536036110fe57e0392cb7ff52076186d9a"
|
||||
integrity sha512-08eKl2Eigb8kzxl+Tqp7PyzSVA0psCR6+hIVNL4V7//J7DjmM2RanBPMRxIUqBBTX/1b+CbZdZb8wnbzD18NZw==
|
||||
dependencies:
|
||||
"@reach/listbox" "^0.15.0"
|
||||
"@reach/menu-button" "^0.15.1"
|
||||
|
||||
Reference in New Issue
Block a user