feat: component viewer (#781)

* wip: component viewer

* feat: get component status from component viewer

* fix: remove unused property

* chore(deps): snjs 2.29.0

* fix: import location
This commit is contained in:
Mo
2021-12-24 10:41:02 -06:00
committed by GitHub
parent 237cd91acd
commit ebdae31965
39 changed files with 874 additions and 1012 deletions

View File

@@ -51,7 +51,6 @@ import {
import { import {
ActionsMenu, ActionsMenu,
ComponentModal,
EditorMenu, EditorMenu,
InputModal, InputModal,
MenuRow, MenuRow,
@@ -166,7 +165,6 @@ const startApplication: StartApplication = async function startApplication(
.directive('accountSwitcher', () => new AccountSwitcher()) .directive('accountSwitcher', () => new AccountSwitcher())
.directive('actionsMenu', () => new ActionsMenu()) .directive('actionsMenu', () => new ActionsMenu())
.directive('challengeModal', () => new ChallengeModal()) .directive('challengeModal', () => new ChallengeModal())
.directive('componentModal', () => new ComponentModal())
.directive('componentView', ComponentViewDirective) .directive('componentView', ComponentViewDirective)
.directive('editorMenu', () => new EditorMenu()) .directive('editorMenu', () => new EditorMenu())
.directive('inputModal', () => new InputModal()) .directive('inputModal', () => new InputModal())

View File

@@ -5,11 +5,14 @@ interface IProps {
expiredDate: string; expiredDate: string;
componentName: string; componentName: string;
featureStatus: FeatureStatus; featureStatus: FeatureStatus;
reloadStatus: () => void;
manageSubscription: () => void; manageSubscription: () => void;
} }
const statusString = (featureStatus: FeatureStatus, expiredDate: string, componentName: string) => { const statusString = (
featureStatus: FeatureStatus,
expiredDate: string,
componentName: string
) => {
switch (featureStatus) { switch (featureStatus) {
case FeatureStatus.InCurrentPlanButExpired: case FeatureStatus.InCurrentPlanButExpired:
return `Your subscription expired on ${expiredDate}`; return `Your subscription expired on ${expiredDate}`;
@@ -25,9 +28,8 @@ const statusString = (featureStatus: FeatureStatus, expiredDate: string, compone
export const IsExpired: FunctionalComponent<IProps> = ({ export const IsExpired: FunctionalComponent<IProps> = ({
expiredDate, expiredDate,
featureStatus, featureStatus,
reloadStatus,
componentName, componentName,
manageSubscription manageSubscription,
}) => { }) => {
return ( return (
<div className={'sn-component'}> <div className={'sn-component'}>
@@ -50,11 +52,13 @@ export const IsExpired: FunctionalComponent<IProps> = ({
</div> </div>
</div> </div>
<div className={'right'}> <div className={'right'}>
<div className={'sk-app-bar-item'} onClick={() => manageSubscription()}> <div
<button className={'sn-button small success'}>Manage Subscription</button> className={'sk-app-bar-item'}
</div> onClick={() => manageSubscription()}
<div className={'sk-app-bar-item'} onClick={() => reloadStatus()}> >
<button className={'sn-button small info'}>Reload</button> <button className={'sn-button small success'}>
Manage Subscription
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,14 +1,6 @@
import { FunctionalComponent } from 'preact'; import { FunctionalComponent } from 'preact';
interface IProps { export const OfflineRestricted: FunctionalComponent = () => {
isReloading: boolean;
reloadStatus: () => void;
}
export const OfflineRestricted: FunctionalComponent<IProps> = ({
isReloading,
reloadStatus,
}) => {
return ( return (
<div className={'sn-component'}> <div className={'sn-component'}>
<div className={'sk-panel static'}> <div className={'sk-panel static'}>
@@ -16,38 +8,29 @@ export const OfflineRestricted: FunctionalComponent<IProps> = ({
<div className={'sk-panel-section stretch'}> <div className={'sk-panel-section stretch'}>
<div className={'sk-panel-column'} /> <div className={'sk-panel-column'} />
<div className={'sk-h1 sk-bold'}> <div className={'sk-h1 sk-bold'}>
You have restricted this component to be used offline only. You have restricted this component to not use a hosted version.
</div> </div>
<div className={'sk-subtitle'}> <div className={'sk-subtitle'}>
Offline components are not available in the web application. Locally-installed components are not available in the web
application.
</div> </div>
<div className={'sk-panel-row'} /> <div className={'sk-panel-row'} />
<div className={'sk-panel-row'}> <div className={'sk-panel-row'}>
<div className={'sk-panel-column'}> <div className={'sk-panel-column'}>
<div className={'sk-p'}>You can either:</div> <div className={'sk-p'}>
To continue, choose from the following options:
</div>
<ul> <ul>
<li className={'sk-p'}> <li className={'sk-p'}>
Enable the Hosted option for this component by opening Enable the Hosted option for this component by opening the
Preferences {'>'} General {'>'} Advanced Settings menu and{' '} Preferences {'>'} General {'>'} Advanced Settings menu and{' '}
toggling 'Use hosted when local is unavailable' under this toggling 'Use hosted when local is unavailable' under this
components's options. Then press Reload below. component's options. Then press Reload.
</li> </li>
<li className={'sk-p'}>Use the desktop application.</li> <li className={'sk-p'}>Use the desktop application.</li>
</ul> </ul>
</div> </div>
</div> </div>
<div className={'sk-panel-row'}>
{isReloading ? (
<div className={'sk-spinner info small'} />
) : (
<button
className={'sn-button small info'}
onClick={() => reloadStatus()}
>
Reload
</button>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,7 +3,9 @@ import {
FeatureStatus, FeatureStatus,
SNComponent, SNComponent,
dateToLocalizedString, dateToLocalizedString,
ApplicationEvent, ComponentViewer,
ComponentViewerEvent,
ComponentViewerError,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { FunctionalComponent } from 'preact'; import { FunctionalComponent } from 'preact';
@@ -22,9 +24,9 @@ import { openSubscriptionDashboard } from '@/hooks/manageSubscription';
interface IProps { interface IProps {
application: WebApplication; application: WebApplication;
appState: AppState; appState: AppState;
componentUuid: string; componentViewer: ComponentViewer;
requestReload?: (viewer: ComponentViewer) => void;
onLoad?: (component: SNComponent) => void; onLoad?: (component: SNComponent) => void;
templateComponent?: SNComponent;
manualDealloc?: boolean; manualDealloc?: boolean;
} }
@@ -34,10 +36,10 @@ interface IProps {
*/ */
const MaxLoadThreshold = 4000; const MaxLoadThreshold = 4000;
const VisibilityChangeKey = 'visibilitychange'; const VisibilityChangeKey = 'visibilitychange';
const avoidFlickerTimeout = 7; const MSToWaitAfterIframeLoadToAvoidFlicker = 35;
export const ComponentView: FunctionalComponent<IProps> = observer( export const ComponentView: FunctionalComponent<IProps> = observer(
({ application, onLoad, componentUuid, templateComponent }) => { ({ application, onLoad, componentViewer, requestReload }) => {
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const excessiveLoadingTimeout = useRef< const excessiveLoadingTimeout = useRef<
ReturnType<typeof setTimeout> | undefined ReturnType<typeof setTimeout> | undefined
@@ -45,44 +47,33 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
const [hasIssueLoading, setHasIssueLoading] = useState(false); const [hasIssueLoading, setHasIssueLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isReloading, setIsReloading] = useState(false);
const [component] = useState<SNComponent>(
application.findItem(componentUuid) as SNComponent
);
const [featureStatus, setFeatureStatus] = useState<FeatureStatus>( const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(
application.getFeatureStatus(component.identifier) componentViewer.getFeatureStatus()
); );
const [isComponentValid, setIsComponentValid] = useState(true); const [isComponentValid, setIsComponentValid] = useState(true);
const [error, setError] = useState< const [error, setError] = useState<ComponentViewerError | undefined>(
'offline-restricted' | 'url-missing' | undefined undefined
>(undefined); );
const [isDeprecated, setIsDeprecated] = useState(false);
const [deprecationMessage, setDeprecationMessage] = useState< const [deprecationMessage, setDeprecationMessage] = useState<
string | undefined string | undefined
>(undefined); >(undefined);
const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] =
useState(false); useState(false);
const [didAttemptReload, setDidAttemptReload] = useState(false); const [didAttemptReload, setDidAttemptReload] = useState(false);
const [contentWindow, setContentWindow] = useState<Window | null>(null);
const component = componentViewer.component;
const manageSubscription = useCallback(() => { const manageSubscription = useCallback(() => {
openSubscriptionDashboard(application); openSubscriptionDashboard(application);
}, [application]); }, [application]);
const reloadIframe = () => {
setTimeout(() => {
setIsReloading(true);
setTimeout(() => {
setIsReloading(false);
});
});
};
useEffect(() => { useEffect(() => {
const loadTimeout = setTimeout(() => { const loadTimeout = setTimeout(() => {
handleIframeTakingTooLongToLoad(); handleIframeTakingTooLongToLoad();
}, MaxLoadThreshold); }, MaxLoadThreshold);
excessiveLoadingTimeout.current = loadTimeout; excessiveLoadingTimeout.current = loadTimeout;
return () => { return () => {
excessiveLoadingTimeout.current && excessiveLoadingTimeout.current &&
clearTimeout(excessiveLoadingTimeout.current); clearTimeout(excessiveLoadingTimeout.current);
@@ -91,42 +82,25 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
}, []); }, []);
const reloadValidityStatus = useCallback(() => { const reloadValidityStatus = useCallback(() => {
const offlineRestricted = setFeatureStatus(componentViewer.getFeatureStatus());
component.offlineOnly && !isDesktopApplication(); if (!componentViewer.lockReadonly) {
const hasUrlError = (function () { componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled);
if (isDesktopApplication()) {
return !component.local_url && !component.hasValidHostedUrl();
} else {
return !component.hasValidHostedUrl();
}
})();
const readonlyState =
application.componentManager.getReadonlyStateForComponent(component);
if (!readonlyState.lockReadonly) {
application.componentManager.setReadonlyStateForComponent(
component,
featureStatus !== FeatureStatus.Entitled
);
} }
setIsComponentValid(!offlineRestricted && !hasUrlError); setIsComponentValid(componentViewer.shouldRender());
if (!isComponentValid) { if (isLoading && !isComponentValid) {
setIsLoading(false); setIsLoading(false);
} }
if (offlineRestricted) { setError(componentViewer.getError());
setError('offline-restricted'); setDeprecationMessage(component.deprecationMessage);
} else if (hasUrlError) { }, [
setError('url-missing'); componentViewer,
} else { component.deprecationMessage,
setError(undefined); featureStatus,
} isComponentValid,
setIsDeprecated(component.isDeprecated); isLoading,
setDeprecationMessage(component.package_info.deprecation_message); ]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
reloadValidityStatus(); reloadValidityStatus();
@@ -141,9 +115,9 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
return; return;
} }
if (hasIssueLoading) { if (hasIssueLoading) {
reloadIframe(); requestReload?.(componentViewer);
} }
}, [hasIssueLoading]); }, [hasIssueLoading, componentViewer, requestReload]);
const handleIframeTakingTooLongToLoad = useCallback(async () => { const handleIframeTakingTooLongToLoad = useCallback(async () => {
setIsLoading(false); setIsLoading(false);
@@ -151,188 +125,133 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
if (!didAttemptReload) { if (!didAttemptReload) {
setDidAttemptReload(true); setDidAttemptReload(true);
reloadIframe(); requestReload?.(componentViewer);
} else { } else {
document.addEventListener(VisibilityChangeKey, onVisibilityChange); document.addEventListener(VisibilityChangeKey, onVisibilityChange);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [componentViewer, didAttemptReload, onVisibilityChange, requestReload]);
}, []);
const handleIframeLoad = useCallback(async (iframe: HTMLIFrameElement) => {
let hasDesktopError = false;
const canAccessWindowOrigin = isDesktopApplication();
if (canAccessWindowOrigin) {
try {
const contentWindow = iframe.contentWindow as Window;
if (!contentWindow.origin || contentWindow.origin === 'null') {
hasDesktopError = true;
}
// eslint-disable-next-line no-empty
} catch (e) {}
}
excessiveLoadingTimeout.current &&
clearTimeout(excessiveLoadingTimeout.current);
setContentWindow(iframe.contentWindow);
setTimeout(() => {
setIsLoading(false);
setHasIssueLoading(hasDesktopError);
onLoad?.(component);
}, avoidFlickerTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (contentWindow) {
application.componentManager.registerComponentWindow(
component,
contentWindow
);
}
return () => {
application.componentManager.onComponentIframeDestroyed(component.uuid);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contentWindow]);
useEffect(() => { useEffect(() => {
if (!iframeRef.current) { if (!iframeRef.current) {
setContentWindow(null);
return; return;
} }
iframeRef.current.onload = () => { const iframe = iframeRef.current as HTMLIFrameElement;
const iframe = application.componentManager.iframeForComponent( iframe.onload = () => {
component.uuid const contentWindow = iframe.contentWindow as Window;
);
if (iframe) { let hasDesktopError = false;
setTimeout(() => { const canAccessWindowOrigin = isDesktopApplication();
handleIframeLoad(iframe); if (canAccessWindowOrigin) {
}); try {
if (!contentWindow.origin || contentWindow.origin === 'null') {
hasDesktopError = true;
}
} catch (e) {
console.error(e);
}
} }
excessiveLoadingTimeout.current &&
clearTimeout(excessiveLoadingTimeout.current);
componentViewer.setWindow(contentWindow);
setTimeout(() => {
setIsLoading(false);
setHasIssueLoading(hasDesktopError);
onLoad?.(component);
}, MSToWaitAfterIframeLoadToAvoidFlicker);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps }, [onLoad, component, componentViewer]);
}, [iframeRef.current]);
useEffect(() => { useEffect(() => {
const removeFeaturesChangedObserver = application.addEventObserver( const removeFeaturesChangedObserver = componentViewer.addEventObserver(
async () => { (event) => {
setFeatureStatus(application.getFeatureStatus(component.identifier)); if (event === ComponentViewerEvent.FeatureStatusUpdated) {
}, setFeatureStatus(componentViewer.getFeatureStatus());
ApplicationEvent.FeaturesUpdated }
}
); );
return () => { return () => {
removeFeaturesChangedObserver(); removeFeaturesChangedObserver();
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps }, [componentViewer]);
}, []);
useEffect(() => { useEffect(() => {
if (!componentUuid) { const removeActionObserver = componentViewer.addActionObserver(
application.componentManager.addTemporaryTemplateComponent( (action, data) => {
templateComponent as SNComponent switch (action) {
); case ComponentAction.KeyDown:
} application.io.handleComponentKeyDown(data.keyboardModifier);
break;
return () => { case ComponentAction.KeyUp:
if (templateComponent) { application.io.handleComponentKeyUp(data.keyboardModifier);
/** componentManager can be destroyed already via locking */ break;
application.componentManager?.removeTemporaryTemplateComponent( case ComponentAction.Click:
templateComponent application.getAppState().notes.setContextMenuOpen(false);
); break;
default:
return;
}
} }
);
document.removeEventListener(VisibilityChangeKey, onVisibilityChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const unregisterComponentHandler =
application.componentManager.registerHandler({
identifier: 'component-view-' + Math.random(),
areas: [component.area],
actionHandler: (component, action, data) => {
switch (action) {
case ComponentAction.SetSize:
application.componentManager.handleSetSizeEvent(
component,
data
);
break;
case ComponentAction.KeyDown:
application.io.handleComponentKeyDown(data.keyboardModifier);
break;
case ComponentAction.KeyUp:
application.io.handleComponentKeyUp(data.keyboardModifier);
break;
case ComponentAction.Click:
application.getAppState().notes.setContextMenuOpen(false);
break;
default:
return;
}
},
});
return () => { return () => {
unregisterComponentHandler(); removeActionObserver();
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps }, [componentViewer, application]);
}, [component]);
useEffect(() => { useEffect(() => {
const unregisterDesktopObserver = application const unregisterDesktopObserver = application
.getDesktopService() .getDesktopService()
.registerUpdateObserver((component: SNComponent) => { .registerUpdateObserver((component: SNComponent) => {
if (component.uuid === component.uuid && component.active) { if (component.uuid === component.uuid && component.active) {
reloadIframe(); requestReload?.(componentViewer);
} }
}); });
return () => { return () => {
unregisterDesktopObserver(); unregisterDesktopObserver();
}; };
}, [application]); }, [application, requestReload, componentViewer]);
return ( return (
<> <>
{hasIssueLoading && ( {hasIssueLoading && (
<IssueOnLoading <IssueOnLoading
componentName={component.name} componentName={component.name}
reloadIframe={reloadIframe} reloadIframe={() => {
reloadValidityStatus(), requestReload?.(componentViewer);
}}
/> />
)} )}
{featureStatus !== FeatureStatus.Entitled && ( {featureStatus !== FeatureStatus.Entitled && (
<IsExpired <IsExpired
expiredDate={dateToLocalizedString(component.valid_until)} expiredDate={dateToLocalizedString(component.valid_until)}
reloadStatus={reloadValidityStatus}
featureStatus={featureStatus} featureStatus={featureStatus}
componentName={component.name} componentName={component.name}
manageSubscription={manageSubscription} manageSubscription={manageSubscription}
/> />
)} )}
{isDeprecated && !isDeprecationMessageDismissed && ( {deprecationMessage && !isDeprecationMessageDismissed && (
<IsDeprecated <IsDeprecated
deprecationMessage={deprecationMessage} deprecationMessage={deprecationMessage}
dismissDeprecationMessage={dismissDeprecationMessage} dismissDeprecationMessage={dismissDeprecationMessage}
/> />
)} )}
{error == 'offline-restricted' && ( {error === ComponentViewerError.OfflineRestricted && (
<OfflineRestricted <OfflineRestricted />
isReloading={isReloading}
reloadStatus={reloadValidityStatus}
/>
)} )}
{error == 'url-missing' && ( {error === ComponentViewerError.MissingUrl && (
<UrlMissing componentName={component.name} /> <UrlMissing componentName={component.name} />
)} )}
{component.uuid && !isReloading && isComponentValid && ( {component.uuid && isComponentValid && (
<iframe <iframe
ref={iframeRef} ref={iframeRef}
data-component-id={component.uuid} data-component-viewer-id={componentViewer.identifier}
frameBorder={0} frameBorder={0}
data-attr-id={`component-iframe-${component.uuid}`}
src={application.componentManager.urlForComponent(component) || ''} src={application.componentManager.urlForComponent(component) || ''}
sandbox="allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms allow-downloads" sandbox="allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms allow-downloads"
> >
@@ -347,7 +266,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
export const ComponentViewDirective = toDirective<IProps>(ComponentView, { export const ComponentViewDirective = toDirective<IProps>(ComponentView, {
onLoad: '=', onLoad: '=',
componentUuid: '=', componentViewer: '=',
templateComponent: '=', requestReload: '=',
manualDealloc: '=', manualDealloc: '=',
}); });

View File

@@ -1,6 +1,5 @@
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { FeatureIdentifier } from '@standardnotes/features'; import { FeatureStatus, FeatureIdentifier } from '@standardnotes/snjs';
import { FeatureStatus } from '@standardnotes/snjs';
import { FunctionComponent } from 'preact'; import { FunctionComponent } from 'preact';
import { useCallback, useState } from 'preact/hooks'; import { useCallback, useState } from 'preact/hooks';
import { JSXInternal } from 'preact/src/jsx'; import { JSXInternal } from 'preact/src/jsx';

View File

@@ -174,7 +174,11 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
}; };
const toggleComponent = (component: SNComponent) => { const toggleComponent = (component: SNComponent) => {
application.toggleComponent(component); if (component.isTheme()) {
application.toggleTheme(component);
} else {
application.toggleComponent(component);
}
}; };
const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = ( const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (
@@ -218,7 +222,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
const activeTheme = themes.find( const activeTheme = themes.find(
(theme) => theme.active && !theme.isLayerable() (theme) => theme.active && !theme.isLayerable()
); );
if (activeTheme) application.toggleComponent(activeTheme); if (activeTheme) application.toggleTheme(activeTheme);
}; };
return ( return (

View File

@@ -18,7 +18,7 @@ export const ThemesMenuButton: FunctionComponent<Props> = ({
const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = (e) => { const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault(); e.preventDefault();
if (theme.isLayerable() || !theme.active) { if (theme.isLayerable() || !theme.active) {
application.toggleComponent(theme); application.toggleTheme(theme);
} }
}; };

View File

@@ -20,11 +20,8 @@ type Props = {
export const SearchOptions = observer(({ appState }: Props) => { export const SearchOptions = observer(({ appState }: Props) => {
const { searchOptions } = appState; const { searchOptions } = appState;
const { const { includeProtectedContents, includeArchived, includeTrashed } =
includeProtectedContents, searchOptions;
includeArchived,
includeTrashed,
} = searchOptions;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [position, setPosition] = useState({ const [position, setPosition] = useState({
@@ -34,7 +31,10 @@ export const SearchOptions = observer(({ appState }: Props) => {
const [maxWidth, setMaxWidth] = useState<number | 'auto'>('auto'); const [maxWidth, setMaxWidth] = useState<number | 'auto'>('auto');
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null); const panelRef = useRef<HTMLDivElement>(null);
const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(panelRef as any, setOpen); const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(
panelRef as any,
setOpen
);
async function toggleIncludeProtectedContents() { async function toggleIncludeProtectedContents() {
setLockCloseOnBlur(true); setLockCloseOnBlur(true);

View File

@@ -1,64 +0,0 @@
import { WebApplication } from '@/ui_models/application';
import { SNComponent, LiveItem } from '@standardnotes/snjs';
import { WebDirective } from './../../types';
import template from '%/directives/component-modal.pug';
export type ComponentModalScope = {
componentUuid: string
onDismiss: () => void
application: WebApplication
}
export class ComponentModalCtrl implements ComponentModalScope {
$element: JQLite
componentUuid!: string
onDismiss!: () => void
application!: WebApplication
liveComponent!: LiveItem<SNComponent>
component!: SNComponent
/* @ngInject */
constructor($element: JQLite) {
this.$element = $element;
}
$onInit() {
this.liveComponent = new LiveItem(
this.componentUuid,
this.application,
(component) => {
this.component = component;
}
);
this.application.componentGroup.activateComponent(this.component);
}
$onDestroy() {
this.application.componentGroup.deactivateComponent(this.component);
this.liveComponent.deinit();
}
dismiss() {
this.onDismiss && this.onDismiss();
const elem = this.$element;
const scope = elem.scope();
scope.$destroy();
elem.remove();
}
}
export class ComponentModal extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = ComponentModalCtrl;
this.controllerAs = 'ctrl';
this.bindToController = true;
this.scope = {
componentUuid: '=',
onDismiss: '&',
application: '='
};
}
}

View File

@@ -1,5 +1,4 @@
export { ActionsMenu } from './actionsMenu'; export { ActionsMenu } from './actionsMenu';
export { ComponentModal } from './componentModal';
export { EditorMenu } from './editorMenu'; export { EditorMenu } from './editorMenu';
export { InputModal } from './inputModal'; export { InputModal } from './inputModal';
export { MenuRow } from './menuRow'; export { MenuRow } from './menuRow';

View File

@@ -2,9 +2,8 @@ import { WebDirective } from './../../types';
import template from '%/directives/permissions-modal.pug'; import template from '%/directives/permissions-modal.pug';
class PermissionsModalCtrl { class PermissionsModalCtrl {
$element: JQLite;
$element: JQLite callback!: (success: boolean) => void;
callback!: (success: boolean) => void
/* @ngInject */ /* @ngInject */
constructor($element: JQLite) { constructor($element: JQLite) {
@@ -41,7 +40,7 @@ export class PermissionsModal extends WebDirective {
show: '=', show: '=',
component: '=', component: '=',
permissionsString: '=', permissionsString: '=',
callback: '=' callback: '=',
}; };
} }
} }

View File

@@ -1,41 +1,38 @@
import { ComponentViewer } from '@standardnotes/snjs/dist/@types';
import { PureViewCtrl } from './../../views/abstract/pure_view_ctrl'; import { PureViewCtrl } from './../../views/abstract/pure_view_ctrl';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types'; import { WebDirective } from './../../types';
import { import { ContentType, PayloadSource, SNNote } from '@standardnotes/snjs';
ContentType,
PayloadSource,
SNComponent,
SNNote,
ComponentArea
} from '@standardnotes/snjs';
import template from '%/directives/revision-preview-modal.pug'; import template from '%/directives/revision-preview-modal.pug';
import { PayloadContent } from '@standardnotes/snjs'; import { PayloadContent } from '@standardnotes/snjs';
import { confirmDialog } from '@/services/alertService'; import { confirmDialog } from '@/services/alertService';
import { STRING_RESTORE_LOCKED_ATTEMPT } from '@/strings'; import { STRING_RESTORE_LOCKED_ATTEMPT } from '@/strings';
interface RevisionPreviewScope { interface RevisionPreviewScope {
uuid: string uuid: string;
content: PayloadContent content: PayloadContent;
application: WebApplication application: WebApplication;
} }
class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewScope { type State = {
componentViewer?: ComponentViewer;
};
$element: JQLite class RevisionPreviewModalCtrl
$timeout: ng.ITimeoutService extends PureViewCtrl<unknown, State>
uuid!: string implements RevisionPreviewScope
content!: PayloadContent {
title?: string $element: JQLite;
application!: WebApplication $timeout: ng.ITimeoutService;
unregisterComponent?: any uuid!: string;
note!: SNNote content!: PayloadContent;
title?: string;
application!: WebApplication;
note!: SNNote;
private originalNote!: SNNote; private originalNote!: SNNote;
/* @ngInject */ /* @ngInject */
constructor( constructor($element: JQLite, $timeout: ng.ITimeoutService) {
$element: JQLite,
$timeout: ng.ITimeoutService
) {
super($timeout); super($timeout);
this.$element = $element; this.$element = $element;
this.$timeout = $timeout; this.$timeout = $timeout;
@@ -43,53 +40,36 @@ class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewSc
$onInit() { $onInit() {
this.configure(); this.configure();
super.$onInit();
} }
$onDestroy() { $onDestroy() {
if (this.unregisterComponent) { if (this.state.componentViewer) {
this.unregisterComponent(); this.application.componentManager.destroyComponentViewer(
this.unregisterComponent = undefined; this.state.componentViewer
);
} }
super.$onDestroy();
} }
get componentManager() { get componentManager() {
return this.application.componentManager!; return this.application.componentManager;
} }
async configure() { async configure() {
this.note = await this.application.createTemplateItem( this.note = (await this.application.createTemplateItem(
ContentType.Note, ContentType.Note,
this.content this.content
) as SNNote; )) as SNNote;
this.originalNote = this.application.findItem(this.uuid) as SNNote; this.originalNote = this.application.findItem(this.uuid) as SNNote;
const editorForNote = this.componentManager.editorForNote(this.originalNote); const component = this.componentManager.editorForNote(this.originalNote);
if (editorForNote) { if (component) {
/** const componentViewer =
* Create temporary copy, as a lot of componentManager is uuid based, so might this.application.componentManager.createComponentViewer(component);
* interfere with active editor. Be sure to copy only the content, as the top level componentViewer.setReadonly(true);
* editor object has non-copyable properties like .window, which cannot be transfered componentViewer.lockReadonly = true;
*/ componentViewer.overrideContextItem = this.note;
const editorCopy = await this.application.createTemplateItem( this.setState({ componentViewer });
ContentType.Component,
editorForNote.safeContent
) as SNComponent;
this.componentManager.setReadonlyStateForComponent(editorCopy, true, true);
this.unregisterComponent = this.componentManager.registerHandler({
identifier: editorCopy.uuid,
areas: [ComponentArea.Editor],
contextRequestHandler: (componentUuid) => {
if (componentUuid === this.state.editor?.uuid) {
return this.note;
}
},
componentForSessionKeyHandler: (key) => {
if (key === this.componentManager.sessionKeyForComponent(this.state.editor!)) {
return this.state.editor;
}
}
});
this.setState({editor: editorCopy});
} }
} }
@@ -98,12 +78,19 @@ class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewSc
if (asCopy) { if (asCopy) {
await this.application.duplicateItem(this.originalNote, { await this.application.duplicateItem(this.originalNote, {
...this.content, ...this.content,
title: this.content.title ? this.content.title + ' (copy)' : undefined title: this.content.title
? this.content.title + ' (copy)'
: undefined,
}); });
} else { } else {
this.application.changeAndSaveItem(this.uuid, (mutator) => { this.application.changeAndSaveItem(
mutator.unsafe_setCustomContent(this.content); this.uuid,
}, true, PayloadSource.RemoteActionRetrieved); (mutator) => {
mutator.unsafe_setCustomContent(this.content);
},
true,
PayloadSource.RemoteActionRetrieved
);
} }
this.dismiss(); this.dismiss();
}; };
@@ -115,7 +102,7 @@ class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewSc
} }
confirmDialog({ confirmDialog({
text: "Are you sure you want to replace the current note's contents with what you see in this preview?", text: "Are you sure you want to replace the current note's contents with what you see in this preview?",
confirmButtonStyle: "danger" confirmButtonStyle: 'danger',
}).then((confirmed) => { }).then((confirmed) => {
if (confirmed) { if (confirmed) {
run(); run();
@@ -146,7 +133,7 @@ export class RevisionPreviewModal extends WebDirective {
uuid: '=', uuid: '=',
content: '=', content: '=',
title: '=', title: '=',
application: '=' application: '=',
}; };
} }
} }

View File

@@ -0,0 +1,115 @@
import { FunctionComponent } from 'preact';
import {
Title,
Subtitle,
Text,
LinkButton,
PreferencesGroup,
PreferencesPane,
PreferencesSegment,
} from '../components';
export const CloudLink: 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
not a government agency. As long as you keep your password safe, and
your password is reasonably strong, then you are the only person in
the world with the ability to decrypt your notes. For more on how we
handle your privacy and security, check out our easy to read{' '}
<a target="_blank" href="https://standardnotes.com/privacy">
Privacy Manifesto.
</a>
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Can I collaborate with others on a note?</Subtitle>
<Text>
Because of our encrypted architecture, Standard Notes does not
currently provide a real-time collaboration solution. Multiple users
can share the same account however, but editing at the same time may
result in sync conflicts, which may result in the duplication of
notes.
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Can I use Standard Notes totally offline?</Subtitle>
<Text>
Standard Notes can be used totally offline without an account, and
without an internet connection. You can find{' '}
<a
target="_blank"
href="https://standardnotes.com/help/59/can-i-use-standard-notes-totally-offline"
>
more details here.
</a>
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Cant find your question here?</Subtitle>
<LinkButton
className="mt-3"
label="Open FAQ"
link="https://standardnotes.com/help"
/>
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Community forum</Title>
<Text>
If you have an issue, found a bug or want to suggest a feature, you
can browse or post to the forum. Its recommended for non-account
related issues. Please read our{' '}
<a target="_blank" href="https://standardnotes.com/longevity/">
Longevity statement
</a>{' '}
before advocating for a feature request.
</Text>
<LinkButton
className="mt-3"
label="Go to the forum"
link="https://forum.standardnotes.org/"
/>
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Community groups</Title>
<Text>
Want to meet other passionate note-takers and privacy enthusiasts?
Want to share your feedback with us? Join the Standard Notes community
groups for discussions on security, themes, editors and more.
</Text>
<LinkButton
className="mt-3"
link="https://standardnotes.com/slack"
label="Join our Slack"
/>
<LinkButton
className="mt-3"
link="https://standardnotes.com/discord"
label="Join our Discord"
/>
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Account related issue?</Title>
<Text>
Send an email to help@standardnotes.com and well sort it out.
</Text>
<LinkButton
className="mt-3"
link="mailto: help@standardnotes.com"
label="Email us"
/>
</PreferencesSegment>
</PreferencesGroup>
</PreferencesPane>
);

View File

@@ -1,12 +1,13 @@
import { PreferencesGroup, PreferencesSegment } from "@/preferences/components"; import { PreferencesGroup, PreferencesSegment } from '@/preferences/components';
import { WebApplication } from "@/ui_models/application"; import { WebApplication } from '@/ui_models/application';
import { SNComponent } from "@standardnotes/snjs/dist/@types"; import { ComponentViewer, SNComponent } from '@standardnotes/snjs/dist/@types';
import { observer } from "mobx-react-lite"; import { observer } from 'mobx-react-lite';
import { FunctionComponent } from "preact"; import { FunctionComponent } from 'preact';
import { ExtensionItem } from "./extensions-segments"; import { ExtensionItem } from './extensions-segments';
import { ComponentView } from '@/components/ComponentView'; import { ComponentView } from '@/components/ComponentView';
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { PreferencesMenu } from '@/preferences/PreferencesMenu'; import { PreferencesMenu } from '@/preferences/PreferencesMenu';
import { useEffect, useState } from 'preact/hooks';
interface IProps { interface IProps {
application: WebApplication; application: WebApplication;
@@ -17,7 +18,17 @@ interface IProps {
export const ExtensionPane: FunctionComponent<IProps> = observer( export const ExtensionPane: FunctionComponent<IProps> = observer(
({ extension, application, appState, preferencesMenu }) => { ({ extension, application, appState, preferencesMenu }) => {
const latestVersion = preferencesMenu.extensionsLatestVersions.getVersion(extension); const [componentViewer] = useState<ComponentViewer>(
application.componentManager.createComponentViewer(extension)
);
const latestVersion =
preferencesMenu.extensionsLatestVersions.getVersion(extension);
useEffect(() => {
return () => {
application.componentManager.destroyComponentViewer(componentViewer);
};
}, [application, componentViewer]);
return ( return (
<div className="preferences-extension-pane color-foreground flex-grow flex flex-row overflow-y-auto min-h-0"> <div className="preferences-extension-pane color-foreground flex-grow flex flex-row overflow-y-auto min-h-0">
@@ -28,15 +39,18 @@ export const ExtensionPane: FunctionComponent<IProps> = observer(
application={application} application={application}
extension={extension} extension={extension}
first={false} first={false}
uninstall={() => application.deleteItem(extension).then(() => preferencesMenu.loadExtensionsPanes())} uninstall={() =>
toggleActivate={() => application.toggleComponent(extension).then(() => preferencesMenu.loadExtensionsPanes())} application
.deleteItem(extension)
.then(() => preferencesMenu.loadExtensionsPanes())
}
latestVersion={latestVersion} latestVersion={latestVersion}
/> />
<PreferencesSegment> <PreferencesSegment>
<ComponentView <ComponentView
application={application} application={application}
appState={appState} appState={appState}
componentUuid={extension.uuid} componentViewer={componentViewer}
/> />
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>
@@ -44,4 +58,5 @@ export const ExtensionPane: FunctionComponent<IProps> = observer(
</div> </div>
</div> </div>
); );
}); }
);

View File

@@ -77,11 +77,6 @@ export const Extensions: FunctionComponent<{
setExtensions(loadExtensions(application)); setExtensions(loadExtensions(application));
}; };
const toggleActivateExtension = (extension: SNComponent) => {
application.toggleComponent(extension);
setExtensions(loadExtensions(application));
};
const visibleExtensions = extensions.filter((extension) => { const visibleExtensions = extensions.filter((extension) => {
return ( return (
extension.package_info != undefined && extension.package_info != undefined &&
@@ -105,7 +100,6 @@ export const Extensions: FunctionComponent<{
latestVersion={extensionsLatestVersions.getVersion(extension)} latestVersion={extensionsLatestVersions.getVersion(extension)}
first={i === 0} first={i === 0}
uninstall={uninstallExtension} uninstall={uninstallExtension}
toggleActivate={toggleActivateExtension}
/> />
))} ))}
</div> </div>

View File

@@ -17,8 +17,9 @@ const DisclosureIconButton: FunctionComponent<{
<DisclosureButton <DisclosureButton
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${className ?? '' className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${
}`} className ?? ''
}`}
> >
<Icon type={icon} /> <Icon type={icon} />
</DisclosureButton> </DisclosureButton>

View File

@@ -1,40 +1,34 @@
import { import {
SNComponent, SNComponent,
PurePayload,
ComponentMutator, ComponentMutator,
AppDataField, AppDataField,
EncryptionIntent, EncryptionIntent,
ApplicationService, ApplicationService,
ApplicationEvent, ApplicationEvent,
removeFromArray, removeFromArray,
BackupFile, DesktopManagerInterface,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
/* eslint-disable camelcase */
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
// An interface used by the Desktop app to interact with SN
import { isDesktopApplication } from '@/utils'; import { isDesktopApplication } from '@/utils';
import { Bridge } from './bridge'; import { Bridge } from './bridge';
type UpdateObserverCallback = (component: SNComponent) => void; /**
type ComponentActivationCallback = (payload: PurePayload) => void; * An interface used by the Desktop application to interact with SN
type ComponentActivationObserver = { */
id: string; export class DesktopManager
callback: ComponentActivationCallback; extends ApplicationService
}; implements DesktopManagerInterface
{
export class DesktopManager extends ApplicationService {
$rootScope: ng.IRootScopeService; $rootScope: ng.IRootScopeService;
$timeout: ng.ITimeoutService; $timeout: ng.ITimeoutService;
componentActivationObservers: ComponentActivationObserver[] = [];
updateObservers: { updateObservers: {
callback: UpdateObserverCallback; callback: (component: SNComponent) => void;
}[] = []; }[] = [];
isDesktop = isDesktopApplication(); isDesktop = isDesktopApplication();
dataLoaded = false; dataLoaded = false;
lastSearchedText?: string; lastSearchedText?: string;
private removeComponentObserver?: () => void;
constructor( constructor(
$rootScope: ng.IRootScopeService, $rootScope: ng.IRootScopeService,
@@ -52,10 +46,7 @@ export class DesktopManager extends ApplicationService {
} }
deinit() { deinit() {
this.componentActivationObservers.length = 0;
this.updateObservers.length = 0; this.updateObservers.length = 0;
this.removeComponentObserver?.();
this.removeComponentObserver = undefined;
super.deinit(); super.deinit();
} }
@@ -73,9 +64,9 @@ export class DesktopManager extends ApplicationService {
this.bridge.onMajorDataChange(); this.bridge.onMajorDataChange();
} }
getExtServerHost() { getExtServerHost(): string {
console.assert(!!this.bridge.extensionsServerHost, 'extServerHost is null'); console.assert(!!this.bridge.extensionsServerHost, 'extServerHost is null');
return this.bridge.extensionsServerHost; return this.bridge.extensionsServerHost!;
} }
/** /**
@@ -83,7 +74,7 @@ export class DesktopManager extends ApplicationService {
* Keys are not passed into ItemParams, so the result is not encrypted * Keys are not passed into ItemParams, so the result is not encrypted
*/ */
convertComponentForTransmission(component: SNComponent) { convertComponentForTransmission(component: SNComponent) {
return this.application!.protocolService!.payloadByEncryptingPayload( return this.application.protocolService!.payloadByEncryptingPayload(
component.payloadRepresentation(), component.payloadRepresentation(),
EncryptionIntent.FileDecrypted EncryptionIntent.FileDecrypted
); );
@@ -107,7 +98,7 @@ export class DesktopManager extends ApplicationService {
}); });
} }
registerUpdateObserver(callback: UpdateObserverCallback) { registerUpdateObserver(callback: (component: SNComponent) => void) {
const observer = { const observer = {
callback: callback, callback: callback,
}; };
@@ -143,11 +134,11 @@ export class DesktopManager extends ApplicationService {
componentData: any, componentData: any,
error: any error: any
) { ) {
const component = this.application!.findItem(componentData.uuid); const component = this.application.findItem(componentData.uuid);
if (!component) { if (!component) {
return; return;
} }
const updatedComponent = await this.application!.changeAndSaveItem( const updatedComponent = await this.application.changeAndSaveItem(
component.uuid, component.uuid,
(m) => { (m) => {
const mutator = m as ComponentMutator; const mutator = m as ComponentMutator;
@@ -168,34 +159,8 @@ export class DesktopManager extends ApplicationService {
}); });
} }
desktop_registerComponentActivationObserver(
callback: ComponentActivationCallback
) {
const observer = { id: `${Math.random}`, callback: callback };
this.componentActivationObservers.push(observer);
return observer;
}
desktop_deregisterComponentActivationObserver(
observer: ComponentActivationObserver
) {
removeFromArray(this.componentActivationObservers, observer);
}
/* Notify observers that a component has been registered/activated */
async notifyComponentActivation(component: SNComponent) {
const serializedComponent = await this.convertComponentForTransmission(
component
);
this.$timeout(() => {
for (const observer of this.componentActivationObservers) {
observer.callback(serializedComponent);
}
});
}
async desktop_requestBackupFile() { async desktop_requestBackupFile() {
const data = await this.application!.createBackupFile( const data = await this.application.createBackupFile(
this.application.hasProtectionSources() this.application.hasProtectionSources()
? EncryptionIntent.FileEncrypted ? EncryptionIntent.FileEncrypted
: EncryptionIntent.FileDecrypted : EncryptionIntent.FileDecrypted

View File

@@ -7,12 +7,14 @@ import {
removeFromArray, removeFromArray,
ApplicationEvent, ApplicationEvent,
ContentType, ContentType,
UuidString,
FeatureStatus,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
const CACHED_THEMES_KEY = 'cachedThemes'; const CACHED_THEMES_KEY = 'cachedThemes';
export class ThemeManager extends ApplicationService { export class ThemeManager extends ApplicationService {
private activeThemes: string[] = []; private activeThemes: UuidString[] = [];
private unregisterDesktop!: () => void; private unregisterDesktop!: () => void;
private unregisterStream!: () => void; private unregisterStream!: () => void;
@@ -22,6 +24,8 @@ export class ThemeManager extends ApplicationService {
this.deactivateAllThemes(); this.deactivateAllThemes();
} else if (event === ApplicationEvent.StorageReady) { } else if (event === ApplicationEvent.StorageReady) {
await this.activateCachedThemes(); await this.activateCachedThemes();
} else if (event === ApplicationEvent.FeaturesUpdated) {
this.reloadThemeStatus();
} }
} }
@@ -34,11 +38,24 @@ export class ThemeManager extends ApplicationService {
this.activeThemes.length = 0; this.activeThemes.length = 0;
this.unregisterDesktop(); this.unregisterDesktop();
this.unregisterStream(); this.unregisterStream();
(this.unregisterDesktop as any) = undefined; (this.unregisterDesktop as unknown) = undefined;
(this.unregisterStream as any) = undefined; (this.unregisterStream as unknown) = undefined;
super.deinit(); super.deinit();
} }
reloadThemeStatus(): void {
for (const themeUuid of this.activeThemes) {
const theme = this.application.findItem(themeUuid) as SNTheme;
if (
!theme ||
this.application.getFeatureStatus(theme.identifier) !==
FeatureStatus.Entitled
) {
this.deactivateTheme(themeUuid);
}
}
}
/** @override */ /** @override */
async onAppStart() { async onAppStart() {
super.onAppStart(); super.onAppStart();
@@ -99,7 +116,11 @@ export class ThemeManager extends ApplicationService {
return; return;
} }
this.activeThemes.push(theme.uuid); this.activeThemes.push(theme.uuid);
const url = this.application!.componentManager!.urlForComponent(theme)!; const url = this.application.componentManager.urlForComponent(theme);
if (!url) {
return;
}
const link = document.createElement('link'); const link = document.createElement('link');
link.href = url; link.href = url;
link.type = 'text/css'; link.type = 'text/css';
@@ -125,19 +146,19 @@ export class ThemeManager extends ApplicationService {
} }
private async cacheThemes() { private async cacheThemes() {
const themes = this.application!.getAll(this.activeThemes) as SNTheme[]; const themes = this.application.getAll(this.activeThemes) as SNTheme[];
const mapped = await Promise.all( const mapped = await Promise.all(
themes.map(async (theme) => { themes.map(async (theme) => {
const payload = theme.payloadRepresentation(); const payload = theme.payloadRepresentation();
const processedPayload = const processedPayload =
await this.application!.protocolService!.payloadByEncryptingPayload( await this.application.protocolService.payloadByEncryptingPayload(
payload, payload,
EncryptionIntent.LocalStorageDecrypted EncryptionIntent.LocalStorageDecrypted
); );
return processedPayload; return processedPayload;
}) })
); );
return this.application!.setValue( return this.application.setValue(
CACHED_THEMES_KEY, CACHED_THEMES_KEY,
mapped, mapped,
StorageValueModes.Nonwrapped StorageValueModes.Nonwrapped
@@ -154,15 +175,15 @@ export class ThemeManager extends ApplicationService {
} }
private async getCachedThemes() { private async getCachedThemes() {
const cachedThemes = (await this.application!.getValue( const cachedThemes = (await this.application.getValue(
CACHED_THEMES_KEY, CACHED_THEMES_KEY,
StorageValueModes.Nonwrapped StorageValueModes.Nonwrapped
)) as SNTheme[]; )) as SNTheme[];
if (cachedThemes) { if (cachedThemes) {
const themes = []; const themes = [];
for (const cachedTheme of cachedThemes) { for (const cachedTheme of cachedThemes) {
const payload = this.application!.createPayloadFromObject(cachedTheme); const payload = this.application.createPayloadFromObject(cachedTheme);
const theme = this.application!.createItemFromPayload( const theme = this.application.createItemFromPayload(
payload payload
) as SNTheme; ) as SNTheme;
themes.push(theme); themes.push(theme);

View File

@@ -77,6 +77,8 @@ export class AppState {
editingTag: SNTag | undefined; editingTag: SNTag | undefined;
_templateTag: SNTag | undefined; _templateTag: SNTag | undefined;
private multiEditorSupport = false;
readonly quickSettingsMenu = new QuickSettingsState(); readonly quickSettingsMenu = new QuickSettingsState();
readonly accountMenu: AccountMenuState; readonly accountMenu: AccountMenuState;
readonly actionsMenu = new ActionsMenuState(); readonly actionsMenu = new ActionsMenuState();
@@ -224,27 +226,21 @@ export class AppState {
storage.set(StorageKey.ShowBetaWarning, true); storage.set(StorageKey.ShowBetaWarning, true);
} }
/**
* Creates a new editor if one doesn't exist. If one does, we'll replace the
* editor's note with an empty one.
*/
async createEditor(title?: string) { async createEditor(title?: string) {
const activeEditor = this.getActiveEditor(); if (!this.multiEditorSupport) {
this.closeActiveEditor();
}
const activeTagUuid = this.selectedTag const activeTagUuid = this.selectedTag
? this.selectedTag.isSmartTag ? this.selectedTag.isSmartTag
? undefined ? undefined
: this.selectedTag.uuid : this.selectedTag.uuid
: undefined; : undefined;
if (!activeEditor) { await this.application.editorGroup.createEditor(
this.application.editorGroup.createEditor( undefined,
undefined, title,
title, activeTagUuid
activeTagUuid );
);
} else {
await activeEditor.reset(title, activeTagUuid);
}
} }
getActiveEditor() { getActiveEditor() {

View File

@@ -167,12 +167,12 @@ export class NotesState {
return; return;
} }
if (!this.activeEditor) { if (this.activeEditor) {
this.application.editorGroup.createEditor(noteUuid); this.application.editorGroup.closeActiveEditor();
} else {
this.activeEditor.setNote(note);
} }
await this.application.editorGroup.createEditor(noteUuid);
this.appState.noteTags.reloadTags(); this.appState.noteTags.reloadTags();
await this.onActiveEditorChanged(); await this.onActiveEditorChanged();

View File

@@ -1,16 +1,20 @@
import { ContentType, SNSmartTag, SNTag, UuidString } from '@standardnotes/snjs'; import {
ContentType,
SNSmartTag,
SNTag,
UuidString,
} from '@standardnotes/snjs';
import { import {
action, action,
computed, computed,
makeAutoObservable, makeAutoObservable,
makeObservable, makeObservable,
observable, observable,
runInAction runInAction,
} from 'mobx'; } from 'mobx';
import { WebApplication } from '../application'; import { WebApplication } from '../application';
import { FeaturesState } from './features_state'; import { FeaturesState } from './features_state';
export class TagsState { export class TagsState {
tags: SNTag[] = []; tags: SNTag[] = [];
smartTags: SNSmartTag[] = []; smartTags: SNSmartTag[] = [];

View File

@@ -18,12 +18,9 @@ import {
PermissionDialog, PermissionDialog,
Platform, Platform,
SNApplication, SNApplication,
SNComponent,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import angular from 'angular'; import angular from 'angular';
import { ComponentModalScope } from './../directives/views/componentModal';
import { AccountSwitcherScope, PermissionsModalScope } from './../types'; import { AccountSwitcherScope, PermissionsModalScope } from './../types';
import { ComponentGroup } from './component_group';
type WebServices = { type WebServices = {
appState: AppState; appState: AppState;
@@ -40,7 +37,6 @@ export class WebApplication extends SNApplication {
private webServices!: WebServices; private webServices!: WebServices;
private currentAuthenticationElement?: angular.IRootElementService; private currentAuthenticationElement?: angular.IRootElementService;
public editorGroup: EditorGroup; public editorGroup: EditorGroup;
public componentGroup: ComponentGroup;
/* @ngInject */ /* @ngInject */
constructor( constructor(
@@ -71,8 +67,6 @@ export class WebApplication extends SNApplication {
this.scope = scope; this.scope = scope;
deviceInterface.setApplication(this); deviceInterface.setApplication(this);
this.editorGroup = new EditorGroup(this); this.editorGroup = new EditorGroup(this);
this.componentGroup = new ComponentGroup(this);
this.openModalComponent = this.openModalComponent.bind(this);
this.presentPermissionsDialog = this.presentPermissionsDialog.bind(this); this.presentPermissionsDialog = this.presentPermissionsDialog.bind(this);
} }
@@ -85,14 +79,12 @@ export class WebApplication extends SNApplication {
(service as any).application = undefined; (service as any).application = undefined;
} }
this.webServices = {} as WebServices; this.webServices = {} as WebServices;
(this.$compile as any) = undefined; (this.$compile as unknown) = undefined;
this.editorGroup.deinit(); this.editorGroup.deinit();
this.componentGroup.deinit(); (this.scope as any).application = undefined;
(this.scope! as any).application = undefined;
this.scope!.$destroy(); this.scope!.$destroy();
this.scope = undefined; this.scope = undefined;
(this.openModalComponent as any) = undefined; (this.presentPermissionsDialog as unknown) = undefined;
(this.presentPermissionsDialog as any) = undefined;
/** Allow our Angular directives to be destroyed and any pending digest cycles /** Allow our Angular directives to be destroyed and any pending digest cycles
* to complete before destroying the global application instance and all its services */ * to complete before destroying the global application instance and all its services */
setTimeout(() => { setTimeout(() => {
@@ -105,8 +97,7 @@ export class WebApplication extends SNApplication {
onStart(): void { onStart(): void {
super.onStart(); super.onStart();
this.componentManager!.openModalComponent = this.openModalComponent; this.componentManager.presentPermissionsDialog =
this.componentManager!.presentPermissionsDialog =
this.presentPermissionsDialog; this.presentPermissionsDialog;
} }
@@ -210,24 +201,6 @@ export class WebApplication extends SNApplication {
this.applicationElement.append(el); this.applicationElement.append(el);
} }
async openModalComponent(component: SNComponent): Promise<void> {
switch (component.package_info?.identifier) {
case 'org.standardnotes.cloudlink':
if (!(await this.authorizeCloudLinkAccess())) {
return;
}
break;
}
const scope = this.scope!.$new(true) as Partial<ComponentModalScope>;
scope.componentUuid = component.uuid;
scope.application = this;
const el = this.$compile!(
"<component-modal application='application' component-uuid='componentUuid' " +
"class='sk-modal'></component-modal>"
)(scope as any);
this.applicationElement.append(el);
}
presentPermissionsDialog(dialog: PermissionDialog) { presentPermissionsDialog(dialog: PermissionDialog) {
const scope = this.scope!.$new(true) as PermissionsModalScope; const scope = this.scope!.$new(true) as PermissionsModalScope;
scope.permissionsString = dialog.permissionsString; scope.permissionsString = dialog.permissionsString;

View File

@@ -1,100 +0,0 @@
import { SNComponent, ComponentArea, removeFromArray, addIfUnique , UuidString } from '@standardnotes/snjs';
import { WebApplication } from './application';
/** Areas that only allow a single component to be active */
const SingleComponentAreas = [
ComponentArea.Editor,
ComponentArea.NoteTags,
ComponentArea.TagsList
];
export class ComponentGroup {
private application: WebApplication
changeObservers: any[] = []
activeComponents: UuidString[] = []
constructor(application: WebApplication) {
this.application = application;
}
get componentManager() {
return this.application.componentManager!;
}
public deinit() {
(this.application as any) = undefined;
}
async activateComponent(component: SNComponent) {
if (this.activeComponents.includes(component.uuid)) {
return;
}
if (SingleComponentAreas.includes(component.area)) {
const currentActive = this.activeComponentForArea(component.area);
if (currentActive) {
await this.deactivateComponent(currentActive, false);
}
}
addIfUnique(this.activeComponents, component.uuid);
await this.componentManager.activateComponent(component.uuid);
this.notifyObservers();
}
async deactivateComponent(component: SNComponent, notify = true) {
if (!this.activeComponents.includes(component.uuid)) {
return;
}
removeFromArray(this.activeComponents, component.uuid);
/** If this function is called as part of global application deinit (locking),
* componentManager can be destroyed. In this case, it's harmless to not take any
* action since the componentManager will be destroyed, and the component will
* essentially be deregistered. */
if(this.componentManager) {
await this.componentManager.deactivateComponent(component.uuid);
if(notify) {
this.notifyObservers();
}
}
}
async deactivateComponentForArea(area: ComponentArea) {
const component = this.activeComponentForArea(area);
if (component) {
return this.deactivateComponent(component);
}
}
activeComponentForArea(area: ComponentArea) {
return this.activeComponentsForArea(area)[0];
}
activeComponentsForArea(area: ComponentArea) {
return this.allActiveComponents().filter((c) => c.area === area);
}
allComponentsForArea(area: ComponentArea) {
return this.componentManager.componentsForArea(area);
}
private allActiveComponents() {
return this.application.getAll(this.activeComponents) as SNComponent[];
}
/**
* Notifies observer when the active editor has changed.
*/
public addChangeObserver(callback: () => void) {
this.changeObservers.push(callback);
callback();
return () => {
removeFromArray(this.changeObservers, callback);
};
}
private notifyObservers() {
for (const observer of this.changeObservers) {
observer();
}
}
}

View File

@@ -3,37 +3,36 @@ import {
ContentType, ContentType,
PayloadSource, PayloadSource,
UuidString, UuidString,
TagMutator,
SNTag, SNTag,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import { WebApplication } from './application'; import { WebApplication } from './application';
import { NoteTagsState } from './app_state/note_tags_state';
export class Editor { export class Editor {
public note!: SNNote; public note!: SNNote;
private application: WebApplication; private application: WebApplication;
private _onNoteChange?: () => void; private onNoteValueChange?: (note: SNNote, source: PayloadSource) => void;
private _onNoteValueChange?: (note: SNNote, source?: PayloadSource) => void;
private removeStreamObserver?: () => void; private removeStreamObserver?: () => void;
public isTemplateNote = false; public isTemplateNote = false;
constructor( constructor(
application: WebApplication, application: WebApplication,
noteUuid: string | undefined, noteUuid: string | undefined,
noteTitle: string | undefined, private defaultTitle: string | undefined,
noteTag: UuidString | undefined private defaultTag: UuidString | undefined
) { ) {
this.application = application; this.application = application;
if (noteUuid) { if (noteUuid) {
this.note = application.findItem(noteUuid) as SNNote; this.note = application.findItem(noteUuid) as SNNote;
this.streamItems();
} else {
this.reset(noteTitle, noteTag)
.then(() => this.streamItems())
.catch(console.error);
} }
} }
async initialize(): Promise<void> {
if (!this.note) {
await this.createTemplateNote(this.defaultTitle, this.defaultTag);
}
this.streamItems();
}
private streamItems() { private streamItems() {
this.removeStreamObserver = this.application.streamItems( this.removeStreamObserver = this.application.streamItems(
ContentType.Note, ContentType.Note,
@@ -45,14 +44,12 @@ export class Editor {
deinit() { deinit() {
this.removeStreamObserver?.(); this.removeStreamObserver?.();
(this.removeStreamObserver as any) = undefined; (this.removeStreamObserver as unknown) = undefined;
this._onNoteChange = undefined; (this.application as unknown) = undefined;
(this.application as any) = undefined; this.onNoteValueChange = undefined;
this._onNoteChange = undefined;
this._onNoteValueChange = undefined;
} }
private handleNoteStream(notes: SNNote[], source?: PayloadSource) { private handleNoteStream(notes: SNNote[], source: PayloadSource) {
/** Update our note object reference whenever it changes */ /** Update our note object reference whenever it changes */
const matchingNote = notes.find((item) => { const matchingNote = notes.find((item) => {
return item.uuid === this.note.uuid; return item.uuid === this.note.uuid;
@@ -60,7 +57,7 @@ export class Editor {
if (matchingNote) { if (matchingNote) {
this.isTemplateNote = false; this.isTemplateNote = false;
this.note = matchingNote; this.note = matchingNote;
this._onNoteValueChange && this._onNoteValueChange!(matchingNote, source); this.onNoteValueChange?.(matchingNote, source);
} }
} }
@@ -73,53 +70,31 @@ export class Editor {
* Reverts the editor to a blank state, removing any existing note from view, * Reverts the editor to a blank state, removing any existing note from view,
* and creating a placeholder note. * and creating a placeholder note.
*/ */
async reset(noteTitle = '', noteTag?: UuidString) { async createTemplateNote(defaultTitle?: string, noteTag?: UuidString) {
const note = (await this.application.createTemplateItem(ContentType.Note, { const note = (await this.application.createTemplateItem(ContentType.Note, {
text: '', text: '',
title: noteTitle, title: defaultTitle,
references: [], references: [],
})) as SNNote; })) as SNNote;
if (noteTag) { if (noteTag) {
const tag = this.application.findItem(noteTag) as SNTag; const tag = this.application.findItem(noteTag) as SNTag;
await this.application.addTagHierarchyToNote(note, tag); await this.application.addTagHierarchyToNote(note, tag);
} }
if (!this.isTemplateNote || this.note.title !== note.title) { this.isTemplateNote = true;
this.setNote(note as SNNote, true); this.note = note;
} this.onNoteValueChange?.(this.note, this.note.payload.source);
}
/**
* Register to be notified when the editor's note changes.
*/
public onNoteChange(callback: () => void) {
this._onNoteChange = callback;
if (this.note) {
callback();
}
}
public clearNoteChangeListener() {
this._onNoteChange = undefined;
} }
/** /**
* Register to be notified when the editor's note's values change * Register to be notified when the editor's note's values change
* (and thus a new object reference is created) * (and thus a new object reference is created)
*/ */
public onNoteValueChange( public setOnNoteValueChange(
callback: (note: SNNote, source?: PayloadSource) => void callback: (note: SNNote, source: PayloadSource) => void
) { ) {
this._onNoteValueChange = callback; this.onNoteValueChange = callback;
} if (this.note) {
this.onNoteValueChange(this.note, this.note.payload.source);
/**
* Sets the editor contents by setting its note.
*/
public setNote(note: SNNote, isTemplate = false) {
this.note = note;
this.isTemplateNote = isTemplate;
if (this._onNoteChange) {
this._onNoteChange();
} }
} }
} }

View File

@@ -2,31 +2,31 @@ import { removeFromArray, UuidString } from '@standardnotes/snjs';
import { Editor } from './editor'; import { Editor } from './editor';
import { WebApplication } from './application'; import { WebApplication } from './application';
type EditorGroupChangeCallback = () => void type EditorGroupChangeCallback = () => void;
export class EditorGroup { export class EditorGroup {
public editors: Editor[] = [];
public editors: Editor[] = [] private application: WebApplication;
private application: WebApplication changeObservers: EditorGroupChangeCallback[] = [];
changeObservers: EditorGroupChangeCallback[] = []
constructor(application: WebApplication) { constructor(application: WebApplication) {
this.application = application; this.application = application;
} }
public deinit() { public deinit() {
(this.application as any) = undefined; (this.application as unknown) = undefined;
for (const editor of this.editors) { for (const editor of this.editors) {
this.deleteEditor(editor); this.deleteEditor(editor);
} }
} }
createEditor( async createEditor(
noteUuid?: string, noteUuid?: string,
noteTitle?: string, noteTitle?: string,
noteTag?: UuidString noteTag?: UuidString
) { ) {
const editor = new Editor(this.application, noteUuid, noteTitle, noteTag); const editor = new Editor(this.application, noteUuid, noteTitle, noteTag);
await editor.initialize();
this.editors.push(editor); this.editors.push(editor);
this.notifyObservers(); this.notifyObservers();
} }
@@ -43,13 +43,13 @@ export class EditorGroup {
closeActiveEditor() { closeActiveEditor() {
const activeEditor = this.activeEditor; const activeEditor = this.activeEditor;
if(activeEditor) { if (activeEditor) {
this.deleteEditor(activeEditor); this.deleteEditor(activeEditor);
} }
} }
closeAllEditors() { closeAllEditors() {
for(const editor of this.editors) { for (const editor of this.editors) {
this.deleteEditor(editor); this.deleteEditor(editor);
} }
} }

View File

@@ -3,29 +3,26 @@ import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { autorun, IReactionDisposer, IReactionPublic } from 'mobx'; import { autorun, IReactionDisposer, IReactionPublic } from 'mobx';
export type CtrlState = Partial<Record<string, any>> export type CtrlState = Partial<Record<string, any>>;
export type CtrlProps = Partial<Record<string, any>> export type CtrlProps = Partial<Record<string, any>>;
export class PureViewCtrl<P = CtrlProps, S = CtrlState> { export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
$timeout: ng.ITimeoutService $timeout: ng.ITimeoutService;
/** Passed through templates */ /** Passed through templates */
application!: WebApplication application!: WebApplication;
state: S = {} as any state: S = {} as any;
private unsubApp: any private unsubApp: any;
private unsubState: any private unsubState: any;
private stateTimeout?: ng.IPromise<void> private stateTimeout?: ng.IPromise<void>;
/** /**
* Subclasses can optionally add an ng-if=ctrl.templateReady to make sure that * Subclasses can optionally add an ng-if=ctrl.templateReady to make sure that
* no Angular handlebars/syntax render in the UI before display data is ready. * no Angular handlebars/syntax render in the UI before display data is ready.
*/ */
protected templateReady = false protected templateReady = false;
private reactionDisposers: IReactionDisposer[] = []; private reactionDisposers: IReactionDisposer[] = [];
/* @ngInject */ /* @ngInject */
constructor( constructor($timeout: ng.ITimeoutService, public props: P = {} as any) {
$timeout: ng.ITimeoutService,
public props: P = {} as any
) {
this.$timeout = $timeout; this.$timeout = $timeout;
} }
@@ -91,8 +88,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
/** @override */ /** @override */
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
afterStateChange(): void { afterStateChange(): void {}
}
/** @returns a promise that resolves after the UI has been updated. */ /** @returns a promise that resolves after the UI has been updated. */
flushUI(): angular.IPromise<void> { flushUI(): angular.IPromise<void> {
@@ -129,22 +125,24 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
if (this.application!.isLaunched()) { if (this.application!.isLaunched()) {
this.onAppLaunch(); this.onAppLaunch();
} }
this.unsubApp = this.application!.addEventObserver(async (eventName, data: any) => { this.unsubApp = this.application!.addEventObserver(
this.onAppEvent(eventName, data); async (eventName, data: any) => {
if (eventName === ApplicationEvent.Started) { this.onAppEvent(eventName, data);
await this.onAppStart(); if (eventName === ApplicationEvent.Started) {
} else if (eventName === ApplicationEvent.Launched) { await this.onAppStart();
await this.onAppLaunch(); } else if (eventName === ApplicationEvent.Launched) {
} else if (eventName === ApplicationEvent.CompletedIncrementalSync) { await this.onAppLaunch();
this.onAppIncrementalSync(); } else if (eventName === ApplicationEvent.CompletedIncrementalSync) {
} else if (eventName === ApplicationEvent.CompletedFullSync) { this.onAppIncrementalSync();
this.onAppFullSync(); } else if (eventName === ApplicationEvent.CompletedFullSync) {
} else if (eventName === ApplicationEvent.KeyStatusChanged) { this.onAppFullSync();
this.onAppKeyChange(); } else if (eventName === ApplicationEvent.KeyStatusChanged) {
} else if (eventName === ApplicationEvent.LocalDataLoaded) { this.onAppKeyChange();
this.onLocalDataLoaded(); } else if (eventName === ApplicationEvent.LocalDataLoaded) {
this.onLocalDataLoaded();
}
} }
}); );
} }
onAppEvent(eventName: ApplicationEvent, data?: any) { onAppEvent(eventName: ApplicationEvent, data?: any) {
@@ -175,5 +173,4 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
onAppFullSync() { onAppFullSync() {
/** Optional override */ /** Optional override */
} }
} }

View File

@@ -115,14 +115,17 @@ class ApplicationViewCtrl extends PureViewCtrl<
/** @override */ /** @override */
async onAppEvent(eventName: ApplicationEvent) { async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName); super.onAppEvent(eventName);
if (eventName === ApplicationEvent.LocalDatabaseReadError) { switch (eventName) {
alertDialog({ case ApplicationEvent.LocalDatabaseReadError:
text: 'Unable to load local database. Please restart the app and try again.', alertDialog({
}); text: 'Unable to load local database. Please restart the app and try again.',
} else if (eventName === ApplicationEvent.LocalDatabaseWriteError) { });
alertDialog({ break;
text: 'Unable to write to local database. Please restart the app and try again.', case ApplicationEvent.LocalDatabaseWriteError:
}); alertDialog({
text: 'Unable to write to local database. Please restart the app and try again.',
});
break;
} }
} }

View File

@@ -219,6 +219,7 @@ class ChallengeModalCtrl extends PureViewCtrl<unknown, ChallengeModalState> {
$onDestroy() { $onDestroy() {
render(<></>, this.$element[0]); render(<></>, this.$element[0]);
super.$onDestroy();
} }
private render() { private render() {

View File

@@ -34,10 +34,8 @@
) )
.title.overflow-auto .title.overflow-auto
input#note-title-editor.input( input#note-title-editor.input(
ng-blur='self.onTitleBlur()',
ng-change='self.onTitleChange()', ng-change='self.onTitleChange()',
ng-disabled='self.noteLocked', ng-disabled='self.noteLocked',
ng-focus='self.onTitleFocus()',
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)', ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
ng-model='self.editorValues.title', ng-model='self.editorValues.title',
select-on-focus='true', select-on-focus='true',
@@ -76,7 +74,7 @@
callback='self.editorMenuOnSelect', callback='self.editorMenuOnSelect',
current-item='self.note', current-item='self.note',
ng-if='self.state.showEditorMenu', ng-if='self.state.showEditorMenu',
selected-editor-uuid='self.state.editorComponent && self.state.editorComponent.uuid', selected-editor-uuid='self.state.editorComponentViewer && self.state.editorComponentViewer.component.uuid',
application='self.application' application='self.application'
) )
.sk-app-bar-item( .sk-app-bar-item(
@@ -114,9 +112,10 @@
property="'left'" property="'left'"
) )
component-view.component-view( component-view.component-view(
component-uuid='self.state.editorComponent.uuid', component-viewer='self.state.editorComponentViewer',
ng-if='self.state.editorComponent && !self.state.editorUnloading', ng-if='self.state.editorComponentViewer',
on-load='self.onEditorLoad', on-load='self.onEditorLoad',
request-reload='self.editorComponentViewerRequestsReload'
application='self.application' application='self.application'
app-state='self.appState' app-state='self.appState'
) )
@@ -126,7 +125,7 @@
ng-change='self.contentChanged()', ng-change='self.contentChanged()',
ng-click='self.clickedTextArea()', ng-click='self.clickedTextArea()',
ng-focus='self.onContentFocus()', ng-focus='self.onContentFocus()',
ng-if='!self.state.editorComponent && !self.state.textareaUnloading', ng-if='self.state.editorStateDidLoad && !self.state.editorComponentViewer && !self.state.textareaUnloading',
ng-model='self.editorValues.text', ng-model='self.editorValues.text',
ng-model-options='{ debounce: self.state.editorDebounce}', ng-model-options='{ debounce: self.state.editorDebounce}',
ng-readonly='self.noteLocked', ng-readonly='self.noteLocked',
@@ -156,24 +155,23 @@
| There was an error decrypting this item. Ensure you are running the | There was an error decrypting this item. Ensure you are running the
| latest version of this app, then sign out and sign back in to try again. | latest version of this app, then sign out and sign back in to try again.
#editor-pane-component-stack(ng-if='!self.note.errorDecrypting' ng-show='self.note') #editor-pane-component-stack(ng-if='!self.note.errorDecrypting' ng-show='self.note')
#component-stack-menu-bar.sk-app-bar.no-edges(ng-if='self.state.stackComponents.length') #component-stack-menu-bar.sk-app-bar.no-edges(ng-if='self.state.availableStackComponents.length')
.left .left
.sk-app-bar-item( .sk-app-bar-item(
ng-repeat='component in self.state.stackComponents track by component.uuid' ng-repeat='component in self.state.availableStackComponents track by component.uuid'
ng-click='self.toggleStackComponentForCurrentItem(component)', ng-click='self.toggleStackComponent(component)',
) )
.sk-app-bar-item-column .sk-app-bar-item-column
.sk-circle.small( .sk-circle.small(
ng-class="{'info' : !self.stackComponentHidden(component) && component.active, 'neutral' : self.stackComponentHidden(component) || !component.active}" ng-class="{'info' : self.stackComponentExpanded(component) && component.active, 'neutral' : !self.stackComponentExpanded(component)}"
) )
.sk-app-bar-item-column .sk-app-bar-item-column
.sk-label {{component.name}} .sk-label {{component.name}}
.sn-component .sn-component
component-view.component-view.component-stack-item( component-view.component-view.component-stack-item(
ng-repeat='component in self.state.stackComponents track by component.uuid', ng-repeat='viewer in self.state.stackComponentViewers track by viewer.componentUuid',
component-uuid='component.uuid', component-viewer='viewer',
manual-dealloc='true', manual-dealloc='true',
ng-show='!self.stackComponentHidden(component)',
application='self.application' application='self.application'
app-state='self.appState' app-state='self.appState'
) )

View File

@@ -11,11 +11,14 @@ import {
SNComponent, SNComponent,
SNNote, SNNote,
NoteMutator, NoteMutator,
Uuids,
ComponentArea, ComponentArea,
PrefKey, PrefKey,
ComponentMutator, ComponentMutator,
PayloadSource, PayloadSource,
ComponentViewer,
ComponentManagerEvent,
TransactionalMutation,
ItemMutator,
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import { isDesktopApplication } from '@/utils'; import { isDesktopApplication } from '@/utils';
@@ -52,8 +55,10 @@ type NoteStatus = {
}; };
type EditorState = { type EditorState = {
stackComponents: SNComponent[]; availableStackComponents: SNComponent[];
editorComponent?: SNComponent; stackComponentViewers: ComponentViewer[];
editorComponentViewer?: ComponentViewer;
editorStateDidLoad: boolean;
saveError?: any; saveError?: any;
noteStatus?: NoteStatus; noteStatus?: NoteStatus;
marginResizersEnabled?: boolean; marginResizersEnabled?: boolean;
@@ -64,11 +69,6 @@ type EditorState = {
showEditorMenu: boolean; showEditorMenu: boolean;
showHistoryMenu: boolean; showHistoryMenu: boolean;
spellcheck: boolean; spellcheck: boolean;
/**
* Setting to false then true will allow the current editor component-view to be destroyed
* then re-initialized. Used when changing between component editors.
*/
editorUnloading: boolean;
/** Setting to true then false will allow the main content textarea to be destroyed /** Setting to true then false will allow the main content textarea to be destroyed
* then re-initialized. Used when reloading spellcheck status. */ * then re-initialized. Used when reloading spellcheck status. */
textareaUnloading: boolean; textareaUnloading: boolean;
@@ -97,7 +97,6 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
private leftPanelPuppet?: PanelPuppet; private leftPanelPuppet?: PanelPuppet;
private rightPanelPuppet?: PanelPuppet; private rightPanelPuppet?: PanelPuppet;
private unregisterComponent: any;
private saveTimeout?: ng.IPromise<void>; private saveTimeout?: ng.IPromise<void>;
private statusTimeout?: ng.IPromise<void>; private statusTimeout?: ng.IPromise<void>;
private lastEditorFocusEventSource?: EventSource; private lastEditorFocusEventSource?: EventSource;
@@ -105,10 +104,11 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
onEditorLoad?: () => void; onEditorLoad?: () => void;
private scrollPosition = 0; private scrollPosition = 0;
private removeTrashKeyObserver?: any; private removeTrashKeyObserver?: () => void;
private removeTabObserver?: any; private removeTabObserver?: () => void;
private removeComponentStreamObserver?: () => void;
private removeComponentManagerObserver?: () => void;
private removeComponentsObserver!: () => void;
private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null; private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null;
/* @ngInject */ /* @ngInject */
@@ -125,25 +125,26 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this); this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this);
this.setScrollPosition = this.setScrollPosition.bind(this); this.setScrollPosition = this.setScrollPosition.bind(this);
this.resetScrollPosition = this.resetScrollPosition.bind(this); this.resetScrollPosition = this.resetScrollPosition.bind(this);
this.editorComponentViewerRequestsReload =
this.editorComponentViewerRequestsReload.bind(this);
this.onEditorLoad = () => { this.onEditorLoad = () => {
this.application.getDesktopService().redoSearch(); this.application.getDesktopService().redoSearch();
}; };
} }
deinit() { deinit() {
this.clearNoteProtectionInactivityTimer(); this.removeComponentStreamObserver?.();
this.editor.clearNoteChangeListener(); (this.removeComponentStreamObserver as unknown) = undefined;
this.removeComponentsObserver(); this.removeComponentManagerObserver?.();
(this.removeComponentsObserver as unknown) = undefined; (this.removeComponentManagerObserver as unknown) = undefined;
this.removeTrashKeyObserver(); this.removeTrashKeyObserver?.();
this.removeTrashKeyObserver = undefined; this.removeTrashKeyObserver = undefined;
this.removeTabObserver && this.removeTabObserver(); this.clearNoteProtectionInactivityTimer();
this.removeTabObserver?.();
this.removeTabObserver = undefined; this.removeTabObserver = undefined;
this.leftPanelPuppet = undefined; this.leftPanelPuppet = undefined;
this.rightPanelPuppet = undefined; this.rightPanelPuppet = undefined;
this.onEditorLoad = undefined; this.onEditorLoad = undefined;
this.unregisterComponent();
this.unregisterComponent = undefined;
this.saveTimeout = undefined; this.saveTimeout = undefined;
this.statusTimeout = undefined; this.statusTimeout = undefined;
(this.onPanelResizeFinish as unknown) = undefined; (this.onPanelResizeFinish as unknown) = undefined;
@@ -162,55 +163,82 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
$onInit() { $onInit() {
super.$onInit(); super.$onInit();
this.registerKeyboardShortcuts(); this.registerKeyboardShortcuts();
this.editor.onNoteChange(() => { this.editor.setOnNoteValueChange((note, source) => {
this.handleEditorNoteChange(); this.onNoteChanges(note, source);
});
this.editor.onNoteValueChange((note, source) => {
if (isPayloadSourceRetrieved(source!)) {
this.editorValues.title = note.title;
this.editorValues.text = note.text;
}
if (!this.editorValues.title) {
this.editorValues.title = note.title;
}
if (!this.editorValues.text) {
this.editorValues.text = note.text;
}
const isTemplateNoteInsertedToBeInteractableWithEditor =
source === PayloadSource.Constructor && note.dirty;
if (isTemplateNoteInsertedToBeInteractableWithEditor) {
return;
}
if (note.lastSyncBegan || note.dirty) {
if (note.lastSyncEnd) {
if (
note.dirty ||
note.lastSyncBegan!.getTime() > note.lastSyncEnd!.getTime()
) {
this.showSavingStatus();
} else if (
note.lastSyncEnd!.getTime() > note.lastSyncBegan!.getTime()
) {
this.showAllChangesSavedStatus();
}
} else {
this.showSavingStatus();
}
}
}); });
this.autorun(() => { this.autorun(() => {
this.setState({ this.setState({
showProtectedWarning: this.appState.notes.showProtectedWarning, showProtectedWarning: this.appState.notes.showProtectedWarning,
}); });
}); });
this.reloadEditorComponent();
this.reloadStackComponents();
const showProtectedWarning =
this.note.protected && !this.application.hasProtectionSources();
this.setShowProtectedOverlay(showProtectedWarning);
this.reloadPreferences();
if (this.note.dirty) {
this.showSavingStatus();
}
}
private onNoteChanges(note: SNNote, source: PayloadSource): void {
if (note.uuid !== this.note.uuid) {
throw Error('Editor received changes for non-current note');
}
if (isPayloadSourceRetrieved(source)) {
this.editorValues.title = note.title;
this.editorValues.text = note.text;
}
if (!this.editorValues.title) {
this.editorValues.title = note.title;
}
if (!this.editorValues.text) {
this.editorValues.text = note.text;
}
const isTemplateNoteInsertedToBeInteractableWithEditor =
source === PayloadSource.Constructor && note.dirty;
if (isTemplateNoteInsertedToBeInteractableWithEditor) {
return;
}
if (note.lastSyncBegan || note.dirty) {
if (note.lastSyncEnd) {
if (
note.dirty ||
note.lastSyncBegan!.getTime() > note.lastSyncEnd!.getTime()
) {
this.showSavingStatus();
} else if (
note.lastSyncEnd!.getTime() > note.lastSyncBegan!.getTime()
) {
this.showAllChangesSavedStatus();
}
} else {
this.showSavingStatus();
}
}
}
$onDestroy(): void {
if (this.state.editorComponentViewer) {
this.application.componentManager?.destroyComponentViewer(
this.state.editorComponentViewer
);
}
super.$onDestroy();
} }
/** @override */ /** @override */
getInitialState() { getInitialState() {
return { return {
stackComponents: [], availableStackComponents: [],
stackComponentViewers: [],
editorStateDidLoad: false,
editorDebounce: EDITOR_DEBOUNCE, editorDebounce: EDITOR_DEBOUNCE,
isDesktop: isDesktopApplication(), isDesktop: isDesktopApplication(),
spellcheck: true, spellcheck: true,
@@ -219,7 +247,6 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
showEditorMenu: false, showEditorMenu: false,
showHistoryMenu: false, showHistoryMenu: false,
noteStatus: undefined, noteStatus: undefined,
editorUnloading: false,
textareaUnloading: false, textareaUnloading: false,
showProtectedWarning: false, showProtectedWarning: false,
} as EditorState; } as EditorState;
@@ -228,7 +255,7 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
async onAppLaunch() { async onAppLaunch() {
await super.onAppLaunch(); await super.onAppLaunch();
this.streamItems(); this.streamItems();
this.registerComponentHandler(); this.registerComponentManagerEventObserver();
} }
/** @override */ /** @override */
@@ -310,34 +337,6 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
} }
} }
async handleEditorNoteChange() {
this.clearNoteProtectionInactivityTimer();
this.cancelPendingSetStatus();
const note = this.editor.note;
const showProtectedWarning =
note.protected &&
(!this.application.hasProtectionSources() ||
this.application.getProtectionSessionExpiryDate().getTime() <
Date.now());
this.setShowProtectedOverlay(showProtectedWarning);
await this.setState({
showActionsMenu: false,
showEditorMenu: false,
showHistoryMenu: false,
noteStatus: undefined,
});
this.editorValues.title = note.title;
this.editorValues.text = note.text;
this.reloadEditor();
this.reloadPreferences();
this.reloadStackComponents();
if (note.dirty) {
this.showSavingStatus();
}
}
async dismissProtectedWarning() { async dismissProtectedWarning() {
let showNoteContents = true; let showNoteContents = true;
if (this.application.hasProtectionSources()) { if (this.application.hasProtectionSources()) {
@@ -366,20 +365,45 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
} }
streamItems() { streamItems() {
this.removeComponentsObserver = this.application.streamItems( this.removeComponentStreamObserver = this.application.streamItems(
ContentType.Component, ContentType.Component,
async (_items, source) => { async (_items, source) => {
if (isPayloadSourceInternalChange(source!)) { if (
isPayloadSourceInternalChange(source) ||
source === PayloadSource.InitialObserverRegistrationPush
) {
return; return;
} }
if (!this.note) return; if (!this.note) return;
this.reloadStackComponents(); await this.reloadStackComponents();
this.reloadEditor(); await this.reloadEditorComponent();
} }
); );
} }
private async reloadEditor() { private createComponentViewer(component: SNComponent) {
const viewer = this.application.componentManager.createComponentViewer(
component,
this.note.uuid
);
return viewer;
}
public async editorComponentViewerRequestsReload(
viewer: ComponentViewer
): Promise<void> {
const component = viewer.component;
this.application.componentManager.destroyComponentViewer(viewer);
await this.setState({
editorComponentViewer: undefined,
});
await this.setState({
editorComponentViewer: this.createComponentViewer(component),
editorStateDidLoad: true,
});
}
private async reloadEditorComponent() {
const newEditor = this.application.componentManager.editorForNote( const newEditor = this.application.componentManager.editorForNote(
this.note this.note
); );
@@ -387,22 +411,29 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
if (newEditor && this.editor.isTemplateNote) { if (newEditor && this.editor.isTemplateNote) {
await this.editor.insertTemplatedNote(); await this.editor.insertTemplatedNote();
} }
const currentEditor = this.state.editorComponent; const currentComponentViewer = this.state.editorComponentViewer;
if (currentEditor?.uuid !== newEditor?.uuid) {
if (currentComponentViewer?.componentUuid !== newEditor?.uuid) {
if (currentComponentViewer) {
this.application.componentManager.destroyComponentViewer(
currentComponentViewer
);
}
await this.setState({ await this.setState({
/** Unload current component view so that we create a new one */ editorComponentViewer: undefined,
editorUnloading: true,
});
await this.setState({
/** Reload component view */
editorComponent: newEditor,
editorUnloading: false,
}); });
if (newEditor) {
await this.setState({
editorComponentViewer: this.createComponentViewer(newEditor),
editorStateDidLoad: true,
});
}
this.reloadFont(); this.reloadFont();
} else {
await this.setState({
editorStateDidLoad: true,
});
} }
this.application.componentManager.contextItemDidChangeInArea(
ComponentArea.Editor
);
} }
setMenuState(menu: string, state: boolean) { setMenuState(menu: string, state: boolean) {
@@ -429,6 +460,8 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
} }
async editorMenuOnSelect(component?: SNComponent) { async editorMenuOnSelect(component?: SNComponent) {
const transactions: TransactionalMutation[] = [];
this.setMenuState('showEditorMenu', false); this.setMenuState('showEditorMenu', false);
if (this.appState.getActiveEditor()?.isTemplateNote) { if (this.appState.getActiveEditor()?.isTemplateNote) {
await this.appState.getActiveEditor().insertTemplatedNote(); await this.appState.getActiveEditor().insertTemplatedNote();
@@ -439,43 +472,56 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
} }
if (!component) { if (!component) {
if (!this.note.prefersPlainEditor) { if (!this.note.prefersPlainEditor) {
await this.application.changeItem(this.note.uuid, (mutator) => { transactions.push({
const noteMutator = mutator as NoteMutator; itemUuid: this.note.uuid,
noteMutator.prefersPlainEditor = true; mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator;
noteMutator.prefersPlainEditor = true;
},
}); });
this.reloadEditor();
} }
if ( if (
this.state.editorComponent?.isExplicitlyEnabledForItem(this.note.uuid) this.state.editorComponentViewer?.component.isExplicitlyEnabledForItem(
this.note.uuid
)
) { ) {
await this.disassociateComponentWithCurrentNote( transactions.push(
this.state.editorComponent this.transactionForDisassociateComponentWithCurrentNote(
this.state.editorComponentViewer.component
)
); );
} }
this.reloadFont(); this.reloadFont();
} else if (component.area === ComponentArea.Editor) { } else if (component.area === ComponentArea.Editor) {
const currentEditor = this.state.editorComponent; const currentEditor = this.state.editorComponentViewer?.component;
if (currentEditor && component.uuid !== currentEditor.uuid) { if (currentEditor && component.uuid !== currentEditor.uuid) {
await this.disassociateComponentWithCurrentNote(currentEditor); transactions.push(
this.transactionForDisassociateComponentWithCurrentNote(currentEditor)
);
} }
const prefersPlain = this.note.prefersPlainEditor; const prefersPlain = this.note.prefersPlainEditor;
if (prefersPlain) { if (prefersPlain) {
await this.application.changeItem(this.note.uuid, (mutator) => { transactions.push({
const noteMutator = mutator as NoteMutator; itemUuid: this.note.uuid,
noteMutator.prefersPlainEditor = false; mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator;
noteMutator.prefersPlainEditor = false;
},
}); });
} }
await this.associateComponentWithCurrentNote(component); transactions.push(
} else if (component.area === ComponentArea.EditorStack) { this.transactionForAssociateComponentWithCurrentNote(component)
await this.toggleStackComponentForCurrentItem(component); );
} }
await this.application.runTransactionalMutations(transactions);
/** Dirtying can happen above */ /** Dirtying can happen above */
this.application.sync(); this.application.sync();
} }
hasAvailableExtensions() { hasAvailableExtensions() {
return ( return (
this.application.actionsManager!.extensionsInContextOfItem(this.note) this.application.actionsManager.extensionsInContextOfItem(this.note)
.length > 0 .length > 0
); );
} }
@@ -534,7 +580,9 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
const truncate = noteText.length > NOTE_PREVIEW_CHAR_LIMIT; const truncate = noteText.length > NOTE_PREVIEW_CHAR_LIMIT;
const substring = noteText.substring(0, NOTE_PREVIEW_CHAR_LIMIT); const substring = noteText.substring(0, NOTE_PREVIEW_CHAR_LIMIT);
const previewPlain = substring + (truncate ? STRING_ELLIPSES : ''); const previewPlain = substring + (truncate ? STRING_ELLIPSES : '');
// eslint-disable-next-line camelcase
noteMutator.preview_plain = previewPlain; noteMutator.preview_plain = previewPlain;
// eslint-disable-next-line camelcase
noteMutator.preview_html = undefined; noteMutator.preview_html = undefined;
} }
}, },
@@ -643,12 +691,6 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
this.closeAllMenus(); this.closeAllMenus();
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTitleFocus() {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onTitleBlur() {}
onContentFocus() { onContentFocus() {
this.application this.application
.getAppState() .getAppState()
@@ -662,11 +704,11 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
async deleteNote(permanently: boolean) { async deleteNote(permanently: boolean) {
if (this.editor.isTemplateNote) { if (this.editor.isTemplateNote) {
this.application.alertService!.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT); this.application.alertService.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT);
return; return;
} }
if (this.note.locked) { if (this.note.locked) {
this.application.alertService!.alert(STRING_DELETE_LOCKED_ATTEMPT); this.application.alertService.alert(STRING_DELETE_LOCKED_ATTEMPT);
return; return;
} }
const title = this.note.safeTitle().length const title = this.note.safeTitle().length
@@ -782,25 +824,15 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
/** @components */ /** @components */
registerComponentHandler() { registerComponentManagerEventObserver() {
this.unregisterComponent = this.removeComponentManagerObserver =
this.application.componentManager.registerHandler({ this.application.componentManager.addEventObserver((eventName, data) => {
identifier: 'editor', if (eventName === ComponentManagerEvent.ViewerDidFocus) {
areas: [ComponentArea.EditorStack, ComponentArea.Editor], const viewer = data?.componentViewer;
contextRequestHandler: (componentUuid) => { if (viewer?.component.isEditor) {
const currentEditor = this.state.editorComponent;
if (
componentUuid === currentEditor?.uuid ||
Uuids(this.state.stackComponents).includes(componentUuid)
) {
return this.note;
}
},
focusHandler: (component, focused) => {
if (component.isEditor() && focused) {
this.closeAllMenus(); this.closeAllMenus();
} }
}, }
}); });
} }
@@ -810,58 +842,98 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
.componentsForArea(ComponentArea.EditorStack) .componentsForArea(ComponentArea.EditorStack)
.filter((component) => component.active) .filter((component) => component.active)
); );
if (this.note) { const enabledComponents = stackComponents.filter((component) => {
for (const component of stackComponents) { return component.isExplicitlyEnabledForItem(this.note.uuid);
if (component.active) { });
this.application.componentManager.setComponentHidden(
component, const needsNewViewer = enabledComponents.filter((component) => {
!component.isExplicitlyEnabledForItem(this.note.uuid) const hasExistingViewer = this.state.stackComponentViewers.find(
); (viewer) => viewer.componentUuid === component.uuid
} );
return !hasExistingViewer;
});
const needsDestroyViewer = this.state.stackComponentViewers.filter(
(viewer) => {
const viewerComponentExistsInEnabledComponents = enabledComponents.find(
(component) => {
return component.uuid === viewer.componentUuid;
}
);
return !viewerComponentExistsInEnabledComponents;
} }
);
const newViewers: ComponentViewer[] = [];
for (const component of needsNewViewer) {
newViewers.push(
this.application.componentManager.createComponentViewer(
component,
this.note.uuid
)
);
} }
await this.setState({ stackComponents });
this.application.componentManager.contextItemDidChangeInArea( for (const viewer of needsDestroyViewer) {
ComponentArea.EditorStack this.application.componentManager.destroyComponentViewer(viewer);
}
await this.setState({
availableStackComponents: stackComponents,
stackComponentViewers: newViewers,
});
}
stackComponentExpanded(component: SNComponent): boolean {
return !!this.state.stackComponentViewers.find(
(viewer) => viewer.componentUuid === component.uuid
); );
} }
stackComponentHidden(component: SNComponent) { async toggleStackComponent(component: SNComponent) {
return this.application.componentManager?.isComponentHidden(component); if (!component.isExplicitlyEnabledForItem(this.note.uuid)) {
}
async toggleStackComponentForCurrentItem(component: SNComponent) {
const hidden =
this.application.componentManager.isComponentHidden(component);
if (hidden || !component.active) {
this.application.componentManager.setComponentHidden(component, false);
await this.associateComponentWithCurrentNote(component); await this.associateComponentWithCurrentNote(component);
this.application.componentManager.contextItemDidChangeInArea(
ComponentArea.EditorStack
);
} else { } else {
this.application.componentManager.setComponentHidden(component, true);
await this.disassociateComponentWithCurrentNote(component); await this.disassociateComponentWithCurrentNote(component);
} }
this.application.sync(); this.application.sync();
} }
async disassociateComponentWithCurrentNote(component: SNComponent) { async disassociateComponentWithCurrentNote(component: SNComponent) {
return this.application.runTransactionalMutation(
this.transactionForDisassociateComponentWithCurrentNote(component)
);
}
transactionForDisassociateComponentWithCurrentNote(component: SNComponent) {
const note = this.note; const note = this.note;
return this.application.changeItem(component.uuid, (m) => { const transaction: TransactionalMutation = {
const mutator = m as ComponentMutator; itemUuid: component.uuid,
mutator.removeAssociatedItemId(note.uuid); mutate: (m: ItemMutator) => {
mutator.disassociateWithItem(note.uuid); const mutator = m as ComponentMutator;
}); mutator.removeAssociatedItemId(note.uuid);
mutator.disassociateWithItem(note.uuid);
},
};
return transaction;
} }
async associateComponentWithCurrentNote(component: SNComponent) { async associateComponentWithCurrentNote(component: SNComponent) {
return this.application.runTransactionalMutation(
this.transactionForAssociateComponentWithCurrentNote(component)
);
}
transactionForAssociateComponentWithCurrentNote(component: SNComponent) {
const note = this.note; const note = this.note;
return this.application.changeItem(component.uuid, (m) => { const transaction: TransactionalMutation = {
const mutator = m as ComponentMutator; itemUuid: component.uuid,
mutator.removeDisassociatedItemId(note.uuid); mutate: (m: ItemMutator) => {
mutator.associateWithItem(note.uuid); const mutator = m as ComponentMutator;
}); mutator.removeDisassociatedItemId(note.uuid);
mutator.associateWithItem(note.uuid);
},
};
return transaction;
} }
registerKeyboardShortcuts() { registerKeyboardShortcuts() {

View File

@@ -6,7 +6,6 @@ import {
ApplicationEvent, ApplicationEvent,
ContentType, ContentType,
SNTheme, SNTheme,
ComponentArea,
CollectionSort, CollectionSort,
} from '@standardnotes/snjs'; } from '@standardnotes/snjs';
import template from './footer-view.pug'; import template from './footer-view.pug';
@@ -43,7 +42,6 @@ class FooterViewCtrl extends PureViewCtrl<
> { > {
private $rootScope: ng.IRootScopeService; private $rootScope: ng.IRootScopeService;
private showSyncResolution = false; private showSyncResolution = false;
private unregisterComponent: any;
private rootScopeListener2: any; private rootScopeListener2: any;
public arbitraryStatusMessage?: string; public arbitraryStatusMessage?: string;
public user?: any; public user?: any;
@@ -73,8 +71,6 @@ class FooterViewCtrl extends PureViewCtrl<
deinit() { deinit() {
for (const remove of this.observerRemovers) remove(); for (const remove of this.observerRemovers) remove();
this.observerRemovers.length = 0; this.observerRemovers.length = 0;
this.unregisterComponent();
this.unregisterComponent = undefined;
this.rootScopeListener2(); this.rootScopeListener2();
this.rootScopeListener2 = undefined; this.rootScopeListener2 = undefined;
(this.closeAccountMenu as unknown) = undefined; (this.closeAccountMenu as unknown) = undefined;
@@ -146,7 +142,6 @@ class FooterViewCtrl extends PureViewCtrl<
this.updateOfflineStatus(); this.updateOfflineStatus();
this.findErrors(); this.findErrors();
this.streamItems(); this.streamItems();
this.registerComponentHandler();
} }
reloadUser() { reloadUser() {
@@ -273,25 +268,6 @@ class FooterViewCtrl extends PureViewCtrl<
); );
} }
registerComponentHandler() {
this.unregisterComponent =
this.application.componentManager.registerHandler({
identifier: 'room-bar',
areas: [ComponentArea.Modal],
focusHandler: (component, focused) => {
if (component.isEditor() && focused) {
if (
component.package_info?.identifier ===
'org.standardnotes.standard-sheets'
) {
return;
}
this.closeAccountMenu();
}
},
});
}
updateSyncStatus() { updateSyncStatus() {
const statusManager = this.application.getStatusManager(); const statusManager = this.application.getStatusManager();
const syncStatus = this.application.getSyncStatus(); const syncStatus = this.application.getSyncStatus();

View File

@@ -1,11 +1,11 @@
#tags-column.sn-component.section.tags(aria-label='Tags') #tags-column.sn-component.section.tags(aria-label='Tags')
.component-view-container(ng-if='self.component') .component-view-container(ng-if='self.state.componentViewer')
component-view.component-view( component-view.component-view(
component-uuid='self.component.uuid', component-viewer='self.state.componentViewer',
application='self.application' application='self.application'
app-state='self.appState' app-state='self.appState'
) )
#tags-content.content(ng-if='!(self.component)') #tags-content.content(ng-if='!(self.state.componentViewer)')
.tags-title-section.section-title-bar .tags-title-section.section-title-bar
.section-title-bar-header .section-title-bar-header
.sk-h3.title .sk-h3.title

View File

@@ -6,7 +6,11 @@ import {
ApplicationEvent, ApplicationEvent,
ComponentAction, ComponentAction,
ComponentArea, ComponentArea,
ComponentViewer,
ContentType, ContentType,
isPayloadSourceInternalChange,
MessageData,
PayloadSource,
PrefKey, PrefKey,
SNComponent, SNComponent,
SNSmartTag, SNSmartTag,
@@ -22,14 +26,14 @@ type TagState = {
smartTags: SNSmartTag[]; smartTags: SNSmartTag[];
noteCounts: NoteCounts; noteCounts: NoteCounts;
selectedTag?: SNTag; selectedTag?: SNTag;
componentViewer?: ComponentViewer;
}; };
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> { class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
/** Passed through template */ /** Passed through template */
readonly application!: WebApplication; readonly application!: WebApplication;
private readonly panelPuppet: PanelPuppet; private readonly panelPuppet: PanelPuppet;
private unregisterComponent?: any; private unregisterComponent?: () => void;
component?: SNComponent;
/** The original name of the edtingTag before it began editing */ /** The original name of the edtingTag before it began editing */
formData: { tagTitle?: string } = {}; formData: { tagTitle?: string } = {};
titles: Partial<Record<UuidString, string>> = {}; titles: Partial<Record<UuidString, string>> = {};
@@ -46,9 +50,9 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
deinit() { deinit() {
this.removeTagsObserver?.(); this.removeTagsObserver?.();
(this.removeTagsObserver as any) = undefined; (this.removeTagsObserver as unknown) = undefined;
(this.removeFoldersObserver as any) = undefined; (this.removeFoldersObserver as unknown) = undefined;
this.unregisterComponent(); this.unregisterComponent?.();
this.unregisterComponent = undefined; this.unregisterComponent = undefined;
super.deinit(); super.deinit();
} }
@@ -64,15 +68,10 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
return this.state; return this.state;
} }
async onAppStart() {
super.onAppStart();
this.registerComponentHandler();
}
async onAppLaunch() { async onAppLaunch() {
super.onAppLaunch(); super.onAppLaunch();
this.loadPreferences(); this.loadPreferences();
this.beginStreamingItems(); this.streamForFoldersComponent();
const smartTags = this.application.getSmartTags(); const smartTags = this.application.getSmartTags();
this.setState({ smartTags }); this.setState({ smartTags });
@@ -85,13 +84,78 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
this.reloadNoteCounts(); this.reloadNoteCounts();
} }
beginStreamingItems() { async setFoldersComponent(component?: SNComponent) {
if (this.state.componentViewer) {
this.application.componentManager.destroyComponentViewer(
this.state.componentViewer
);
await this.setState({ componentViewer: undefined });
}
if (component) {
await this.setState({
componentViewer:
this.application.componentManager.createComponentViewer(
component,
undefined,
this.handleFoldersComponentMessage.bind(this)
),
});
}
}
handleFoldersComponentMessage(
action: ComponentAction,
data: MessageData
): void {
if (action === ComponentAction.SelectItem) {
const item = data.item;
if (!item) {
return;
}
if (item.content_type === ContentType.Tag) {
const matchingTag = this.application.findItem(item.uuid);
if (matchingTag) {
this.selectTag(matchingTag as SNTag);
}
} else 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]);
}
}
streamForFoldersComponent() {
this.removeFoldersObserver = this.application.streamItems( this.removeFoldersObserver = this.application.streamItems(
[ContentType.Component], [ContentType.Component],
async () => { async (items, source) => {
this.component = this.application.componentManager if (
.componentsForArea(ComponentArea.TagsList).find((component) => component.active); isPayloadSourceInternalChange(source) ||
}); source === PayloadSource.InitialObserverRegistrationPush
) {
return;
}
const components = items as SNComponent[];
const hasFoldersChange = !!components.find(
(component) => component.area === ComponentArea.TagsList
);
if (hasFoldersChange) {
this.setFoldersComponent(
this.application.componentManager
.componentsForArea(ComponentArea.TagsList)
.find((component) => component.active)
);
}
}
);
this.removeTagsObserver = this.application.streamItems( this.removeTagsObserver = this.application.streamItems(
[ContentType.Tag, ContentType.SmartTag], [ContentType.Tag, ContentType.SmartTag],
@@ -200,41 +264,6 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
this.application.getAppState().panelDidResize(PANEL_NAME_TAGS, isCollapsed); 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) {
const item = data.item;
if (!item) {
return;
}
if (item.content_type === ContentType.Tag) {
const matchingTag = this.application.findItem(item.uuid);
if (matchingTag) {
this.selectTag(matchingTag as SNTag);
}
} else 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]);
}
},
});
}
async selectTag(tag: SNTag) { async selectTag(tag: SNTag) {
if (tag.conflictOf) { if (tag.conflictOf) {
this.application.changeAndSaveItem(tag.uuid, (mutator) => { this.application.changeAndSaveItem(tag.uuid, (mutator) => {

View File

@@ -1,18 +0,0 @@
.sk-modal-background(ng-click="ctrl.dismiss()")
.sk-modal-content(
ng-attr-id="component-content-outer-{{ctrl.component.uuid}}"
)
.sn-component
.sk-panel(
ng-attr-id="component-content-inner-{{ctrl.component.uuid}}"
)
.sk-panel-header
.sk-panel-header-title
| {{ctrl.component.name}}
a.sk-a.info.close-button(ng-click="ctrl.dismiss()") Close
component-view.component-view(
ng-if='ctrl.component.active'
component-uuid="ctrl.component.uuid",
application='ctrl.application'
app-state='self.appState'
)

View File

@@ -3,7 +3,7 @@
.sn-component .sn-component
.sk-panel .sk-panel
.sk-panel-header .sk-panel-header
.sk-panel-header-title Activate Extension .sk-panel-header-title Activate Component
a.sk-a.info.close-button(ng-click='ctrl.deny()') Cancel a.sk-a.info.close-button(ng-click='ctrl.deny()') Cancel
.sk-panel-content .sk-panel-content
.sk-panel-section .sk-panel-section
@@ -14,8 +14,8 @@
| {{ctrl.permissionsString}} | {{ctrl.permissionsString}}
.sk-panel-row .sk-panel-row
p.sk-p p.sk-p
| Extensions use an offline messaging system to communicate. Learn more at | Components use an offline messaging system to communicate. Learn more at
| |
a.sk-a.info( a.sk-a.info(
href='https://standardnotes.com/permissions', href='https://standardnotes.com/permissions',
rel='noopener', rel='noopener',

View File

@@ -9,7 +9,7 @@
.sk-panel-header-title Preview .sk-panel-header-title Preview
.sk-subtitle.neutral.mt-1( .sk-subtitle.neutral.mt-1(
ng-if="ctrl.title" ng-if="ctrl.title"
) {{ctrl.title}} ) {{ctrl.title}}
.sk-horizontal-group .sk-horizontal-group
a.sk-a.info.close-button( a.sk-a.info.close-button(
ng-click="ctrl.restore(false)" ng-click="ctrl.restore(false)"
@@ -20,18 +20,18 @@
a.sk-a.info.close-button( a.sk-a.info.close-button(
ng-click="ctrl.dismiss(); $event.stopPropagation()" ng-click="ctrl.dismiss(); $event.stopPropagation()"
) Close ) Close
.sk-panel-content.selectable(ng-if="!ctrl.state.editor") .sk-panel-content.selectable(ng-if="!ctrl.state.componentViewer")
.sk-h2 {{ctrl.content.title}} .sk-h2 {{ctrl.content.title}}
p.normal.sk-p( p.normal.sk-p(
style="white-space: pre-wrap; font-size: 16px;" style="white-space: pre-wrap; font-size: 16px;"
) {{ctrl.content.text}} ) {{ctrl.content.text}}
.sk-panel-content.sk-h2( .sk-panel-content.sk-h2(
ng-if="ctrl.state.editor" ng-if="ctrl.state.componentViewer"
style="height: auto; flex-grow: 0" style="height: auto; flex-grow: 0"
) {{ctrl.content.title}} ) {{ctrl.content.title}}
component-view.component-view( component-view.component-view(
ng-if="ctrl.state.editor", ng-if="ctrl.state.componentViewer",
template-component="ctrl.state.editor", component-viewer="ctrl.state.componentViewer",
application='ctrl.application' application='ctrl.application'
app-state='self.appState' app-state='self.appState'
) )

View File

@@ -87,7 +87,7 @@
"@standardnotes/features": "1.10.2", "@standardnotes/features": "1.10.2",
"@reach/tooltip": "^0.16.2", "@reach/tooltip": "^0.16.2",
"@standardnotes/sncrypto-web": "1.5.3", "@standardnotes/sncrypto-web": "1.5.3",
"@standardnotes/snjs": "2.25.0", "@standardnotes/snjs": "2.29.0",
"mobx": "^6.3.5", "mobx": "^6.3.5",
"mobx-react-lite": "^3.2.2", "mobx-react-lite": "^3.2.2",
"preact": "^10.5.15", "preact": "^10.5.15",

View File

@@ -0,0 +1,10 @@
{
"identifier": "org.standardnotes.markdown-basic-local",
"name": "Markdown Basic - Local",
"content_type": "SN|Component",
"area": "editor-editor",
"version": "1.0.0",
"description": "Write Markdown in private",
"url": "http://localhost:8004/dist/index.html",
"thumbnail_url": "http://localhost:8004/dist/favicon.png"
}

View File

@@ -2597,6 +2597,13 @@
dependencies: dependencies:
"@sinonjs/commons" "^1.7.0" "@sinonjs/commons" "^1.7.0"
"@standardnotes/auth@3.8.1":
version "3.8.1"
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.8.1.tgz#4197fb2f7e223c6bd13a870a3feac3c73294fb3c"
integrity sha512-Q2/81dgFGIGuYlQ4VnSjGRsDB0Qw0tQP/qsiuV+DQj+wdp5Wy5WX3Q4g+p2PNvoyEAYgbuduEHZfWuTLAaIdyw==
dependencies:
"@standardnotes/common" "^1.2.1"
"@standardnotes/auth@3.8.3", "@standardnotes/auth@^3.8.1": "@standardnotes/auth@3.8.3", "@standardnotes/auth@^3.8.1":
version "3.8.3" version "3.8.3"
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.8.3.tgz#6e627c1a1a9ebf91d97f52950d099bf7704382e3" resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.8.3.tgz#6e627c1a1a9ebf91d97f52950d099bf7704382e3"
@@ -2604,15 +2611,15 @@
dependencies: dependencies:
"@standardnotes/common" "^1.2.1" "@standardnotes/common" "^1.2.1"
"@standardnotes/common@^1.2.1": "@standardnotes/common@1.2.1", "@standardnotes/common@^1.2.1":
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.2.1.tgz#9db212db86ccbf08b347da02549b3dbe4bedbb02" resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.2.1.tgz#9db212db86ccbf08b347da02549b3dbe4bedbb02"
integrity sha512-HilBxS50CBlC6TJvy1mrnhGVDzOH63M/Jf+hyMxQ0Vt1nYzpd0iyxVEUrgMh7ZiyO1b9CLnCDED99Jy9rnZWVQ== integrity sha512-HilBxS50CBlC6TJvy1mrnhGVDzOH63M/Jf+hyMxQ0Vt1nYzpd0iyxVEUrgMh7ZiyO1b9CLnCDED99Jy9rnZWVQ==
"@standardnotes/domain-events@^2.5.1": "@standardnotes/domain-events@2.5.1":
version "2.10.0" version "2.5.1"
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.10.0.tgz#719c430d1736daffcb4233aa3381b58280564dc0" resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.5.1.tgz#e6433e940ae616683d1c24f76133c70755504c44"
integrity sha512-8jvkhNjoYrXN81RA8Q4vGEKH9R002Y/aEK29GyxmQmijT5+JwlA4f0ySycz5sJxWGULohL1k96RueYPs97hV3g== integrity sha512-p0VB4Al/ZcVqcj9ztU7TNqzc3jjjG6/U7x9lBW/QURHxpB+PnwJq3kFU5V5JA9QpCOYlXLT71CMERMf/O5QX6g==
dependencies: dependencies:
"@standardnotes/auth" "^3.8.1" "@standardnotes/auth" "^3.8.1"
@@ -2624,20 +2631,20 @@
"@standardnotes/auth" "3.8.3" "@standardnotes/auth" "3.8.3"
"@standardnotes/common" "^1.2.1" "@standardnotes/common" "^1.2.1"
"@standardnotes/features@^1.10.3": "@standardnotes/features@1.11.0":
version "1.10.3" version "1.11.0"
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.10.3.tgz#f5824342446e69f006ea8ac8916203d1d3992f21" resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.11.0.tgz#66e960a20358c5f58b6be4e19226b34df6f4efbf"
integrity sha512-PU4KthoDr6NL1bOfKnYV1WXYqRu1/IcdkZkJa2LHcYMPduUjDUKO6qRK73dF0+EEI1U+YXY/9rHyfadGwd0Ymg== integrity sha512-KMP60C1lf5C141s5VVOs7mISS1IUCioJfYsbsxtPycx9Q1mgbDB3xJv/GuCK/avsQOiGrB7QN06CQJ7fw4XV3Q==
dependencies: dependencies:
"@standardnotes/auth" "3.8.3" "@standardnotes/auth" "3.8.3"
"@standardnotes/common" "^1.2.1" "@standardnotes/common" "^1.2.1"
"@standardnotes/settings@^1.2.1": "@standardnotes/settings@^1.8.0":
version "1.4.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.4.0.tgz#b8f43383fa1b469d609f5ac6c2ce571efb8bfe71" resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.8.0.tgz#d7bd1f35c3b500d12ba73f5f385b1019baae3efc"
integrity sha512-mGptrIaM/3UWOkc9xmzuRRM2A75caX6vyqCeKhyqPdM3ZR/YpYH7I6qYDsO6wpkoF3soD2nRJ6pLV7HBjGdGag== integrity sha512-gszghenDHFHoEeIGW7fHtIOJw35WpuaOdTD6UNMrH71xEduAr0JtzaLwrFXJ7XIp62zY7CSY1V6Npxo6HTGn+w==
"@standardnotes/sncrypto-common@^1.5.2": "@standardnotes/sncrypto-common@1.5.2", "@standardnotes/sncrypto-common@^1.5.2":
version "1.5.2" version "1.5.2"
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.5.2.tgz#be9404689d94f953c68302609a4f76751eaa82cd" resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.5.2.tgz#be9404689d94f953c68302609a4f76751eaa82cd"
integrity sha512-+OQ6gajTcVSHruw33T52MHyBDKL1vRCfQBXQn4tt4+bCfBAe+PFLkEQMHp35bg5twCfg9+wUf2KhmNNSNyBBZw== integrity sha512-+OQ6gajTcVSHruw33T52MHyBDKL1vRCfQBXQn4tt4+bCfBAe+PFLkEQMHp35bg5twCfg9+wUf2KhmNNSNyBBZw==
@@ -2651,17 +2658,17 @@
buffer "^6.0.3" buffer "^6.0.3"
libsodium-wrappers "^0.7.9" libsodium-wrappers "^0.7.9"
"@standardnotes/snjs@2.25.0": "@standardnotes/snjs@2.29.0":
version "2.25.0" version "2.29.0"
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.25.0.tgz#742ee451547c1f36d29bb3e58678068d6a455520" resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.29.0.tgz#6c7c6ccd983df4a1a5e2063647eb731304002fd9"
integrity sha512-Sb2sZItuxWAFepbNyqGvH8CIC436VirEjAqc0NK9+1CK0wqPLfpCiDBEkTzsQa2UovnoKu/p4tpTrMnx3FvK2A== integrity sha512-Y+GpNiFyJtVr2W3nVbC2zljtXpBlqe3cB4+R1REE0V4hnQBaq/HE6PaUd80TnFj99Kl8lowyH/o4bNV3+CjGgg==
dependencies: dependencies:
"@standardnotes/auth" "^3.8.1" "@standardnotes/auth" "3.8.1"
"@standardnotes/common" "^1.2.1" "@standardnotes/common" "1.2.1"
"@standardnotes/domain-events" "^2.5.1" "@standardnotes/domain-events" "2.5.1"
"@standardnotes/features" "^1.10.3" "@standardnotes/features" "1.11.0"
"@standardnotes/settings" "^1.2.1" "@standardnotes/settings" "^1.8.0"
"@standardnotes/sncrypto-common" "^1.5.2" "@standardnotes/sncrypto-common" "1.5.2"
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0": "@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
version "5.4.0" version "5.4.0"