Merge branch 'release/10.1.0'

This commit is contained in:
Mo Bitar
2021-12-06 08:50:50 -06:00
42 changed files with 3738 additions and 602 deletions

View File

@@ -11,7 +11,7 @@
"parserOptions": {
"project": "./app/assets/javascripts/tsconfig.json"
},
"ignorePatterns": [".eslintrc.js", "webpack.*.js", "webpack-defaults.js"],
"ignorePatterns": [".eslintrc.js", "webpack.*.js", "webpack-defaults.js", "jest.config.js"],
"rules": {
"standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals
"no-throw-literal": 0,

13
.github/workflows/git-sync.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
on: push
jobs:
git-sync:
runs-on: ubuntu-latest
steps:
- name: git-sync
uses: wei/git-sync@v3
with:
source_repo: "standardnotes/web"
source_branch: "refs/remotes/source/*"
destination_repo: "standardnotes/private-web"
destination_branch: "refs/heads/*"
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}

2
.gitignore vendored
View File

@@ -4,6 +4,8 @@
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
.eslintcache
# OS & IDE
.DS_Store
.idea

1
.husky/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_

5
.husky/pre-commit Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# npx pretty-quick --staged # trying lint-staged for now, it is slower but uses eslint.
npx lint-staged

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.5 13.0083L16.325 14.1667L12.15 10L16.325 5.83333L17.5 6.99167L14.5333 10L17.5 13.0083ZM2.5 5H13.3333V6.66667H2.5V5ZM2.5 10.8333V9.16667H10.8333V10.8333H2.5ZM2.5 15V13.3333H13.3333V15H2.5Z" />
</svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.36" x="1" y="1" width="18" height="18" rx="9" fill="#BBBEC4" />
<path
d="M6.6665 12H7.99984C7.99984 12.72 8.91317 13.3333 9.99984 13.3333C11.0865 13.3333 11.9998 12.72 11.9998 12C11.9998 11.2667 11.3065 11 9.83984 10.6467C8.4265 10.2933 6.6665 9.85333 6.6665 8C6.6665 6.80667 7.6465 5.79333 8.99984 5.45333V4H10.9998V5.45333C12.3532 5.79333 13.3332 6.80667 13.3332 8H11.9998C11.9998 7.28 11.0865 6.66667 9.99984 6.66667C8.91317 6.66667 7.99984 7.28 7.99984 8C7.99984 8.73333 8.69317 9 10.1598 9.35333C11.5732 9.70667 13.3332 10.1467 13.3332 12C13.3332 13.1933 12.3532 14.2067 10.9998 14.5467V16H8.99984V14.5467C7.6465 14.2067 6.6665 13.1933 6.6665 12Z"
fill="#72767E" />
</svg>

After

Width:  |  Height:  |  Size: 779 B

View File

@@ -78,8 +78,10 @@ import { PreferencesDirective } from './preferences';
import { AppVersion, IsWebPlatform } from '@/version';
import { NotesListOptionsDirective } from './components/NotesListOptionsMenu';
import { PurchaseFlowDirective } from './purchaseFlow';
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu';
import { ComponentViewDirective } from '@/components/ComponentView';
import { TagsListDirective } from '@/components/TagsList';
import { PinNoteButtonDirective } from '@/components/PinNoteButton';
function reloadHiddenFirefoxTab(): boolean {
/**
@@ -181,8 +183,10 @@ const startApplication: StartApplication = async function startApplication(
.directive('notesListOptionsMenu', NotesListOptionsDirective)
.directive('icon', IconDirective)
.directive('noteTagsContainer', NoteTagsContainerDirective)
.directive('tags', TagsListDirective)
.directive('preferences', PreferencesDirective)
.directive('purchaseFlow', PurchaseFlowDirective);
.directive('purchaseFlow', PurchaseFlowDirective)
.directive('pinNoteButton', PinNoteButtonDirective);
// Filters
angular.module('app').filter('trusted', ['$sce', trusted]);

View File

@@ -34,7 +34,6 @@ const avoidFlickerTimeout = 7;
export const ComponentView: FunctionalComponent<IProps> = observer(
({
application,
appState,
onLoad,
componentUuid,
templateComponent
@@ -45,7 +44,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
const [isIssueOnLoading, setIsIssueOnLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isReloading, setIsReloading] = useState(false);
const [loadTimeout, setLoadTimeout] = useState<number | undefined>(undefined);
const [loadTimeout, setLoadTimeout] = useState<ReturnType<typeof setTimeout> | undefined>(undefined);
const [featureStatus, setFeatureStatus] = useState<FeatureStatus | undefined>(FeatureStatus.Entitled);
const [isComponentValid, setIsComponentValid] = useState(true);
const [error, setError] = useState<'offline-restricted' | 'url-missing' | undefined>(undefined);
@@ -158,7 +157,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
} catch (e) {
}
}
clearTimeout(loadTimeout);
loadTimeout && clearTimeout(loadTimeout);
await application.componentManager.registerComponentWindow(
component,
iframe.contentWindow!
@@ -194,6 +193,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
useEffect(() => {
reloadStatus();
if (!iframeRef.current) {
return;
}
@@ -250,7 +250,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
onVisibilityChange
);
};
}, [appState, application, component, componentUuid, onVisibilityChange, reloadStatus, templateComponent]);
}, [application, component, componentUuid, onVisibilityChange, templateComponent]);
useEffect(() => {
// Set/update `component` based on `componentUuid` prop.

View File

@@ -1,3 +1,4 @@
import PremiumFeatureIcon from '../../icons/ic-premium-feature.svg';
import PencilOffIcon from '../../icons/ic-pencil-off.svg';
import PlainTextIcon from '../../icons/ic-text-paragraph.svg';
import RichTextIcon from '../../icons/ic-text-rich.svg';
@@ -15,6 +16,7 @@ import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
import MoreIcon from '../../icons/ic-more.svg';
import TuneIcon from '../../icons/ic-tune.svg';
import MenuArrowDownIcon from '../../icons/ic-menu-arrow-down.svg';
import MenuCloseIcon from '../../icons/ic-menu-close.svg';
import AuthenticatorIcon from '../../icons/ic-authenticator.svg';
import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg';
import TasksIcon from '../../icons/ic-tasks.svg';
@@ -107,7 +109,9 @@ const ICONS = {
'check-bold': CheckBoldIcon,
'account-circle': AccountCircleIcon,
'menu-arrow-down': MenuArrowDownIcon,
window: WindowIcon
'menu-close': MenuCloseIcon,
window: WindowIcon,
'premium-feature': PremiumFeatureIcon
};
export type IconType = keyof typeof ICONS;

View File

@@ -4,6 +4,7 @@ import NotesIcon from '../../icons/il-notes.svg';
import { observer } from 'mobx-react-lite';
import { NotesOptionsPanel } from './NotesOptionsPanel';
import { WebApplication } from '@/ui_models/application';
import { PinNoteButton } from './PinNoteButton';
type Props = {
application: WebApplication;
@@ -17,13 +18,16 @@ const MultipleSelectedNotes = observer(({ application, appState }: Props) => {
<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 application={application} appState={appState} />
<div className="flex">
<div className="mr-3">
<PinNoteButton appState={appState} />
</div>
<NotesOptionsPanel application={application} appState={appState} />
</div>
</div>
<div className="flex-grow flex flex-col justify-center items-center w-full max-w-md">
<NotesIcon className="block" />
<h2 className="text-lg m-0 text-center mt-4">
{count} selected notes
</h2>
<h2 className="text-lg m-0 text-center mt-4">{count} selected notes</h2>
<p className="text-sm mt-2 text-center max-w-60">
Actions will be performed on all selected notes.
</p>

View File

@@ -0,0 +1,38 @@
import { AppState } from '@/ui_models/app_state';
import VisuallyHidden from '@reach/visually-hidden';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { Icon } from './Icon';
import { toDirective } from './utils';
type Props = {
appState: AppState;
className?: string;
};
export const PinNoteButton: FunctionComponent<Props> = observer(
({ appState, className = '' }) => {
const notes = Object.values(appState.notes.selectedNotes);
const pinned = notes.some((note) => note.pinned);
const togglePinned = () => {
if (!pinned) {
appState.notes.setPinSelectedNotes(true);
} else {
appState.notes.setPinSelectedNotes(false);
}
};
return (
<button
className={`sn-icon-button ${pinned ? 'toggled' : ''} ${className}`}
onClick={togglePinned}
>
<VisuallyHidden>Pin selected notes</VisuallyHidden>
<Icon type="pin" className="block" />
</button>
);
}
);
export const PinNoteButtonDirective = toDirective<Props>(PinNoteButton);

View File

@@ -0,0 +1,75 @@
import {
AlertDialog,
AlertDialogDescription,
AlertDialogLabel,
} from '@reach/alert-dialog';
import { FunctionalComponent } from 'preact';
import { Icon } from './Icon';
import PremiumIllustration from '../../svg/il-premium.svg';
import { useRef } from 'preact/hooks';
type Props = {
featureName: string;
onClose: () => void;
showModal: boolean;
};
export const PremiumFeaturesModal: FunctionalComponent<Props> = ({
featureName,
onClose,
showModal,
}) => {
const plansButtonRef = useRef<HTMLButtonElement>(null);
const onClickPlans = () => {
if (window._plans_url) {
window.location.assign(window._plans_url);
}
};
return showModal ? (
<AlertDialog leastDestructiveRef={plansButtonRef}>
<div tabIndex={-1} className="sn-component">
<div
tabIndex={0}
className="max-w-89 bg-default rounded shadow-overlay p-4"
>
<AlertDialogLabel>
<div className="flex justify-end p-1">
<button
className="flex p-0 cursor-pointer bg-transparent border-0"
onClick={onClose}
aria-label="Close modal"
>
<Icon className="color-neutral" type="close" />
</button>
</div>
<div
className="flex items-center justify-center p-1"
aria-hidden={true}
>
<PremiumIllustration className="mb-2" />
</div>
<div className="text-lg text-center font-bold mb-1">
Enable premium features
</div>
</AlertDialogLabel>
<AlertDialogDescription className="text-sm text-center color-grey-1 px-4.5 mb-2">
In order to use <span className="font-semibold">{featureName}</span>{' '}
and other premium features, please purchase a subscription or
upgrade your current plan.
</AlertDialogDescription>
<div className="p-4">
<button
onClick={onClickPlans}
className="w-full rounded no-border py-2 font-bold bg-info color-info-contrast hover:brightness-130 focus:brightness-130 cursor-pointer"
ref={plansButtonRef}
>
See our plans
</button>
</div>
</div>
</div>
</AlertDialog>
) : null;
};

View File

@@ -0,0 +1,66 @@
import { WebApplication } from '@/ui_models/application';
import { FeatureIdentifier } from '@standardnotes/features';
import { FeatureStatus } from '@standardnotes/snjs';
import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { JSXInternal } from 'preact/src/jsx';
import { Icon } from '../Icon';
import { PremiumFeaturesModal } from '../PremiumFeaturesModal';
import { Switch } from '../Switch';
type Props = {
application: WebApplication;
closeQuickSettingsMenu: () => void;
focusModeEnabled: boolean;
setFocusModeEnabled: (enabled: boolean) => void;
};
export const FocusModeSwitch: FunctionComponent<Props> = ({
application,
closeQuickSettingsMenu,
focusModeEnabled,
setFocusModeEnabled,
}) => {
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const isEntitledToFocusMode =
application.getFeatureStatus(FeatureIdentifier.FocusMode) ===
FeatureStatus.Entitled;
const toggleFocusMode = (
e: JSXInternal.TargetedMouseEvent<HTMLButtonElement>
) => {
e.preventDefault();
if (isEntitledToFocusMode) {
setFocusModeEnabled(!focusModeEnabled);
closeQuickSettingsMenu();
} else {
setShowUpgradeModal(true);
}
};
return (
<>
<button
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between"
onClick={toggleFocusMode}
>
<div className="flex items-center">
<Icon type="menu-close" className="color-neutral mr-2" />
Focused Writing
</div>
{isEntitledToFocusMode ? (
<Switch className="px-0" checked={focusModeEnabled} />
) : (
<div title="Premium feature">
<Icon type="premium-feature" />
</div>
)}
</button>
<PremiumFeaturesModal
showModal={showUpgradeModal}
featureName="Focus Mode"
onClose={() => setShowUpgradeModal(false)}
/>
</>
);
};

View File

@@ -15,81 +15,48 @@ import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { JSXInternal } from 'preact/src/jsx';
import { Icon } from './Icon';
import { Switch } from './Switch';
import { toDirective, useCloseOnBlur } from './utils';
import { Icon } from '../Icon';
import { Switch } from '../Switch';
import { toDirective, useCloseOnBlur } from '../utils';
import {
quickSettingsKeyDownHandler,
themesMenuKeyDownHandler,
} from './eventHandlers';
import { FocusModeSwitch } from './FocusModeSwitch';
import { ThemesMenuButton } from './ThemesMenuButton';
const focusModeAnimationDuration = 1255;
const MENU_CLASSNAME =
'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto';
type ThemeButtonProps = {
theme: SNTheme;
application: WebApplication;
onBlur: (event: { relatedTarget: EventTarget | null }) => void;
};
type MenuProps = {
appState: AppState;
application: WebApplication;
};
const ThemeButton: FunctionComponent<ThemeButtonProps> = ({
application,
theme,
onBlur,
}) => {
const toggleTheme = (e: any) => {
e.preventDefault();
if (theme.isLayerable() || !theme.active) {
application.toggleComponent(theme);
const toggleFocusMode = (enabled: boolean) => {
if (enabled) {
document.body.classList.add('focus-mode');
} else {
if (document.body.classList.contains('focus-mode')) {
document.body.classList.add('disable-focus-mode');
document.body.classList.remove('focus-mode');
setTimeout(() => {
document.body.classList.remove('disable-focus-mode');
}, focusModeAnimationDuration);
}
};
return (
<button
className={`sn-dropdown-item focus:bg-info-backdrop focus:shadow-none ${
theme.isLayerable() ? `justify-start` : `justify-between`
}`}
onClick={toggleTheme}
onBlur={onBlur}
>
{theme.isLayerable() ? (
<>
<Switch
className="px-0 mr-2"
checked={theme.active}
onChange={toggleTheme}
/>
{theme.package_info.name}
</>
) : (
<>
<div className="flex items-center">
<div
className={`pseudo-radio-btn ${
theme.active ? 'pseudo-radio-btn--checked' : ''
} mr-2`}
></div>
<span className={theme.active ? 'font-semibold' : undefined}>
{theme.package_info.name}
</span>
</div>
<div
className="w-5 h-5 rounded-full"
style={{
backgroundColor: theme.package_info?.dock_icon?.background_color,
}}
></div>
</>
)}
</button>
);
}
};
const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
({ application, appState }) => {
const { closeQuickSettingsMenu, shouldAnimateCloseMenu } =
appState.quickSettingsMenu;
const {
closeQuickSettingsMenu,
shouldAnimateCloseMenu,
focusModeEnabled,
setFocusModeEnabled,
} = appState.quickSettingsMenu;
const [themes, setThemes] = useState<SNTheme[]>([]);
const [toggleableComponents, setToggleableComponents] = useState<
SNComponent[]
@@ -104,6 +71,10 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
const quickSettingsMenuRef = useRef<HTMLDivElement>(null);
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
toggleFocusMode(focusModeEnabled);
}, [focusModeEnabled]);
const reloadThemes = useCallback(() => {
application.streamItems(ContentType.Theme, () => {
const themes = application.getDisplayableItems(
@@ -157,12 +128,12 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
useEffect(() => {
if (themesMenuOpen) {
defaultThemeButtonRef.current!.focus();
defaultThemeButtonRef.current?.focus();
}
}, [themesMenuOpen]);
useEffect(() => {
prefsButtonRef.current!.focus();
prefsButtonRef.current?.focus();
}, []);
const [closeOnBlur] = useCloseOnBlur(
@@ -171,9 +142,9 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
);
const toggleThemesMenu = () => {
if (!themesMenuOpen) {
if (!themesMenuOpen && themesButtonRef.current) {
const themesButtonRect =
themesButtonRef.current!.getBoundingClientRect();
themesButtonRef.current.getBoundingClientRect();
setThemesMenuPosition({
left: themesButtonRect.right,
bottom:
@@ -200,7 +171,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
switch (event.key) {
case 'Escape':
setThemesMenuOpen(false);
themesButtonRef.current!.focus();
themesButtonRef.current?.focus();
break;
case 'ArrowRight':
if (!themesMenuOpen) {
@@ -211,65 +182,23 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> =
(event) => {
const items: NodeListOf<HTMLButtonElement> =
quickSettingsMenuRef.current!.querySelectorAll(':scope > button');
const currentFocusedIndex = Array.from(items).findIndex(
(btn) => btn === document.activeElement
quickSettingsKeyDownHandler(
closeQuickSettingsMenu,
event,
quickSettingsMenuRef,
themesMenuOpen
);
if (!themesMenuOpen) {
switch (event.key) {
case 'Escape':
closeQuickSettingsMenu();
break;
case 'ArrowDown':
if (items[currentFocusedIndex + 1]) {
items[currentFocusedIndex + 1].focus();
} else {
items[0].focus();
}
break;
case 'ArrowUp':
if (items[currentFocusedIndex - 1]) {
items[currentFocusedIndex - 1].focus();
} else {
items[items.length - 1].focus();
}
break;
}
}
};
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (
event
) => {
const themes = themesMenuRef.current!.querySelectorAll('button');
const currentFocusedIndex = Array.from(themes).findIndex(
(themeBtn) => themeBtn === document.activeElement
themesMenuKeyDownHandler(
event,
themesMenuRef,
setThemesMenuOpen,
themesButtonRef
);
switch (event.key) {
case 'Escape':
case 'ArrowLeft':
event.stopPropagation();
setThemesMenuOpen(false);
themesButtonRef.current!.focus();
break;
case 'ArrowDown':
if (themes[currentFocusedIndex + 1]) {
themes[currentFocusedIndex + 1].focus();
} else {
themes[0].focus();
}
break;
case 'ArrowUp':
if (themes[currentFocusedIndex - 1]) {
themes[currentFocusedIndex - 1].focus();
} else {
themes[themes.length - 1].focus();
}
break;
}
};
const toggleDefaultTheme = () => {
@@ -332,7 +261,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
Default
</button>
{themes.map((theme) => (
<ThemeButton
<ThemesMenuButton
theme={theme}
application={application}
key={theme.uuid}
@@ -341,7 +270,6 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
))}
</DisclosurePanel>
</Disclosure>
{toggleableComponents.map((component) => (
<Switch
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
@@ -356,10 +284,15 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
</div>
</Switch>
))}
<FocusModeSwitch
application={application}
closeQuickSettingsMenu={closeQuickSettingsMenu}
focusModeEnabled={focusModeEnabled}
setFocusModeEnabled={setFocusModeEnabled}
/>
<div className="h-1px my-2 bg-border"></div>
<button
class="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
onClick={openPreferences}
ref={prefsButtonRef}
>

View File

@@ -0,0 +1,60 @@
import { WebApplication } from '@/ui_models/application';
import { SNTheme } from '@standardnotes/snjs';
import { FunctionComponent } from 'preact';
import { JSXInternal } from 'preact/src/jsx';
import { Switch } from '../Switch';
type Props = {
theme: SNTheme;
application: WebApplication;
onBlur: (event: { relatedTarget: EventTarget | null }) => void;
};
export const ThemesMenuButton: FunctionComponent<Props> = ({
application,
theme,
onBlur,
}) => {
const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
if (theme.isLayerable() || !theme.active) {
application.toggleComponent(theme);
}
};
return (
<button
className={`sn-dropdown-item focus:bg-info-backdrop focus:shadow-none ${
theme.isLayerable() ? `justify-start` : `justify-between`
}`}
onClick={toggleTheme}
onBlur={onBlur}
>
{theme.isLayerable() ? (
<>
<Switch className="px-0 mr-2" checked={theme.active} />
{theme.package_info.name}
</>
) : (
<>
<div className="flex items-center">
<div
className={`pseudo-radio-btn ${
theme.active ? 'pseudo-radio-btn--checked' : ''
} mr-2`}
></div>
<span className={theme.active ? 'font-semibold' : undefined}>
{theme.package_info.name}
</span>
</div>
<div
className="w-5 h-5 rounded-full"
style={{
backgroundColor: theme.package_info?.dock_icon?.background_color,
}}
></div>
</>
)}
</button>
);
};

View File

@@ -0,0 +1,77 @@
import { RefObject } from 'preact';
import { StateUpdater } from 'preact/hooks';
import { JSXInternal } from 'preact/src/jsx';
export const quickSettingsKeyDownHandler = (
closeQuickSettingsMenu: () => void,
event: JSXInternal.TargetedKeyboardEvent<HTMLDivElement>,
quickSettingsMenuRef: RefObject<HTMLDivElement>,
themesMenuOpen: boolean
) => {
if (quickSettingsMenuRef?.current) {
const items: NodeListOf<HTMLButtonElement> =
quickSettingsMenuRef.current.querySelectorAll(':scope > button');
const currentFocusedIndex = Array.from(items).findIndex(
(btn) => btn === document.activeElement
);
if (!themesMenuOpen) {
switch (event.key) {
case 'Escape':
closeQuickSettingsMenu();
break;
case 'ArrowDown':
if (items[currentFocusedIndex + 1]) {
items[currentFocusedIndex + 1].focus();
} else {
items[0].focus();
}
break;
case 'ArrowUp':
if (items[currentFocusedIndex - 1]) {
items[currentFocusedIndex - 1].focus();
} else {
items[items.length - 1].focus();
}
break;
}
}
}
};
export const themesMenuKeyDownHandler = (
event: React.KeyboardEvent<HTMLDivElement>,
themesMenuRef: RefObject<HTMLDivElement>,
setThemesMenuOpen: StateUpdater<boolean>,
themesButtonRef: RefObject<HTMLButtonElement>
) => {
if (themesMenuRef?.current) {
const themes = themesMenuRef.current.querySelectorAll('button');
const currentFocusedIndex = Array.from(themes).findIndex(
(themeBtn) => themeBtn === document.activeElement
);
switch (event.key) {
case 'Escape':
case 'ArrowLeft':
event.stopPropagation();
setThemesMenuOpen(false);
themesButtonRef.current?.focus();
break;
case 'ArrowDown':
if (themes[currentFocusedIndex + 1]) {
themes[currentFocusedIndex + 1].focus();
} else {
themes[0].focus();
}
break;
case 'ArrowUp':
if (themes[currentFocusedIndex - 1]) {
themes[currentFocusedIndex - 1].focus();
} else {
themes[themes.length - 1].focus();
}
break;
}
}
};

View File

@@ -10,7 +10,8 @@ import '@reach/checkbox/styles.css';
export type SwitchProps = HTMLProps<HTMLInputElement> & {
checked?: boolean;
onChange: (checked: boolean) => void;
// Optional in case it is wrapped in a button (e.g. a menu item)
onChange?: (checked: boolean) => void;
className?: string;
children?: ComponentChildren;
role?: string;
@@ -32,7 +33,7 @@ export const Switch: FunctionalComponent<SwitchProps> = (
checked={checked}
onChange={(event) => {
setChecked(event.target.checked);
props.onChange(event.target.checked);
props.onChange?.(event.target.checked);
}}
className={`sn-switch ${checked ? 'bg-info' : 'bg-neutral'}`}
>

View File

@@ -0,0 +1,136 @@
import { confirmDialog } from '@/services/alertService';
import { STRING_DELETE_TAG } from '@/strings';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { SNTag, TagMutator } from '@standardnotes/snjs';
import { runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useCallback } from 'preact/hooks';
import { TagsListItem } from './TagsListItem';
import { toDirective } from './utils';
type Props = {
application: WebApplication;
appState: AppState;
};
const tagsWithOptionalTemplate = (
template: SNTag | undefined,
tags: SNTag[]
): SNTag[] => {
if (!template) {
return tags;
}
return [template, ...tags];
};
export const TagsList: FunctionComponent<Props> = observer(
({ application, appState }) => {
const templateTag = appState.templateTag;
const tags = appState.tags.tags;
const allTags = tagsWithOptionalTemplate(templateTag, tags);
const selectTag = useCallback(
(tag: SNTag) => {
appState.setSelectedTag(tag);
},
[appState]
);
const saveTag = useCallback(
async (tag: SNTag, newTitle: string) => {
const templateTag = appState.templateTag;
const hasEmptyTitle = newTitle.length === 0;
const hasNotChangedTitle = newTitle === tag.title;
const isTemplateChange = templateTag && tag.uuid === templateTag.uuid;
const hasDuplicatedTitle = !!application.findTagByTitle(newTitle);
runInAction(() => {
appState.templateTag = undefined;
appState.editingTag = undefined;
});
if (hasEmptyTitle || hasNotChangedTitle) {
if (isTemplateChange) {
appState.undoCreateNewTag();
}
return;
}
if (hasDuplicatedTitle) {
if (isTemplateChange) {
appState.undoCreateNewTag();
}
application.alertService?.alert(
'A tag with this name already exists.'
);
return;
}
if (isTemplateChange) {
const insertedTag = await application.insertItem(templateTag);
const changedTag = await application.changeItem<TagMutator>(
insertedTag.uuid,
(m) => {
m.title = newTitle;
}
);
selectTag(changedTag as SNTag);
await application.saveItem(insertedTag.uuid);
} else {
await application.changeAndSaveItem<TagMutator>(
tag.uuid,
(mutator) => {
mutator.title = newTitle;
}
);
}
},
[appState, application, selectTag]
);
const removeTag = useCallback(
async (tag: SNTag) => {
if (
await confirmDialog({
text: STRING_DELETE_TAG,
confirmButtonStyle: 'danger',
})
) {
appState.removeTag(tag);
}
},
[appState]
);
return (
<>
{allTags.length === 0 ? (
<div className="no-tags-placeholder">
No tags. Create one using the add button above.
</div>
) : (
<>
{allTags.map((tag) => {
return (
<TagsListItem
key={tag.uuid}
tag={tag}
selectTag={selectTag}
saveTag={saveTag}
removeTag={removeTag}
appState={appState}
/>
);
})}
</>
)}
</>
);
}
);
export const TagsListDirective = toDirective<Props>(TagsList);

View File

@@ -0,0 +1,134 @@
import { SNTag } from '@standardnotes/snjs';
import { runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
import { FunctionComponent, JSX } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
type Props = {
tag: SNTag;
selectTag: (tag: SNTag) => void;
removeTag: (tag: SNTag) => void;
saveTag: (tag: SNTag, newTitle: string) => void;
appState: TagsListState;
};
export type TagsListState = {
readonly selectedTag: SNTag | undefined;
editingTag: SNTag | undefined;
};
export const TagsListItem: FunctionComponent<Props> = observer(
({ tag, selectTag, saveTag, removeTag, appState }) => {
const [title, setTitle] = useState(tag.title || '');
const inputRef = useRef<HTMLInputElement>(null);
const isSelected = appState.selectedTag === tag;
const isEditing = appState.editingTag === tag;
const noteCounts = tag.noteCount;
useEffect(() => {
setTitle(tag.title || '');
}, [setTitle, tag]);
const selectCurrentTag = useCallback(() => {
if (isEditing || isSelected) {
return;
}
selectTag(tag);
}, [isSelected, isEditing, selectTag, tag]);
const onBlur = useCallback(() => {
saveTag(tag, title);
}, [tag, saveTag, title]);
const onInput = useCallback(
(e: JSX.TargetedEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value;
setTitle(value);
},
[setTitle]
);
const onKeyUp = useCallback(
(e: KeyboardEvent) => {
if (e.code === 'Enter') {
inputRef.current?.blur();
e.preventDefault();
}
},
[inputRef]
);
useEffect(() => {
if (isEditing) {
inputRef.current?.focus();
}
}, [inputRef, isEditing]);
const onClickRename = useCallback(() => {
runInAction(() => {
appState.editingTag = tag;
});
}, [appState, tag]);
const onClickSave = useCallback(() => {
inputRef.current?.blur();
}, [inputRef]);
const onClickDelete = useCallback(() => {
removeTag(tag);
}, [removeTag, tag]);
return (
<div
className={`tag ${isSelected ? 'selected' : ''}`}
onClick={selectCurrentTag}
>
{!tag.errorDecrypting ? (
<div className="tag-info">
<div className="tag-icon">#</div>
<input
className={`title ${isEditing ? 'editing' : ''}`}
id={`react-tag-${tag.uuid}`}
onBlur={onBlur}
onInput={onInput}
value={title}
onKeyUp={onKeyUp}
spellCheck={false}
ref={inputRef}
/>
<div className="count">{noteCounts}</div>
</div>
) : null}
{tag.conflictOf && (
<div className="danger small-text font-bold">
Conflicted Copy {tag.conflictOf}
</div>
)}
{tag.errorDecrypting && !tag.waitingForKey && (
<div className="danger small-text font-bold">Missing Keys</div>
)}
{tag.errorDecrypting && tag.waitingForKey && (
<div className="info small-text font-bold">Waiting For Keys</div>
)}
{isSelected && (
<div className="menu">
{!isEditing && (
<a className="item" onClick={onClickRename}>
Rename
</a>
)}
{isEditing && (
<a className="item" onClick={onClickSave}>
Save
</a>
)}
<a className="item" onClick={onClickDelete}>
Delete
</a>
</div>
)}
</div>
);
}
);

View File

@@ -5,7 +5,7 @@ import { useEffect } from 'react';
/**
* @returns a callback that will close a dropdown if none of its children has
* focus. Must be set as the onBlur callback of children that need to be
* focus. Use the returned function as the onBlur callback of children that need to be
* monitored.
*/
export function useCloseOnBlur(
@@ -36,13 +36,14 @@ export function useCloseOnClickOutside(
container: { current: HTMLDivElement },
setOpen: (open: boolean) => void
): void {
const closeOnClickOutside = useCallback((event: { target: EventTarget | null }) => {
if (
!container.current?.contains(event.target as Node)
) {
setOpen(false);
}
}, [container, setOpen]);
const closeOnClickOutside = useCallback(
(event: { target: EventTarget | null }) => {
if (!container.current?.contains(event.target as Node)) {
setOpen(false);
}
},
[container, setOpen]
);
useEffect(() => {
document.addEventListener('click', closeOnClickOutside);

View File

@@ -35,8 +35,8 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
$onInit() {
super.$onInit();
const editors = this.application
.componentManager!.componentsForArea(ComponentArea.Editor)
const editors = this.application.componentManager
.componentsForArea(ComponentArea.Editor)
.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});

View File

@@ -0,0 +1,28 @@
const pathsToModuleNameMapper = require('ts-jest/utils').pathsToModuleNameMapper;
const tsConfig = require('./tsconfig.json');
const pathsFromTsconfig = tsConfig.compilerOptions.paths;
module.exports = {
clearMocks: true,
moduleNameMapper: {
...pathsToModuleNameMapper(pathsFromTsconfig, {
prefix: '<rootDir>',
}),
'^react$': ['preact/compat'],
'^react-dom$': 'preact',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
globals: {
window: {},
__VERSION__: '1.0.0',
__DESKTOP__: false,
__WEB__: true,
self: {}, // fixes error happening on `import { SKAlert } from 'sn-stylekit'`
},
transform: {
'\\.(pug)$': '../../../node_modules/jest-transform-pug',
'^.+\\.(ts|tsx)?$': 'ts-jest',
'\\.svg$': 'svg-jest',
},
};

View File

@@ -96,8 +96,8 @@ export const OfflineSubscription: FunctionalComponent<IProps> = observer(({ appl
)}
</div>
{(isSuccessfullyActivated || isSuccessfullyRemoved) && (
<div className={'mt-3 mb-3 info font-bold'}>
Successfully {isSuccessfullyActivated ? 'Activated' : 'Removed'}!
<div className={'mt-3 mb-3 info'}>
Your offline subscription code has been successfully {isSuccessfullyActivated ? 'activated' : 'removed'}.
</div>
)}
{hasUserPreviouslyStoredCode && (

View File

@@ -15,22 +15,53 @@ const StatusText = observer(({ subscriptionState }: Props) => {
const { userSubscription, userSubscriptionName } = subscriptionState;
const expirationDate = new Date(
convertTimestampToMilliseconds(userSubscription!.endsAt)
).toLocaleString();
);
const expirationDateString = expirationDate.toLocaleString();
const expired = expirationDate.getTime() < new Date().getTime();
const canceled = userSubscription!.cancelled;
return userSubscription!.cancelled ? (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription has been{' '}
<span className="font-bold">
canceled but will remain valid until {expirationDate}
</span>
. You may resubscribe below if you wish.
</Text>
) : (
if (canceled) {
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription has been canceled
{' '}
{expired ? (
<span className="font-bold">
and expired on {expirationDateString}
</span>
) : (
<span className="font-bold">
but will remain valid until {expirationDateString}
</span>
)}
. You may resubscribe below if you wish.
</Text>
);
}
if (expired) {
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription {' '}
<span className="font-bold">
expired on {expirationDateString}
</span>
. You may resubscribe below if you wish.
</Text>
);
}
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
@@ -38,7 +69,7 @@ const StatusText = observer(({ subscriptionState }: Props) => {
{userSubscriptionName}
</span>{' '}
subscription will be{' '}
<span className="font-bold">renewed on {expirationDate}</span>.
<span className="font-bold">renewed on {expirationDateString}</span>.
</Text>
);
});

View File

@@ -5,16 +5,16 @@ import {
ApplicationService,
SNTheme,
removeFromArray,
ApplicationEvent, ContentType
ApplicationEvent,
ContentType,
} from '@standardnotes/snjs';
const CACHED_THEMES_KEY = 'cachedThemes';
export class ThemeManager extends ApplicationService {
private activeThemes: string[] = []
private unregisterDesktop!: () => void
private unregisterStream!: () => void
private activeThemes: string[] = [];
private unregisterDesktop!: () => void;
private unregisterStream!: () => void;
async onAppEvent(event: ApplicationEvent) {
super.onAppEvent(event);
@@ -54,7 +54,8 @@ export class ThemeManager extends ApplicationService {
}
private registerObservers() {
this.unregisterDesktop = this.webApplication.getDesktopService()
this.unregisterDesktop = this.webApplication
.getDesktopService()
.registerUpdateObserver((component) => {
if (component.active && component.isTheme()) {
this.deactivateTheme(component.uuid);
@@ -64,16 +65,21 @@ export class ThemeManager extends ApplicationService {
}
});
this.unregisterStream = this.application.streamItems(ContentType.Theme, (items) => {
const themes = items as SNTheme[];
for (const theme of themes) {
if (theme.active) {
this.activateTheme(theme);
} else {
this.deactivateTheme(theme.uuid);
this.unregisterStream = this.application.streamItems(
ContentType.Theme,
() => {
const themes = this.application.getDisplayableItems(
ContentType.Theme
) as SNTheme[];
for (const theme of themes) {
if (theme.active) {
this.activateTheme(theme);
} else {
this.deactivateTheme(theme.uuid);
}
}
}
});
);
}
private clearAppThemeState() {
@@ -120,14 +126,17 @@ export class ThemeManager extends ApplicationService {
private async cacheThemes() {
const themes = this.application!.getAll(this.activeThemes) as SNTheme[];
const mapped = await Promise.all(themes.map(async (theme) => {
const payload = theme.payloadRepresentation();
const processedPayload = await this.application!.protocolService!.payloadByEncryptingPayload(
payload,
EncryptionIntent.LocalStorageDecrypted
);
return processedPayload;
}));
const mapped = await Promise.all(
themes.map(async (theme) => {
const payload = theme.payloadRepresentation();
const processedPayload =
await this.application!.protocolService!.payloadByEncryptingPayload(
payload,
EncryptionIntent.LocalStorageDecrypted
);
return processedPayload;
})
);
return this.application!.setValue(
CACHED_THEMES_KEY,
mapped,
@@ -145,15 +154,17 @@ export class ThemeManager extends ApplicationService {
}
private async getCachedThemes() {
const cachedThemes = await this.application!.getValue(
const cachedThemes = (await this.application!.getValue(
CACHED_THEMES_KEY,
StorageValueModes.Nonwrapped
) as SNTheme[];
)) as SNTheme[];
if (cachedThemes) {
const themes = [];
for (const cachedTheme of cachedThemes) {
const payload = this.application!.createPayloadFromObject(cachedTheme);
const theme = this.application!.createItemFromPayload(payload) as SNTheme;
const theme = this.application!.createItemFromPayload(
payload
) as SNTheme;
themes.push(theme);
}
return themes;

View File

@@ -8,6 +8,7 @@
"strict": true,
"isolatedModules": false,
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true,
"newLine": "lf",
"declarationDir": "../../../dist/@types",
@@ -15,13 +16,14 @@
"jsx": "react-jsx",
"jsxImportSource": "preact",
"typeRoots": ["./@types"],
"types": ["@types/jest"],
"paths": {
"%/*": ["../templates/*"],
"@/*": ["./*"],
"@Controllers/*": ["./controllers/*"],
"@Views/*": ["./views/*"],
"@Services/*": ["./services/*"],
"@node_modules/*": ["../../../node_modules/*"],
"@node_modules/*": ["../../../node_modules/*"]
}
}
}

View File

@@ -1,30 +1,36 @@
import { isDesktopApplication } from '@/utils';
import pull from 'lodash/pull';
import {
ApplicationEvent,
SNTag,
SNNote,
ContentType,
PayloadSource,
DeinitSource,
PrefKey,
} from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application';
import { Editor } from '@/ui_models/editor';
import { action, makeObservable, observable } from 'mobx';
import { Bridge } from '@/services/bridge';
import { storage, StorageKey } from '@/services/localStorage';
import { WebApplication } from '@/ui_models/application';
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
import { Editor } from '@/ui_models/editor';
import { isDesktopApplication } from '@/utils';
import {
ApplicationEvent,
ContentType,
DeinitSource,
PayloadSource,
PrefKey,
SNNote,
SNTag,
} from '@standardnotes/snjs';
import pull from 'lodash/pull';
import {
action,
computed,
makeObservable,
observable,
runInAction,
} from 'mobx';
import { ActionsMenuState } from './actions_menu_state';
import { NotesState } from './notes_state';
import { NoteTagsState } from './note_tags_state';
import { NoAccountWarningState } from './no_account_warning_state';
import { SyncState } from './sync_state';
import { SearchOptionsState } from './search_options_state';
import { NotesState } from './notes_state';
import { TagsState } from './tags_state';
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
import { PreferencesState } from './preferences_state';
import { PurchaseFlowState } from './purchase_flow_state';
import { QuickSettingsState } from './quick_settings_state';
import { SearchOptionsState } from './search_options_state';
import { SyncState } from './sync_state';
import { TagsState } from './tags_state';
export enum AppStateEvent {
TagChanged,
@@ -34,7 +40,7 @@ export enum AppStateEvent {
BeganBackupDownload,
EndedBackupDownload,
WindowDidFocus,
WindowDidBlur
WindowDidBlur,
}
export type PanelResizedData = {
@@ -62,8 +68,13 @@ export class AppState {
rootScopeCleanup1: any;
rootScopeCleanup2: any;
onVisibilityChange: any;
selectedTag?: SNTag;
showBetaWarning: boolean;
selectedTag: SNTag | undefined;
previouslySelectedTag: SNTag | undefined;
editingTag: SNTag | undefined;
_templateTag: SNTag | undefined;
readonly quickSettingsMenu = new QuickSettingsState();
readonly accountMenu: AccountMenuState;
readonly actionsMenu = new ActionsMenuState();
@@ -133,11 +144,25 @@ export class AppState {
this.showBetaWarning = false;
}
this.selectedTag = undefined;
this.previouslySelectedTag = undefined;
this.editingTag = undefined;
this._templateTag = undefined;
makeObservable(this, {
showBetaWarning: observable,
isSessionsModalVisible: observable,
preferences: observable,
selectedTag: observable,
previouslySelectedTag: observable,
_templateTag: observable,
templateTag: computed,
createNewTag: action,
editingTag: observable,
setSelectedTag: action,
removeTag: action,
enableBetaWarning: action,
disableBetaWarning: action,
openSessionsModal: action,
@@ -236,7 +261,7 @@ export class AppState {
}
streamNotesAndTags() {
this.application!.streamItems(
this.application.streamItems(
[ContentType.Note, ContentType.Tag],
async (items, source) => {
/** Close any editors for deleted/trashed/archived notes */
@@ -269,10 +294,13 @@ export class AppState {
}
if (this.selectedTag) {
const matchingTag = items.find(
(candidate) => candidate.uuid === this.selectedTag!.uuid
(candidate) =>
this.selectedTag && candidate.uuid === this.selectedTag.uuid
);
if (matchingTag) {
this.selectedTag = matchingTag as SNTag;
runInAction(() => {
this.selectedTag = matchingTag as SNTag;
});
}
}
}
@@ -343,17 +371,69 @@ export class AppState {
}
setSelectedTag(tag: SNTag) {
if (tag.conflictOf) {
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
mutator.conflictOf = undefined;
});
}
if (this.selectedTag === tag) {
return;
}
const previousTag = this.selectedTag;
this.previouslySelectedTag = this.selectedTag;
this.selectedTag = tag;
if (this.templateTag?.uuid === tag.uuid) {
return;
}
this.notifyEvent(AppStateEvent.TagChanged, {
tag: tag,
previousTag: previousTag,
previousTag: this.previouslySelectedTag,
});
}
public getSelectedTag() {
return this.selectedTag;
}
public get templateTag(): SNTag | undefined {
return this._templateTag;
}
public set templateTag(tag: SNTag | undefined) {
const previous = this._templateTag;
this._templateTag = tag;
if (tag) {
this.setSelectedTag(tag);
this.editingTag = tag;
} else if (previous) {
this.selectedTag =
previous === this.selectedTag ? undefined : this.selectedTag;
this.editingTag =
previous === this.editingTag ? undefined : this.editingTag;
}
}
public removeTag(tag: SNTag) {
this.application.deleteItem(tag);
this.setSelectedTag(this.tags.smartTags[0]);
}
public async createNewTag() {
const newTag = (await this.application.createTemplateItem(
ContentType.Tag
)) as SNTag;
this.templateTag = newTag;
}
public async undoCreateNewTag() {
const previousTag = this.previouslySelectedTag || this.tags.smartTags[0];
this.setSelectedTag(previousTag);
}
/** Returns the tags that are referncing this note */
public getNoteTags(note: SNNote) {
return this.application.referencingForItem(note).filter((ref) => {
@@ -361,10 +441,6 @@ export class AppState {
}) as SNTag[];
}
public getSelectedTag() {
return this.selectedTag;
}
panelDidResize(name: string, collapsed: boolean) {
const data: PanelResizedData = {
panel: name,

View File

@@ -3,14 +3,17 @@ import { action, makeObservable, observable } from 'mobx';
export class QuickSettingsState {
open = false;
shouldAnimateCloseMenu = false;
focusModeEnabled = false;
constructor() {
makeObservable(this, {
open: observable,
shouldAnimateCloseMenu: observable,
focusModeEnabled: observable,
setOpen: action,
setShouldAnimateCloseMenu: action,
setFocusModeEnabled: action,
toggle: action,
closeQuickSettingsMenu: action,
});
@@ -24,6 +27,10 @@ export class QuickSettingsState {
this.shouldAnimateCloseMenu = shouldAnimate;
};
setFocusModeEnabled = (enabled: boolean): void => {
this.focusModeEnabled = enabled;
};
toggle = (): void => {
if (this.open) {
this.closeQuickSettingsMenu();

View File

@@ -1,30 +1,29 @@
import { ComponentModalScope } from './../directives/views/componentModal';
import { AccountSwitcherScope, PermissionsModalScope } from './../types';
import { ComponentGroup } from './component_group';
import { EditorGroup } from '@/ui_models/editor_group';
import { InputModalScope } from '@/directives/views/inputModal';
import { PasswordWizardType, PasswordWizardScope } from '@/types';
import {
SNApplication,
SNComponent,
PermissionDialog,
DeinitSource,
Platform,
} from '@standardnotes/snjs';
import angular from 'angular';
import { WebDeviceInterface } from '@/web_device_interface';
import { AppState } from '@/ui_models/app_state';
import { Bridge } from '@/services/bridge';
import { WebCrypto } from '@/crypto';
import { InputModalScope } from '@/directives/views/inputModal';
import { AlertService } from '@/services/alertService';
import { AutolockService } from '@/services/autolock_service';
import { ArchiveManager } from '@/services/archiveManager';
import { AutolockService } from '@/services/autolock_service';
import { Bridge } from '@/services/bridge';
import { DesktopManager } from '@/services/desktopManager';
import { IOService } from '@/services/ioService';
import { StatusManager } from '@/services/statusManager';
import { ThemeManager } from '@/services/themeManager';
import { PasswordWizardScope, PasswordWizardType } from '@/types';
import { AppState } from '@/ui_models/app_state';
import { EditorGroup } from '@/ui_models/editor_group';
import { AppVersion } from '@/version';
import { isDev } from '@/utils';
import { WebDeviceInterface } from '@/web_device_interface';
import {
DeinitSource,
PermissionDialog,
Platform,
SNApplication,
SNComponent,
} from '@standardnotes/snjs';
import angular from 'angular';
import { ComponentModalScope } from './../directives/views/componentModal';
import { AccountSwitcherScope, PermissionsModalScope } from './../types';
import { ComponentGroup } from './component_group';
type WebServices = {
appState: AppState;
@@ -34,15 +33,14 @@ type WebServices = {
statusManager: StatusManager;
themeService: ThemeManager;
io: IOService;
}
};
export class WebApplication extends SNApplication {
private scope?: angular.IScope
private webServices!: WebServices
private currentAuthenticationElement?: angular.IRootElementService
public editorGroup: EditorGroup
public componentGroup: ComponentGroup
private scope?: angular.IScope;
private webServices!: WebServices;
private currentAuthenticationElement?: angular.IRootElementService;
public editorGroup: EditorGroup;
public componentGroup: ComponentGroup;
/* @ngInject */
constructor(
@@ -54,7 +52,7 @@ export class WebApplication extends SNApplication {
defaultSyncServerHost: string,
public bridge: Bridge,
enableUnfinishedFeatures: boolean,
webSocketUrl: string,
webSocketUrl: string
) {
super(
bridge.environment,
@@ -67,7 +65,7 @@ export class WebApplication extends SNApplication {
defaultSyncServerHost,
AppVersion,
enableUnfinishedFeatures,
webSocketUrl,
webSocketUrl
);
this.$compile = $compile;
this.scope = scope;
@@ -108,7 +106,8 @@ export class WebApplication extends SNApplication {
onStart(): void {
super.onStart();
this.componentManager!.openModalComponent = this.openModalComponent;
this.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog;
this.componentManager!.presentPermissionsDialog =
this.presentPermissionsDialog;
}
setWebServices(services: WebServices): void {
@@ -176,8 +175,8 @@ export class WebApplication extends SNApplication {
presentPasswordModal(callback: () => void) {
const scope = this.scope!.$new(true) as InputModalScope;
scope.type = "password";
scope.title = "Decryption Assistance";
scope.type = 'password';
scope.title = 'Decryption Assistance';
scope.message = `Unable to decrypt this item with your current keys.
Please enter your account password at the time of this revision.`;
scope.callback = callback;
@@ -205,8 +204,8 @@ export class WebApplication extends SNApplication {
const scope = this.scope!.$new(true) as Partial<AccountSwitcherScope>;
scope.application = this;
const el = this.$compile!(
"<account-switcher application='application' "
+ "class='sk-modal'></account-switcher>"
"<account-switcher application='application' " +
"class='sk-modal'></account-switcher>"
)(scope as any);
this.applicationElement.append(el);
}
@@ -214,7 +213,7 @@ export class WebApplication extends SNApplication {
async openModalComponent(component: SNComponent): Promise<void> {
switch (component.package_info?.identifier) {
case 'org.standardnotes.cloudlink':
if (!await this.authorizeCloudLinkAccess()) {
if (!(await this.authorizeCloudLinkAccess())) {
return;
}
break;
@@ -223,8 +222,8 @@ export class WebApplication extends SNApplication {
scope.componentUuid = component.uuid;
scope.application = this;
const el = this.$compile!(
"<component-modal application='application' component-uuid='componentUuid' "
+ "class='sk-modal'></component-modal>"
"<component-modal application='application' component-uuid='componentUuid' " +
"class='sk-modal'></component-modal>"
)(scope as any);
this.applicationElement.append(el);
}
@@ -235,8 +234,8 @@ export class WebApplication extends SNApplication {
scope.component = dialog.component;
scope.callback = dialog.callback;
const el = this.$compile!(
"<permissions-modal component='component' permissions-string='permissionsString'"
+ " callback='callback' class='sk-modal'></permissions-modal>"
"<permissions-modal component='component' permissions-string='permissionsString'" +
" callback='callback' class='sk-modal'></permissions-modal>"
)(scope as any);
this.applicationElement.append(el);
}

View File

@@ -48,6 +48,11 @@
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
) {{self.state.noteStatus.message}}
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
pin-note-button(
class='mr-3'
app-state='self.appState',
ng-if='self.appState.notes.selectedNotesCount > 0'
)
notes-options-panel(
application='self.application',
app-state='self.appState',

View File

@@ -740,8 +740,8 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
async reloadStackComponents() {
const stackComponents = sortAlphabetically(
this.application
.componentManager!.componentsForArea(ComponentArea.EditorStack)
this.application.componentManager
.componentsForArea(ComponentArea.EditorStack)
.filter((component) => component.active)
);
if (this.note) {

View File

@@ -1,11 +1,11 @@
#tags-column.sn-component.section.tags(aria-label='Tags')
.component-view-container(ng-if='self.component && self.component.active')
.component-view-container(ng-if='self.component')
component-view.component-view(
component-uuid='self.component.uuid',
application='self.application'
app-state='self.appState'
)
#tags-content.content(ng-if='!(self.component && self.component.active)')
#tags-content.content(ng-if='!(self.component)')
.tags-title-section.section-title-bar
.section-title-bar-header
.sk-h3.title
@@ -33,35 +33,10 @@
.section-title-bar-header
.sk-h3.title
span.sk-bold Tags
.tag(
ng-class="{'selected' : self.state.selectedTag == tag}",
ng-click='self.selectTag(tag)',
ng-repeat='tag in self.state.tags track by tag.uuid'
)
.tag-info(ng-if="!tag.errorDecrypting")
.tag-icon #
input.title(
ng-attr-id='tag-{{tag.uuid}}',
ng-blur='self.saveTag($event, tag)'
ng-change='self.onTagTitleChange(tag)',
ng-model='self.titles[tag.uuid]',
ng-class="{'editing' : self.state.editingTag == tag}",
ng-click='self.selectTag(tag)',
ng-keyup='$event.keyCode == 13 && $event.target.blur()',
should-focus='self.state.templateTag || self.state.editingTag == tag',
sn-autofocus='true',
spellcheck='false'
)
.count {{self.state.noteCounts[tag.uuid]}}
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
.menu(ng-show='self.state.selectedTag == tag')
a.item(ng-click='self.selectedRenameTag(tag)' ng-show='!self.state.editingTag') Rename
a.item(ng-click='self.saveTag($event, tag)' ng-show='self.state.editingTag') Save
a.item(ng-click='self.selectedDeleteTag(tag)') Delete
.no-tags-placeholder(ng-show='self.state.tags.length == 0')
| No tags. Create one using the add button above.
tags(
application='self.application',
app-state='self.appState'
)
panel-resizer(
collapsable='true',
control='self.panelPuppet',

View File

@@ -1,62 +1,46 @@
import { PayloadContent } from '@standardnotes/snjs';
import { WebDirective, PanelPuppet } from '@/types';
import { PanelPuppet, WebDirective } from '@/types';
import { WebApplication } from '@/ui_models/application';
import {
SNTag,
ContentType,
ApplicationEvent,
ComponentAction,
SNSmartTag,
ComponentArea,
SNComponent,
PrefKey,
UuidString,
TagMutator
} from '@standardnotes/snjs';
import template from './tags-view.pug';
import { AppStateEvent } from '@/ui_models/app_state';
import { PANEL_NAME_TAGS } from '@/views/constants';
import { STRING_DELETE_TAG } from '@/strings';
import {
ApplicationEvent,
ComponentAction,
ComponentArea,
ContentType,
PrefKey,
SNComponent,
SNSmartTag,
SNTag,
UuidString,
} from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { confirmDialog } from '@/services/alertService';
import template from './tags-view.pug';
type NoteCounts = Partial<Record<string, number>>
type NoteCounts = Partial<Record<string, number>>;
type TagState = {
tags: SNTag[]
smartTags: SNSmartTag[]
noteCounts: NoteCounts
selectedTag?: SNTag
/** If creating a new tag, the previously selected tag will be set here, so that if new
* tag creation is canceled, the previous tag is re-selected */
previousTag?: SNTag
/** If a tag is in edit state, it will be set as the editingTag */
editingTag?: SNTag
/** If a tag is new and not yet saved, it will be set as the template tag */
templateTag?: SNTag
}
smartTags: SNSmartTag[];
noteCounts: NoteCounts;
selectedTag?: SNTag;
};
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
/** Passed through template */
readonly application!: WebApplication
private readonly panelPuppet: PanelPuppet
private unregisterComponent?: any
component?: SNComponent
readonly application!: WebApplication;
private readonly panelPuppet: PanelPuppet;
private unregisterComponent?: any;
component?: SNComponent;
/** The original name of the edtingTag before it began editing */
private editingOriginalName?: string
formData: { tagTitle?: string } = {}
titles: Partial<Record<UuidString, string>> = {}
private removeTagsObserver!: () => void
private removeFoldersObserver!: () => void
formData: { tagTitle?: string } = {};
titles: Partial<Record<UuidString, string>> = {};
private removeTagsObserver!: () => void;
private removeFoldersObserver!: () => void;
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
) {
constructor($timeout: ng.ITimeoutService) {
super($timeout);
this.panelPuppet = {
onReady: () => this.loadPreferences()
onReady: () => this.loadPreferences(),
};
}
@@ -69,16 +53,15 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
super.deinit();
}
getInitialState() {
getInitialState(): TagState {
return {
tags: [],
smartTags: [],
noteCounts: {},
};
}
getState() {
return this.state as TagState;
getState(): TagState {
return this.state;
}
async onAppStart() {
@@ -90,10 +73,9 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
super.onAppLaunch();
this.loadPreferences();
this.beginStreamingItems();
const smartTags = this.application.getSmartTags();
this.setState({
smartTags: smartTags,
});
this.setState({ smartTags });
this.selectTag(smartTags[0]);
}
@@ -114,28 +96,31 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
this.removeTagsObserver = this.application.streamItems(
[ContentType.Tag, ContentType.SmartTag],
async (items) => {
const tags = items as Array<SNTag | SNSmartTag>;
await this.setState({
tags: this.application.getDisplayableItems(ContentType.Tag) as SNTag[],
smartTags: this.application.getSmartTags(),
});
for (const tag of items as Array<SNTag | SNSmartTag>) {
for (const tag of tags) {
this.titles[tag.uuid] = tag.title;
}
this.reloadNoteCounts();
const selectedTag = this.state.selectedTag;
if (selectedTag) {
/** If the selected tag has been deleted, revert to All view. */
const matchingTag = items.find((tag) => {
const matchingTag = tags.find((tag) => {
return tag.uuid === selectedTag.uuid;
}) as SNTag;
});
if (matchingTag) {
if (matchingTag.deleted) {
this.selectTag(this.getState().smartTags[0]);
} else {
this.setState({
selectedTag: matchingTag
selectedTag: matchingTag,
});
}
}
@@ -148,7 +133,7 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
onAppStateEvent(eventName: AppStateEvent) {
if (eventName === AppStateEvent.TagChanged) {
this.setState({
selectedTag: this.application.getAppState().getSelectedTag()
selectedTag: this.application.getAppState().getSelectedTag(),
});
}
}
@@ -167,37 +152,23 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
}
reloadNoteCounts() {
let allTags: Array<SNTag | SNSmartTag> = [];
if (this.getState().tags) {
allTags = allTags.concat(this.getState().tags);
}
if (this.getState().smartTags) {
allTags = allTags.concat(this.getState().smartTags);
}
const smartTags = this.state.smartTags;
const noteCounts: NoteCounts = {};
for (const tag of allTags) {
if (tag === this.state.templateTag) {
continue;
}
if (tag.isSmartTag) {
/** Other smart tags do not contain counts */
if (tag.isAllTag) {
const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag)
.filter((note) => {
return !note.archived && !note.trashed;
});
noteCounts[tag.uuid] = notes.length;
}
} else {
const notes = this.application.referencesForItem(tag, ContentType.Note)
for (const tag of smartTags) {
/** Other smart tags do not contain counts */
if (tag.isAllTag) {
const notes = this.application
.notesMatchingSmartTag(tag as SNSmartTag)
.filter((note) => {
return !note.archived && !note.trashed;
});
noteCounts[tag.uuid] = notes.length;
}
}
this.setState({
noteCounts: noteCounts
noteCounts: noteCounts,
});
}
@@ -205,14 +176,14 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
if (!this.panelPuppet.ready) {
return;
}
const width = this.application.getPreference(PrefKey.TagsPanelWidth);
if (width) {
this.panelPuppet.setWidth!(width);
if (this.panelPuppet.isCollapsed!()) {
this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS,
this.panelPuppet.isCollapsed!()
);
this.application
.getAppState()
.panelDidResize(PANEL_NAME_TAGS, this.panelPuppet.isCollapsed!());
}
}
}
@@ -223,36 +194,39 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
_isAtMaxWidth: boolean,
isCollapsed: boolean
) => {
this.application.setPreference(
PrefKey.TagsPanelWidth,
newWidth
).then(() => this.application.sync());
this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS,
isCollapsed
);
}
this.application
.setPreference(PrefKey.TagsPanelWidth, newWidth)
.then(() => this.application.sync());
this.application.getAppState().panelDidResize(PANEL_NAME_TAGS, isCollapsed);
};
registerComponentHandler() {
this.unregisterComponent = this.application.componentManager!.registerHandler({
identifier: 'tags',
areas: [ComponentArea.TagsList],
actionHandler: (_, action, data) => {
if (action === ComponentAction.SelectItem) {
if (data.item!.content_type === ContentType.Tag) {
const tag = this.application.findItem(data.item!.uuid);
if (tag) {
this.selectTag(tag as SNTag);
this.unregisterComponent =
this.application.componentManager!.registerHandler({
identifier: 'tags',
areas: [ComponentArea.TagsList],
actionHandler: (_, action, data) => {
if (action === ComponentAction.SelectItem) {
const item = data.item;
if (!item) {
return;
}
} else if (data.item!.content_type === ContentType.SmartTag) {
const matchingTag = this.getState().smartTags.find(t => t.uuid === data.item!.uuid);
this.selectTag(matchingTag as SNSmartTag);
if (item.content_type === ContentType.SmartTag) {
const matchingTag = this.getState().smartTags.find(
(t) => t.uuid === item.uuid
);
if (matchingTag) {
this.selectTag(matchingTag);
}
}
} else if (action === ComponentAction.ClearSelection) {
this.selectTag(this.getState().smartTags[0]);
}
} else if (action === ComponentAction.ClearSelection) {
this.selectTag(this.getState().smartTags[0]);
}
}
});
},
});
}
async selectTag(tag: SNTag) {
@@ -265,123 +239,11 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
}
async clickedAddNewTag() {
if (this.getState().editingTag) {
if (this.appState.templateTag) {
return;
}
const newTag = await this.application.createTemplateItem(
ContentType.Tag
) as SNTag;
this.setState({
tags: [newTag].concat(this.getState().tags),
previousTag: this.getState().selectedTag,
selectedTag: newTag,
editingTag: newTag,
templateTag: newTag
});
}
onTagTitleChange(tag: SNTag | SNSmartTag) {
this.setState({
editingTag: tag
});
}
async saveTag($event: Event, tag: SNTag) {
($event.target! as HTMLInputElement).blur();
if (this.getState().templateTag) {
if (!this.titles[tag.uuid]?.length) {
return this.undoCreateTag(tag);
}
return this.saveNewTag();
} else {
return this.saveTagRename(tag);
}
}
private async undoCreateTag(tag: SNTag) {
await this.setState({
templateTag: undefined,
editingTag: undefined,
selectedTag: this.appState.selectedTag,
tags: this.state.tags.filter(existingTag => existingTag !== tag)
});
delete this.titles[tag.uuid];
}
async saveTagRename(tag: SNTag) {
const newTitle = this.titles[tag.uuid] || '';
if (newTitle.length === 0) {
this.titles[tag.uuid] = this.editingOriginalName;
this.editingOriginalName = undefined;
await this.setState({
editingTag: undefined
});
return;
}
const existingTag = this.application.findTagByTitle(newTitle);
if (existingTag && existingTag.uuid !== tag.uuid) {
this.application.alertService!.alert(
"A tag with this name already exists."
);
return;
}
await this.application.changeAndSaveItem<TagMutator>(tag.uuid, (mutator) => {
mutator.title = newTitle;
});
await this.setState({
editingTag: undefined
});
}
async saveNewTag() {
const newTag = this.getState().templateTag!;
const newTitle = this.titles[newTag.uuid] || '';
if (newTitle.length === 0) {
await this.setState({
templateTag: undefined
});
return;
}
const existingTag = this.application.findTagByTitle(newTitle);
if (existingTag) {
this.application.alertService!.alert(
"A tag with this name already exists."
);
this.undoCreateTag(newTag);
return;
}
const insertedTag = await this.application.insertItem(newTag);
const changedTag = await this.application.changeItem<TagMutator>(insertedTag.uuid, (m) => {
m.title = newTitle;
});
await this.setState({
templateTag: undefined,
editingTag: undefined
});
this.selectTag(changedTag as SNTag);
await this.application.saveItem(changedTag!.uuid);
}
async selectedRenameTag(tag: SNTag) {
this.editingOriginalName = tag.title;
await this.setState({
editingTag: tag
});
document.getElementById('tag-' + tag.uuid)!.focus();
}
selectedDeleteTag(tag: SNTag) {
this.removeTag(tag);
}
async removeTag(tag: SNTag) {
if (await confirmDialog({
text: STRING_DELETE_TAG,
confirmButtonStyle: 'danger'
})) {
this.application.deleteItem(tag);
this.selectTag(this.getState().smartTags[0]);
}
this.appState.createNewTag();
}
}
@@ -390,7 +252,7 @@ export class TagsView extends WebDirective {
super();
this.restrict = 'E';
this.scope = {
application: '='
application: '=',
};
this.template = template;
this.replace = true;

View File

@@ -33,7 +33,7 @@ $heading-height: 75px;
padding-top: 14px;
padding-left: 14px;
padding-bottom: 10px;
padding-right: 10px;
padding-right: 14px;
border-bottom: none;
z-index: $z-index-editor-title-bar;

View File

@@ -0,0 +1,85 @@
.focus-mode {
.mac-desktop #editor-column {
// To offset colored circles in Mac
padding-top: 35px;
}
.mac-desktop #editor-column:before {
content: '';
display: block;
position: absolute;
top: 0;
width: 100%;
height: 38px;
-webkit-app-region: drag;
}
#editor-title-bar {
display: none;
}
#editor-menu-bar {
display: none;
}
#editor-pane-component-stack {
display: none;
}
#footer-bar {
opacity: 0.08;
transition: opacity 0.25s;
}
#footer-bar:hover {
opacity: 1;
}
.section.tags,
.section.notes {
will-change: opacity;
animation: fade-out 1.25s forwards;
transition: width 1.25s;
transition-delay: 0s;
width: 0px !important;
flex: none !important;
}
.section.tags:hover {
flex: initial;
width: 0px !important;
}
.section.notes:hover {
flex: initial;
width: 0px !important;
}
}
.disable-focus-mode {
.section.tags,
.section.notes {
transition: width 1.25s;
will-change: opacity;
animation: fade-in 1.25s forwards;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
border: none !important;
}
}

View File

@@ -289,6 +289,10 @@
max-width: 18rem;
}
.max-w-89 {
max-width: 22.25rem;
}
.mb-4 {
margin-bottom: 1rem;
}
@@ -300,10 +304,6 @@
margin-bottom: 2rem;
}
.max-w-89 {
max-width: 22.25rem;
}
.w-26 {
width: 6.5rem;
}
@@ -461,6 +461,11 @@
padding-right: 0;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.sn-component .px-4\.5,
.sn-component .sk-panel .px-4\.5 {
padding-left: 1.375rem;
@@ -743,6 +748,36 @@
}
}
.border-info-contrast {
border-color: var(--sn-stylekit-info-contrast-color);
}
.sn-icon-button {
&:focus {
border-color: transparent;
}
&.toggled {
border-color: transparent;
@extend .bg-info;
@extend .color-info-contrast;
&:focus {
background-color: var(--sn-stylekit-info-color) !important;
border: none;
}
&:hover {
@extend .color-info;
@extend .border-info;
}
&:focus:hover {
background-color: var(--sn-stylekit-contrast-background-color) !important;
}
}
}
@media screen and (max-width: $screen-md-min) {
.sn-component {
.md\:hidden {

View File

@@ -1,6 +1,8 @@
.tags {
width: 180px;
flex-grow: 0;
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
@@ -85,18 +87,25 @@
// Required for Safari to avoid highlighting when dragging panel resizers
// Make sure to undo if it's selected (for editing)
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
pointer-events: none;
&.editing {
-webkit-user-select: text !important;
pointer-events: auto;
user-select: text;
-moz-user-select: text;
-khtml-user-select: text;
-webkit-user-select: text;
}
&:focus {
outline: 0;
box-shadow: 0;
}
pointer-events: none;
}
> .count {

View File

@@ -13,4 +13,5 @@
@import 'reach-sub';
@import 'sessions-modal';
@import 'preferences';
@import 'focused';
@import 'sn';

View File

@@ -0,0 +1,22 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="60" cy="60" r="60" fill="#F4F5F7" />
<g filter="url(#filter0_d_5_1809)">
<rect x="31" y="31" width="58" height="58" rx="4" fill="white" />
<path
d="M49.9997 66H53.9997C53.9997 68.16 56.7397 70 59.9997 70C63.2596 70 65.9996 68.16 65.9996 66C65.9996 63.8 63.9197 63 59.5197 61.94C55.2797 60.88 49.9997 59.56 49.9997 54C49.9997 50.42 52.9397 47.38 56.9997 46.36V42H62.9996V46.36C67.0596 47.38 69.9996 50.42 69.9996 54H65.9996C65.9996 51.84 63.2596 50 59.9997 50C56.7397 50 53.9997 51.84 53.9997 54C53.9997 56.2 56.0796 57 60.4796 58.06C64.7196 59.12 69.9996 60.44 69.9996 66C69.9996 69.58 67.0596 72.62 62.9996 73.64V78H56.9997V73.64C52.9397 72.62 49.9997 69.58 49.9997 66Z"
fill="#BBBEC4" />
</g>
<defs>
<filter id="filter0_d_5_1809" x="19" y="23" width="82" height="82" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="4" />
<feGaussianBlur stdDeviation="6" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5_1809" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5_1809" result="shape" />
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "standard-notes-web",
"version": "3.9.5",
"version": "3.9.6",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
@@ -16,7 +16,9 @@
"bundle:desktop:beta": "webpack --config webpack.prod.js --env platform='desktop' --env public_beta='true'",
"build": "bundle install && yarn install --frozen-lockfile && bundle exec rails assets:precompile && yarn bundle",
"lint": "eslint --fix app/assets/javascripts",
"tsc": "tsc --project app/assets/javascripts/tsconfig.json"
"tsc": "tsc --project app/assets/javascripts/tsconfig.json",
"test": "jest --config app/assets/javascripts/jest.config.js",
"prepare": "husky install"
},
"devDependencies": {
"@babel/core": "^7.15.8",
@@ -27,6 +29,7 @@
"@reach/visually-hidden": "^0.16.0",
"@svgr/webpack": "^5.5.0",
"@types/angular": "^1.8.3",
"@types/jest": "^27.0.3",
"@types/lodash": "^4.14.176",
"@types/pug": "^2.0.5",
"@types/react": "^17.0.31",
@@ -46,15 +49,25 @@
"eslint-plugin-react-hooks": "^4.2.1-beta-149b420f6-20211119",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.4.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.3.1",
"jest-transform-pug": "^0.1.0",
"husky": "^7.0.4",
"lint-staged": ">=10",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.3",
"ng-cache-loader": "0.0.26",
"node-sass": "^6.0.1",
"prettier": "^2.5.0",
"pretty-quick": "^3.1.2",
"pug": "^3.0.2",
"pug-jest": "^1.0.1",
"pug-loader": "^2.4.0",
"sass-loader": "^12.2.0",
"serve-static": "^1.14.1",
"sn-stylekit": "5.2.15",
"sn-stylekit": "5.2.17",
"svg-jest": "^1.0.1",
"ts-jest": "^27.0.7",
"ts-loader": "^9.2.6",
"typescript": "4.4.4",
"typescript-eslint": "0.0.1-alpha.0",
@@ -70,12 +83,16 @@
"@reach/checkbox": "^0.16.0",
"@reach/dialog": "^0.16.2",
"@reach/listbox": "^0.16.2",
"@standardnotes/features": "1.9.0",
"@standardnotes/features": "1.10.1",
"@standardnotes/sncrypto-web": "1.5.3",
"@standardnotes/snjs": "2.18.2",
"@standardnotes/snjs": "2.19.6",
"mobx": "^6.3.5",
"mobx-react-lite": "^3.2.1",
"mobx-react-lite": "^3.2.2",
"preact": "^10.5.15",
"qrcode.react": "^1.0.1"
},
"lint-staged": {
"*.{js,ts,jsx,tsx}": "eslint --cache --fix",
"*.{js,ts,jsx,tsx,css,md}": "prettier --write"
}
}

2551
yarn.lock

File diff suppressed because it is too large Load Diff