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:
@@ -51,7 +51,6 @@ import {
|
||||
|
||||
import {
|
||||
ActionsMenu,
|
||||
ComponentModal,
|
||||
EditorMenu,
|
||||
InputModal,
|
||||
MenuRow,
|
||||
@@ -166,7 +165,6 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('accountSwitcher', () => new AccountSwitcher())
|
||||
.directive('actionsMenu', () => new ActionsMenu())
|
||||
.directive('challengeModal', () => new ChallengeModal())
|
||||
.directive('componentModal', () => new ComponentModal())
|
||||
.directive('componentView', ComponentViewDirective)
|
||||
.directive('editorMenu', () => new EditorMenu())
|
||||
.directive('inputModal', () => new InputModal())
|
||||
|
||||
@@ -5,11 +5,14 @@ interface IProps {
|
||||
expiredDate: string;
|
||||
componentName: string;
|
||||
featureStatus: FeatureStatus;
|
||||
reloadStatus: () => void;
|
||||
manageSubscription: () => void;
|
||||
}
|
||||
|
||||
const statusString = (featureStatus: FeatureStatus, expiredDate: string, componentName: string) => {
|
||||
const statusString = (
|
||||
featureStatus: FeatureStatus,
|
||||
expiredDate: string,
|
||||
componentName: string
|
||||
) => {
|
||||
switch (featureStatus) {
|
||||
case FeatureStatus.InCurrentPlanButExpired:
|
||||
return `Your subscription expired on ${expiredDate}`;
|
||||
@@ -25,9 +28,8 @@ const statusString = (featureStatus: FeatureStatus, expiredDate: string, compone
|
||||
export const IsExpired: FunctionalComponent<IProps> = ({
|
||||
expiredDate,
|
||||
featureStatus,
|
||||
reloadStatus,
|
||||
componentName,
|
||||
manageSubscription
|
||||
manageSubscription,
|
||||
}) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
@@ -50,11 +52,13 @@ export const IsExpired: FunctionalComponent<IProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className={'right'}>
|
||||
<div className={'sk-app-bar-item'} onClick={() => manageSubscription()}>
|
||||
<button className={'sn-button small success'}>Manage Subscription</button>
|
||||
</div>
|
||||
<div className={'sk-app-bar-item'} onClick={() => reloadStatus()}>
|
||||
<button className={'sn-button small info'}>Reload</button>
|
||||
<div
|
||||
className={'sk-app-bar-item'}
|
||||
onClick={() => manageSubscription()}
|
||||
>
|
||||
<button className={'sn-button small success'}>
|
||||
Manage Subscription
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
interface IProps {
|
||||
isReloading: boolean;
|
||||
reloadStatus: () => void;
|
||||
}
|
||||
|
||||
export const OfflineRestricted: FunctionalComponent<IProps> = ({
|
||||
isReloading,
|
||||
reloadStatus,
|
||||
}) => {
|
||||
export const OfflineRestricted: FunctionalComponent = () => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-panel static'}>
|
||||
@@ -16,38 +8,29 @@ export const OfflineRestricted: FunctionalComponent<IProps> = ({
|
||||
<div className={'sk-panel-section stretch'}>
|
||||
<div className={'sk-panel-column'} />
|
||||
<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 className={'sk-subtitle'}>
|
||||
Offline components are not available in the web application.
|
||||
Locally-installed components are not available in the web
|
||||
application.
|
||||
</div>
|
||||
<div className={'sk-panel-row'} />
|
||||
<div className={'sk-panel-row'}>
|
||||
<div className={'sk-panel-column'}>
|
||||
<div className={'sk-p'}>You can either:</div>
|
||||
<div className={'sk-p'}>
|
||||
To continue, choose from the following options:
|
||||
</div>
|
||||
<ul>
|
||||
<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{' '}
|
||||
toggling 'Use hosted when local is unavailable' under this
|
||||
components's options. Then press Reload below.
|
||||
component's options. Then press Reload.
|
||||
</li>
|
||||
<li className={'sk-p'}>Use the desktop application.</li>
|
||||
</ul>
|
||||
</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>
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
FeatureStatus,
|
||||
SNComponent,
|
||||
dateToLocalizedString,
|
||||
ApplicationEvent,
|
||||
ComponentViewer,
|
||||
ComponentViewerEvent,
|
||||
ComponentViewerError,
|
||||
} from '@standardnotes/snjs';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
@@ -22,9 +24,9 @@ import { openSubscriptionDashboard } from '@/hooks/manageSubscription';
|
||||
interface IProps {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
componentUuid: string;
|
||||
componentViewer: ComponentViewer;
|
||||
requestReload?: (viewer: ComponentViewer) => void;
|
||||
onLoad?: (component: SNComponent) => void;
|
||||
templateComponent?: SNComponent;
|
||||
manualDealloc?: boolean;
|
||||
}
|
||||
|
||||
@@ -34,10 +36,10 @@ interface IProps {
|
||||
*/
|
||||
const MaxLoadThreshold = 4000;
|
||||
const VisibilityChangeKey = 'visibilitychange';
|
||||
const avoidFlickerTimeout = 7;
|
||||
const MSToWaitAfterIframeLoadToAvoidFlicker = 35;
|
||||
|
||||
export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
({ application, onLoad, componentUuid, templateComponent }) => {
|
||||
({ application, onLoad, componentViewer, requestReload }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const excessiveLoadingTimeout = useRef<
|
||||
ReturnType<typeof setTimeout> | undefined
|
||||
@@ -45,44 +47,33 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
|
||||
const [hasIssueLoading, setHasIssueLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isReloading, setIsReloading] = useState(false);
|
||||
const [component] = useState<SNComponent>(
|
||||
application.findItem(componentUuid) as SNComponent
|
||||
);
|
||||
const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(
|
||||
application.getFeatureStatus(component.identifier)
|
||||
componentViewer.getFeatureStatus()
|
||||
);
|
||||
const [isComponentValid, setIsComponentValid] = useState(true);
|
||||
const [error, setError] = useState<
|
||||
'offline-restricted' | 'url-missing' | undefined
|
||||
>(undefined);
|
||||
const [isDeprecated, setIsDeprecated] = useState(false);
|
||||
const [error, setError] = useState<ComponentViewerError | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [deprecationMessage, setDeprecationMessage] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] =
|
||||
useState(false);
|
||||
const [didAttemptReload, setDidAttemptReload] = useState(false);
|
||||
const [contentWindow, setContentWindow] = useState<Window | null>(null);
|
||||
|
||||
const component = componentViewer.component;
|
||||
|
||||
const manageSubscription = useCallback(() => {
|
||||
openSubscriptionDashboard(application);
|
||||
}, [application]);
|
||||
|
||||
const reloadIframe = () => {
|
||||
setTimeout(() => {
|
||||
setIsReloading(true);
|
||||
setTimeout(() => {
|
||||
setIsReloading(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadTimeout = setTimeout(() => {
|
||||
handleIframeTakingTooLongToLoad();
|
||||
}, MaxLoadThreshold);
|
||||
|
||||
excessiveLoadingTimeout.current = loadTimeout;
|
||||
|
||||
return () => {
|
||||
excessiveLoadingTimeout.current &&
|
||||
clearTimeout(excessiveLoadingTimeout.current);
|
||||
@@ -91,42 +82,25 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
}, []);
|
||||
|
||||
const reloadValidityStatus = useCallback(() => {
|
||||
const offlineRestricted =
|
||||
component.offlineOnly && !isDesktopApplication();
|
||||
const hasUrlError = (function () {
|
||||
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
|
||||
);
|
||||
setFeatureStatus(componentViewer.getFeatureStatus());
|
||||
if (!componentViewer.lockReadonly) {
|
||||
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled);
|
||||
}
|
||||
setIsComponentValid(!offlineRestricted && !hasUrlError);
|
||||
setIsComponentValid(componentViewer.shouldRender());
|
||||
|
||||
if (!isComponentValid) {
|
||||
if (isLoading && !isComponentValid) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
if (offlineRestricted) {
|
||||
setError('offline-restricted');
|
||||
} else if (hasUrlError) {
|
||||
setError('url-missing');
|
||||
} else {
|
||||
setError(undefined);
|
||||
}
|
||||
setIsDeprecated(component.isDeprecated);
|
||||
setDeprecationMessage(component.package_info.deprecation_message);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
setError(componentViewer.getError());
|
||||
setDeprecationMessage(component.deprecationMessage);
|
||||
}, [
|
||||
componentViewer,
|
||||
component.deprecationMessage,
|
||||
featureStatus,
|
||||
isComponentValid,
|
||||
isLoading,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadValidityStatus();
|
||||
@@ -141,9 +115,9 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
return;
|
||||
}
|
||||
if (hasIssueLoading) {
|
||||
reloadIframe();
|
||||
requestReload?.(componentViewer);
|
||||
}
|
||||
}, [hasIssueLoading]);
|
||||
}, [hasIssueLoading, componentViewer, requestReload]);
|
||||
|
||||
const handleIframeTakingTooLongToLoad = useCallback(async () => {
|
||||
setIsLoading(false);
|
||||
@@ -151,188 +125,133 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
|
||||
if (!didAttemptReload) {
|
||||
setDidAttemptReload(true);
|
||||
reloadIframe();
|
||||
requestReload?.(componentViewer);
|
||||
} else {
|
||||
document.addEventListener(VisibilityChangeKey, onVisibilityChange);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
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]);
|
||||
}, [componentViewer, didAttemptReload, onVisibilityChange, requestReload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!iframeRef.current) {
|
||||
setContentWindow(null);
|
||||
return;
|
||||
}
|
||||
|
||||
iframeRef.current.onload = () => {
|
||||
const iframe = application.componentManager.iframeForComponent(
|
||||
component.uuid
|
||||
);
|
||||
if (iframe) {
|
||||
setTimeout(() => {
|
||||
handleIframeLoad(iframe);
|
||||
});
|
||||
const iframe = iframeRef.current as HTMLIFrameElement;
|
||||
iframe.onload = () => {
|
||||
const contentWindow = iframe.contentWindow as Window;
|
||||
|
||||
let hasDesktopError = false;
|
||||
const canAccessWindowOrigin = isDesktopApplication();
|
||||
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
|
||||
}, [iframeRef.current]);
|
||||
}, [onLoad, component, componentViewer]);
|
||||
|
||||
useEffect(() => {
|
||||
const removeFeaturesChangedObserver = application.addEventObserver(
|
||||
async () => {
|
||||
setFeatureStatus(application.getFeatureStatus(component.identifier));
|
||||
},
|
||||
ApplicationEvent.FeaturesUpdated
|
||||
const removeFeaturesChangedObserver = componentViewer.addEventObserver(
|
||||
(event) => {
|
||||
if (event === ComponentViewerEvent.FeatureStatusUpdated) {
|
||||
setFeatureStatus(componentViewer.getFeatureStatus());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
removeFeaturesChangedObserver();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [componentViewer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!componentUuid) {
|
||||
application.componentManager.addTemporaryTemplateComponent(
|
||||
templateComponent as SNComponent
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (templateComponent) {
|
||||
/** componentManager can be destroyed already via locking */
|
||||
application.componentManager?.removeTemporaryTemplateComponent(
|
||||
templateComponent
|
||||
);
|
||||
const removeActionObserver = componentViewer.addActionObserver(
|
||||
(action, data) => {
|
||||
switch (action) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 () => {
|
||||
unregisterComponentHandler();
|
||||
removeActionObserver();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [component]);
|
||||
}, [componentViewer, application]);
|
||||
|
||||
useEffect(() => {
|
||||
const unregisterDesktopObserver = application
|
||||
.getDesktopService()
|
||||
.registerUpdateObserver((component: SNComponent) => {
|
||||
if (component.uuid === component.uuid && component.active) {
|
||||
reloadIframe();
|
||||
requestReload?.(componentViewer);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterDesktopObserver();
|
||||
};
|
||||
}, [application]);
|
||||
}, [application, requestReload, componentViewer]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasIssueLoading && (
|
||||
<IssueOnLoading
|
||||
componentName={component.name}
|
||||
reloadIframe={reloadIframe}
|
||||
reloadIframe={() => {
|
||||
reloadValidityStatus(), requestReload?.(componentViewer);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{featureStatus !== FeatureStatus.Entitled && (
|
||||
<IsExpired
|
||||
expiredDate={dateToLocalizedString(component.valid_until)}
|
||||
reloadStatus={reloadValidityStatus}
|
||||
featureStatus={featureStatus}
|
||||
componentName={component.name}
|
||||
manageSubscription={manageSubscription}
|
||||
/>
|
||||
)}
|
||||
{isDeprecated && !isDeprecationMessageDismissed && (
|
||||
{deprecationMessage && !isDeprecationMessageDismissed && (
|
||||
<IsDeprecated
|
||||
deprecationMessage={deprecationMessage}
|
||||
dismissDeprecationMessage={dismissDeprecationMessage}
|
||||
/>
|
||||
)}
|
||||
{error == 'offline-restricted' && (
|
||||
<OfflineRestricted
|
||||
isReloading={isReloading}
|
||||
reloadStatus={reloadValidityStatus}
|
||||
/>
|
||||
{error === ComponentViewerError.OfflineRestricted && (
|
||||
<OfflineRestricted />
|
||||
)}
|
||||
{error == 'url-missing' && (
|
||||
{error === ComponentViewerError.MissingUrl && (
|
||||
<UrlMissing componentName={component.name} />
|
||||
)}
|
||||
{component.uuid && !isReloading && isComponentValid && (
|
||||
{component.uuid && isComponentValid && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
data-component-id={component.uuid}
|
||||
data-component-viewer-id={componentViewer.identifier}
|
||||
frameBorder={0}
|
||||
data-attr-id={`component-iframe-${component.uuid}`}
|
||||
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"
|
||||
>
|
||||
@@ -347,7 +266,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
|
||||
export const ComponentViewDirective = toDirective<IProps>(ComponentView, {
|
||||
onLoad: '=',
|
||||
componentUuid: '=',
|
||||
templateComponent: '=',
|
||||
componentViewer: '=',
|
||||
requestReload: '=',
|
||||
manualDealloc: '=',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { FeatureIdentifier } from '@standardnotes/features';
|
||||
import { FeatureStatus } from '@standardnotes/snjs';
|
||||
import { FeatureStatus, FeatureIdentifier } from '@standardnotes/snjs';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
|
||||
@@ -174,7 +174,11 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
};
|
||||
|
||||
const toggleComponent = (component: SNComponent) => {
|
||||
application.toggleComponent(component);
|
||||
if (component.isTheme()) {
|
||||
application.toggleTheme(component);
|
||||
} else {
|
||||
application.toggleComponent(component);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (
|
||||
@@ -218,7 +222,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
const activeTheme = themes.find(
|
||||
(theme) => theme.active && !theme.isLayerable()
|
||||
);
|
||||
if (activeTheme) application.toggleComponent(activeTheme);
|
||||
if (activeTheme) application.toggleTheme(activeTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ThemesMenuButton: FunctionComponent<Props> = ({
|
||||
const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.preventDefault();
|
||||
if (theme.isLayerable() || !theme.active) {
|
||||
application.toggleComponent(theme);
|
||||
application.toggleTheme(theme);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,11 +20,8 @@ type Props = {
|
||||
export const SearchOptions = observer(({ appState }: Props) => {
|
||||
const { searchOptions } = appState;
|
||||
|
||||
const {
|
||||
includeProtectedContents,
|
||||
includeArchived,
|
||||
includeTrashed,
|
||||
} = searchOptions;
|
||||
const { includeProtectedContents, includeArchived, includeTrashed } =
|
||||
searchOptions;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [position, setPosition] = useState({
|
||||
@@ -34,7 +31,10 @@ export const SearchOptions = observer(({ appState }: Props) => {
|
||||
const [maxWidth, setMaxWidth] = useState<number | 'auto'>('auto');
|
||||
const buttonRef = useRef<HTMLButtonElement>(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() {
|
||||
setLockCloseOnBlur(true);
|
||||
|
||||
@@ -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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export { ActionsMenu } from './actionsMenu';
|
||||
export { ComponentModal } from './componentModal';
|
||||
export { EditorMenu } from './editorMenu';
|
||||
export { InputModal } from './inputModal';
|
||||
export { MenuRow } from './menuRow';
|
||||
|
||||
@@ -2,9 +2,8 @@ import { WebDirective } from './../../types';
|
||||
import template from '%/directives/permissions-modal.pug';
|
||||
|
||||
class PermissionsModalCtrl {
|
||||
|
||||
$element: JQLite
|
||||
callback!: (success: boolean) => void
|
||||
$element: JQLite;
|
||||
callback!: (success: boolean) => void;
|
||||
|
||||
/* @ngInject */
|
||||
constructor($element: JQLite) {
|
||||
@@ -41,7 +40,7 @@ export class PermissionsModal extends WebDirective {
|
||||
show: '=',
|
||||
component: '=',
|
||||
permissionsString: '=',
|
||||
callback: '='
|
||||
callback: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
import { ComponentViewer } from '@standardnotes/snjs/dist/@types';
|
||||
import { PureViewCtrl } from './../../views/abstract/pure_view_ctrl';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { WebDirective } from './../../types';
|
||||
import {
|
||||
ContentType,
|
||||
PayloadSource,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
ComponentArea
|
||||
} from '@standardnotes/snjs';
|
||||
import { ContentType, PayloadSource, SNNote } from '@standardnotes/snjs';
|
||||
import template from '%/directives/revision-preview-modal.pug';
|
||||
import { PayloadContent } from '@standardnotes/snjs';
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
import { STRING_RESTORE_LOCKED_ATTEMPT } from '@/strings';
|
||||
|
||||
interface RevisionPreviewScope {
|
||||
uuid: string
|
||||
content: PayloadContent
|
||||
application: WebApplication
|
||||
uuid: string;
|
||||
content: PayloadContent;
|
||||
application: WebApplication;
|
||||
}
|
||||
|
||||
class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewScope {
|
||||
type State = {
|
||||
componentViewer?: ComponentViewer;
|
||||
};
|
||||
|
||||
$element: JQLite
|
||||
$timeout: ng.ITimeoutService
|
||||
uuid!: string
|
||||
content!: PayloadContent
|
||||
title?: string
|
||||
application!: WebApplication
|
||||
unregisterComponent?: any
|
||||
note!: SNNote
|
||||
class RevisionPreviewModalCtrl
|
||||
extends PureViewCtrl<unknown, State>
|
||||
implements RevisionPreviewScope
|
||||
{
|
||||
$element: JQLite;
|
||||
$timeout: ng.ITimeoutService;
|
||||
uuid!: string;
|
||||
content!: PayloadContent;
|
||||
title?: string;
|
||||
application!: WebApplication;
|
||||
note!: SNNote;
|
||||
private originalNote!: SNNote;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
constructor($element: JQLite, $timeout: ng.ITimeoutService) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
this.$timeout = $timeout;
|
||||
@@ -43,53 +40,36 @@ class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewSc
|
||||
|
||||
$onInit() {
|
||||
this.configure();
|
||||
super.$onInit();
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.unregisterComponent) {
|
||||
this.unregisterComponent();
|
||||
this.unregisterComponent = undefined;
|
||||
if (this.state.componentViewer) {
|
||||
this.application.componentManager.destroyComponentViewer(
|
||||
this.state.componentViewer
|
||||
);
|
||||
}
|
||||
super.$onDestroy();
|
||||
}
|
||||
|
||||
get componentManager() {
|
||||
return this.application.componentManager!;
|
||||
return this.application.componentManager;
|
||||
}
|
||||
|
||||
async configure() {
|
||||
this.note = await this.application.createTemplateItem(
|
||||
this.note = (await this.application.createTemplateItem(
|
||||
ContentType.Note,
|
||||
this.content
|
||||
) as SNNote;
|
||||
)) as SNNote;
|
||||
this.originalNote = this.application.findItem(this.uuid) as SNNote;
|
||||
const editorForNote = this.componentManager.editorForNote(this.originalNote);
|
||||
if (editorForNote) {
|
||||
/**
|
||||
* Create temporary copy, as a lot of componentManager is uuid based, so might
|
||||
* interfere with active editor. Be sure to copy only the content, as the top level
|
||||
* editor object has non-copyable properties like .window, which cannot be transfered
|
||||
*/
|
||||
const editorCopy = await this.application.createTemplateItem(
|
||||
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});
|
||||
const component = this.componentManager.editorForNote(this.originalNote);
|
||||
if (component) {
|
||||
const componentViewer =
|
||||
this.application.componentManager.createComponentViewer(component);
|
||||
componentViewer.setReadonly(true);
|
||||
componentViewer.lockReadonly = true;
|
||||
componentViewer.overrideContextItem = this.note;
|
||||
this.setState({ componentViewer });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,12 +78,19 @@ class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewSc
|
||||
if (asCopy) {
|
||||
await this.application.duplicateItem(this.originalNote, {
|
||||
...this.content,
|
||||
title: this.content.title ? this.content.title + ' (copy)' : undefined
|
||||
title: this.content.title
|
||||
? this.content.title + ' (copy)'
|
||||
: undefined,
|
||||
});
|
||||
} else {
|
||||
this.application.changeAndSaveItem(this.uuid, (mutator) => {
|
||||
mutator.unsafe_setCustomContent(this.content);
|
||||
}, true, PayloadSource.RemoteActionRetrieved);
|
||||
this.application.changeAndSaveItem(
|
||||
this.uuid,
|
||||
(mutator) => {
|
||||
mutator.unsafe_setCustomContent(this.content);
|
||||
},
|
||||
true,
|
||||
PayloadSource.RemoteActionRetrieved
|
||||
);
|
||||
}
|
||||
this.dismiss();
|
||||
};
|
||||
@@ -115,7 +102,7 @@ class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewSc
|
||||
}
|
||||
confirmDialog({
|
||||
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) => {
|
||||
if (confirmed) {
|
||||
run();
|
||||
@@ -146,7 +133,7 @@ export class RevisionPreviewModal extends WebDirective {
|
||||
uuid: '=',
|
||||
content: '=',
|
||||
title: '=',
|
||||
application: '='
|
||||
application: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
115
app/assets/javascripts/preferences/panes/CloudLink.tsx
Normal file
115
app/assets/javascripts/preferences/panes/CloudLink.tsx
Normal 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>Can’t 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. It’s 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 we’ll sort it out.
|
||||
</Text>
|
||||
<LinkButton
|
||||
className="mt-3"
|
||||
link="mailto: help@standardnotes.com"
|
||||
label="Email us"
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
);
|
||||
@@ -1,12 +1,13 @@
|
||||
import { PreferencesGroup, PreferencesSegment } from "@/preferences/components";
|
||||
import { WebApplication } from "@/ui_models/application";
|
||||
import { SNComponent } from "@standardnotes/snjs/dist/@types";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { FunctionComponent } from "preact";
|
||||
import { ExtensionItem } from "./extensions-segments";
|
||||
import { PreferencesGroup, PreferencesSegment } from '@/preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { ComponentViewer, SNComponent } from '@standardnotes/snjs/dist/@types';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { ExtensionItem } from './extensions-segments';
|
||||
import { ComponentView } from '@/components/ComponentView';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PreferencesMenu } from '@/preferences/PreferencesMenu';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
interface IProps {
|
||||
application: WebApplication;
|
||||
@@ -17,7 +18,17 @@ interface IProps {
|
||||
|
||||
export const ExtensionPane: FunctionComponent<IProps> = observer(
|
||||
({ 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 (
|
||||
<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}
|
||||
extension={extension}
|
||||
first={false}
|
||||
uninstall={() => application.deleteItem(extension).then(() => preferencesMenu.loadExtensionsPanes())}
|
||||
toggleActivate={() => application.toggleComponent(extension).then(() => preferencesMenu.loadExtensionsPanes())}
|
||||
uninstall={() =>
|
||||
application
|
||||
.deleteItem(extension)
|
||||
.then(() => preferencesMenu.loadExtensionsPanes())
|
||||
}
|
||||
latestVersion={latestVersion}
|
||||
/>
|
||||
<PreferencesSegment>
|
||||
<ComponentView
|
||||
application={application}
|
||||
appState={appState}
|
||||
componentUuid={extension.uuid}
|
||||
componentViewer={componentViewer}
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
@@ -44,4 +58,5 @@ export const ExtensionPane: FunctionComponent<IProps> = observer(
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -77,11 +77,6 @@ export const Extensions: FunctionComponent<{
|
||||
setExtensions(loadExtensions(application));
|
||||
};
|
||||
|
||||
const toggleActivateExtension = (extension: SNComponent) => {
|
||||
application.toggleComponent(extension);
|
||||
setExtensions(loadExtensions(application));
|
||||
};
|
||||
|
||||
const visibleExtensions = extensions.filter((extension) => {
|
||||
return (
|
||||
extension.package_info != undefined &&
|
||||
@@ -105,7 +100,6 @@ export const Extensions: FunctionComponent<{
|
||||
latestVersion={extensionsLatestVersions.getVersion(extension)}
|
||||
first={i === 0}
|
||||
uninstall={uninstallExtension}
|
||||
toggleActivate={toggleActivateExtension}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -17,8 +17,9 @@ const DisclosureIconButton: FunctionComponent<{
|
||||
<DisclosureButton
|
||||
onMouseEnter={onMouseEnter}
|
||||
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} />
|
||||
</DisclosureButton>
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
import {
|
||||
SNComponent,
|
||||
PurePayload,
|
||||
ComponentMutator,
|
||||
AppDataField,
|
||||
EncryptionIntent,
|
||||
ApplicationService,
|
||||
ApplicationEvent,
|
||||
removeFromArray,
|
||||
BackupFile,
|
||||
DesktopManagerInterface,
|
||||
} from '@standardnotes/snjs';
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
// An interface used by the Desktop app to interact with SN
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { Bridge } from './bridge';
|
||||
|
||||
type UpdateObserverCallback = (component: SNComponent) => void;
|
||||
type ComponentActivationCallback = (payload: PurePayload) => void;
|
||||
type ComponentActivationObserver = {
|
||||
id: string;
|
||||
callback: ComponentActivationCallback;
|
||||
};
|
||||
|
||||
export class DesktopManager extends ApplicationService {
|
||||
/**
|
||||
* An interface used by the Desktop application to interact with SN
|
||||
*/
|
||||
export class DesktopManager
|
||||
extends ApplicationService
|
||||
implements DesktopManagerInterface
|
||||
{
|
||||
$rootScope: ng.IRootScopeService;
|
||||
$timeout: ng.ITimeoutService;
|
||||
componentActivationObservers: ComponentActivationObserver[] = [];
|
||||
updateObservers: {
|
||||
callback: UpdateObserverCallback;
|
||||
callback: (component: SNComponent) => void;
|
||||
}[] = [];
|
||||
|
||||
isDesktop = isDesktopApplication();
|
||||
|
||||
dataLoaded = false;
|
||||
lastSearchedText?: string;
|
||||
private removeComponentObserver?: () => void;
|
||||
|
||||
constructor(
|
||||
$rootScope: ng.IRootScopeService,
|
||||
@@ -52,10 +46,7 @@ export class DesktopManager extends ApplicationService {
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.componentActivationObservers.length = 0;
|
||||
this.updateObservers.length = 0;
|
||||
this.removeComponentObserver?.();
|
||||
this.removeComponentObserver = undefined;
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
@@ -73,9 +64,9 @@ export class DesktopManager extends ApplicationService {
|
||||
this.bridge.onMajorDataChange();
|
||||
}
|
||||
|
||||
getExtServerHost() {
|
||||
getExtServerHost(): string {
|
||||
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
|
||||
*/
|
||||
convertComponentForTransmission(component: SNComponent) {
|
||||
return this.application!.protocolService!.payloadByEncryptingPayload(
|
||||
return this.application.protocolService!.payloadByEncryptingPayload(
|
||||
component.payloadRepresentation(),
|
||||
EncryptionIntent.FileDecrypted
|
||||
);
|
||||
@@ -107,7 +98,7 @@ export class DesktopManager extends ApplicationService {
|
||||
});
|
||||
}
|
||||
|
||||
registerUpdateObserver(callback: UpdateObserverCallback) {
|
||||
registerUpdateObserver(callback: (component: SNComponent) => void) {
|
||||
const observer = {
|
||||
callback: callback,
|
||||
};
|
||||
@@ -143,11 +134,11 @@ export class DesktopManager extends ApplicationService {
|
||||
componentData: any,
|
||||
error: any
|
||||
) {
|
||||
const component = this.application!.findItem(componentData.uuid);
|
||||
const component = this.application.findItem(componentData.uuid);
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
const updatedComponent = await this.application!.changeAndSaveItem(
|
||||
const updatedComponent = await this.application.changeAndSaveItem(
|
||||
component.uuid,
|
||||
(m) => {
|
||||
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() {
|
||||
const data = await this.application!.createBackupFile(
|
||||
const data = await this.application.createBackupFile(
|
||||
this.application.hasProtectionSources()
|
||||
? EncryptionIntent.FileEncrypted
|
||||
: EncryptionIntent.FileDecrypted
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
removeFromArray,
|
||||
ApplicationEvent,
|
||||
ContentType,
|
||||
UuidString,
|
||||
FeatureStatus,
|
||||
} from '@standardnotes/snjs';
|
||||
|
||||
const CACHED_THEMES_KEY = 'cachedThemes';
|
||||
|
||||
export class ThemeManager extends ApplicationService {
|
||||
private activeThemes: string[] = [];
|
||||
private activeThemes: UuidString[] = [];
|
||||
private unregisterDesktop!: () => void;
|
||||
private unregisterStream!: () => void;
|
||||
|
||||
@@ -22,6 +24,8 @@ export class ThemeManager extends ApplicationService {
|
||||
this.deactivateAllThemes();
|
||||
} else if (event === ApplicationEvent.StorageReady) {
|
||||
await this.activateCachedThemes();
|
||||
} else if (event === ApplicationEvent.FeaturesUpdated) {
|
||||
this.reloadThemeStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +38,24 @@ export class ThemeManager extends ApplicationService {
|
||||
this.activeThemes.length = 0;
|
||||
this.unregisterDesktop();
|
||||
this.unregisterStream();
|
||||
(this.unregisterDesktop as any) = undefined;
|
||||
(this.unregisterStream as any) = undefined;
|
||||
(this.unregisterDesktop as unknown) = undefined;
|
||||
(this.unregisterStream as unknown) = undefined;
|
||||
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 */
|
||||
async onAppStart() {
|
||||
super.onAppStart();
|
||||
@@ -99,7 +116,11 @@ export class ThemeManager extends ApplicationService {
|
||||
return;
|
||||
}
|
||||
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');
|
||||
link.href = url;
|
||||
link.type = 'text/css';
|
||||
@@ -125,19 +146,19 @@ export class ThemeManager extends ApplicationService {
|
||||
}
|
||||
|
||||
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(
|
||||
themes.map(async (theme) => {
|
||||
const payload = theme.payloadRepresentation();
|
||||
const processedPayload =
|
||||
await this.application!.protocolService!.payloadByEncryptingPayload(
|
||||
await this.application.protocolService.payloadByEncryptingPayload(
|
||||
payload,
|
||||
EncryptionIntent.LocalStorageDecrypted
|
||||
);
|
||||
return processedPayload;
|
||||
})
|
||||
);
|
||||
return this.application!.setValue(
|
||||
return this.application.setValue(
|
||||
CACHED_THEMES_KEY,
|
||||
mapped,
|
||||
StorageValueModes.Nonwrapped
|
||||
@@ -154,15 +175,15 @@ export class ThemeManager extends ApplicationService {
|
||||
}
|
||||
|
||||
private async getCachedThemes() {
|
||||
const cachedThemes = (await this.application!.getValue(
|
||||
const cachedThemes = (await this.application.getValue(
|
||||
CACHED_THEMES_KEY,
|
||||
StorageValueModes.Nonwrapped
|
||||
)) as SNTheme[];
|
||||
if (cachedThemes) {
|
||||
const themes = [];
|
||||
for (const cachedTheme of cachedThemes) {
|
||||
const payload = this.application!.createPayloadFromObject(cachedTheme);
|
||||
const theme = this.application!.createItemFromPayload(
|
||||
const payload = this.application.createPayloadFromObject(cachedTheme);
|
||||
const theme = this.application.createItemFromPayload(
|
||||
payload
|
||||
) as SNTheme;
|
||||
themes.push(theme);
|
||||
|
||||
@@ -77,6 +77,8 @@ export class AppState {
|
||||
editingTag: SNTag | undefined;
|
||||
_templateTag: SNTag | undefined;
|
||||
|
||||
private multiEditorSupport = false;
|
||||
|
||||
readonly quickSettingsMenu = new QuickSettingsState();
|
||||
readonly accountMenu: AccountMenuState;
|
||||
readonly actionsMenu = new ActionsMenuState();
|
||||
@@ -224,27 +226,21 @@ export class AppState {
|
||||
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) {
|
||||
const activeEditor = this.getActiveEditor();
|
||||
if (!this.multiEditorSupport) {
|
||||
this.closeActiveEditor();
|
||||
}
|
||||
const activeTagUuid = this.selectedTag
|
||||
? this.selectedTag.isSmartTag
|
||||
? undefined
|
||||
: this.selectedTag.uuid
|
||||
: undefined;
|
||||
|
||||
if (!activeEditor) {
|
||||
this.application.editorGroup.createEditor(
|
||||
undefined,
|
||||
title,
|
||||
activeTagUuid
|
||||
);
|
||||
} else {
|
||||
await activeEditor.reset(title, activeTagUuid);
|
||||
}
|
||||
await this.application.editorGroup.createEditor(
|
||||
undefined,
|
||||
title,
|
||||
activeTagUuid
|
||||
);
|
||||
}
|
||||
|
||||
getActiveEditor() {
|
||||
|
||||
@@ -167,12 +167,12 @@ export class NotesState {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.activeEditor) {
|
||||
this.application.editorGroup.createEditor(noteUuid);
|
||||
} else {
|
||||
this.activeEditor.setNote(note);
|
||||
if (this.activeEditor) {
|
||||
this.application.editorGroup.closeActiveEditor();
|
||||
}
|
||||
|
||||
await this.application.editorGroup.createEditor(noteUuid);
|
||||
|
||||
this.appState.noteTags.reloadTags();
|
||||
await this.onActiveEditorChanged();
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { ContentType, SNSmartTag, SNTag, UuidString } from '@standardnotes/snjs';
|
||||
import {
|
||||
ContentType,
|
||||
SNSmartTag,
|
||||
SNTag,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs';
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
makeAutoObservable,
|
||||
makeObservable,
|
||||
observable,
|
||||
runInAction
|
||||
runInAction,
|
||||
} from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
import { FeaturesState } from './features_state';
|
||||
|
||||
|
||||
export class TagsState {
|
||||
tags: SNTag[] = [];
|
||||
smartTags: SNSmartTag[] = [];
|
||||
|
||||
@@ -18,12 +18,9 @@ import {
|
||||
PermissionDialog,
|
||||
Platform,
|
||||
SNApplication,
|
||||
SNComponent,
|
||||
} from '@standardnotes/snjs';
|
||||
import angular from 'angular';
|
||||
import { ComponentModalScope } from './../directives/views/componentModal';
|
||||
import { AccountSwitcherScope, PermissionsModalScope } from './../types';
|
||||
import { ComponentGroup } from './component_group';
|
||||
|
||||
type WebServices = {
|
||||
appState: AppState;
|
||||
@@ -40,7 +37,6 @@ export class WebApplication extends SNApplication {
|
||||
private webServices!: WebServices;
|
||||
private currentAuthenticationElement?: angular.IRootElementService;
|
||||
public editorGroup: EditorGroup;
|
||||
public componentGroup: ComponentGroup;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
@@ -71,8 +67,6 @@ export class WebApplication extends SNApplication {
|
||||
this.scope = scope;
|
||||
deviceInterface.setApplication(this);
|
||||
this.editorGroup = new EditorGroup(this);
|
||||
this.componentGroup = new ComponentGroup(this);
|
||||
this.openModalComponent = this.openModalComponent.bind(this);
|
||||
this.presentPermissionsDialog = this.presentPermissionsDialog.bind(this);
|
||||
}
|
||||
|
||||
@@ -85,14 +79,12 @@ export class WebApplication extends SNApplication {
|
||||
(service as any).application = undefined;
|
||||
}
|
||||
this.webServices = {} as WebServices;
|
||||
(this.$compile as any) = undefined;
|
||||
(this.$compile as unknown) = undefined;
|
||||
this.editorGroup.deinit();
|
||||
this.componentGroup.deinit();
|
||||
(this.scope! as any).application = undefined;
|
||||
(this.scope as any).application = undefined;
|
||||
this.scope!.$destroy();
|
||||
this.scope = undefined;
|
||||
(this.openModalComponent as any) = undefined;
|
||||
(this.presentPermissionsDialog as any) = undefined;
|
||||
(this.presentPermissionsDialog as unknown) = undefined;
|
||||
/** Allow our Angular directives to be destroyed and any pending digest cycles
|
||||
* to complete before destroying the global application instance and all its services */
|
||||
setTimeout(() => {
|
||||
@@ -105,8 +97,7 @@ export class WebApplication extends SNApplication {
|
||||
|
||||
onStart(): void {
|
||||
super.onStart();
|
||||
this.componentManager!.openModalComponent = this.openModalComponent;
|
||||
this.componentManager!.presentPermissionsDialog =
|
||||
this.componentManager.presentPermissionsDialog =
|
||||
this.presentPermissionsDialog;
|
||||
}
|
||||
|
||||
@@ -210,24 +201,6 @@ export class WebApplication extends SNApplication {
|
||||
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) {
|
||||
const scope = this.scope!.$new(true) as PermissionsModalScope;
|
||||
scope.permissionsString = dialog.permissionsString;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,37 +3,36 @@ import {
|
||||
ContentType,
|
||||
PayloadSource,
|
||||
UuidString,
|
||||
TagMutator,
|
||||
SNTag,
|
||||
} from '@standardnotes/snjs';
|
||||
import { WebApplication } from './application';
|
||||
import { NoteTagsState } from './app_state/note_tags_state';
|
||||
|
||||
export class Editor {
|
||||
public note!: SNNote;
|
||||
private application: WebApplication;
|
||||
private _onNoteChange?: () => void;
|
||||
private _onNoteValueChange?: (note: SNNote, source?: PayloadSource) => void;
|
||||
private onNoteValueChange?: (note: SNNote, source: PayloadSource) => void;
|
||||
private removeStreamObserver?: () => void;
|
||||
public isTemplateNote = false;
|
||||
|
||||
constructor(
|
||||
application: WebApplication,
|
||||
noteUuid: string | undefined,
|
||||
noteTitle: string | undefined,
|
||||
noteTag: UuidString | undefined
|
||||
private defaultTitle: string | undefined,
|
||||
private defaultTag: UuidString | undefined
|
||||
) {
|
||||
this.application = application;
|
||||
if (noteUuid) {
|
||||
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() {
|
||||
this.removeStreamObserver = this.application.streamItems(
|
||||
ContentType.Note,
|
||||
@@ -45,14 +44,12 @@ export class Editor {
|
||||
|
||||
deinit() {
|
||||
this.removeStreamObserver?.();
|
||||
(this.removeStreamObserver as any) = undefined;
|
||||
this._onNoteChange = undefined;
|
||||
(this.application as any) = undefined;
|
||||
this._onNoteChange = undefined;
|
||||
this._onNoteValueChange = undefined;
|
||||
(this.removeStreamObserver as unknown) = undefined;
|
||||
(this.application as unknown) = undefined;
|
||||
this.onNoteValueChange = undefined;
|
||||
}
|
||||
|
||||
private handleNoteStream(notes: SNNote[], source?: PayloadSource) {
|
||||
private handleNoteStream(notes: SNNote[], source: PayloadSource) {
|
||||
/** Update our note object reference whenever it changes */
|
||||
const matchingNote = notes.find((item) => {
|
||||
return item.uuid === this.note.uuid;
|
||||
@@ -60,7 +57,7 @@ export class Editor {
|
||||
if (matchingNote) {
|
||||
this.isTemplateNote = false;
|
||||
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,
|
||||
* and creating a placeholder note.
|
||||
*/
|
||||
async reset(noteTitle = '', noteTag?: UuidString) {
|
||||
async createTemplateNote(defaultTitle?: string, noteTag?: UuidString) {
|
||||
const note = (await this.application.createTemplateItem(ContentType.Note, {
|
||||
text: '',
|
||||
title: noteTitle,
|
||||
title: defaultTitle,
|
||||
references: [],
|
||||
})) as SNNote;
|
||||
if (noteTag) {
|
||||
const tag = this.application.findItem(noteTag) as SNTag;
|
||||
await this.application.addTagHierarchyToNote(note, tag);
|
||||
}
|
||||
if (!this.isTemplateNote || this.note.title !== note.title) {
|
||||
this.setNote(note as SNNote, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
this.isTemplateNote = true;
|
||||
this.note = note;
|
||||
this.onNoteValueChange?.(this.note, this.note.payload.source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register to be notified when the editor's note's values change
|
||||
* (and thus a new object reference is created)
|
||||
*/
|
||||
public onNoteValueChange(
|
||||
callback: (note: SNNote, source?: PayloadSource) => void
|
||||
public setOnNoteValueChange(
|
||||
callback: (note: SNNote, source: PayloadSource) => void
|
||||
) {
|
||||
this._onNoteValueChange = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
this.onNoteValueChange = callback;
|
||||
if (this.note) {
|
||||
this.onNoteValueChange(this.note, this.note.payload.source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,31 @@ import { removeFromArray, UuidString } from '@standardnotes/snjs';
|
||||
import { Editor } from './editor';
|
||||
import { WebApplication } from './application';
|
||||
|
||||
type EditorGroupChangeCallback = () => void
|
||||
type EditorGroupChangeCallback = () => void;
|
||||
|
||||
export class EditorGroup {
|
||||
|
||||
public editors: Editor[] = []
|
||||
private application: WebApplication
|
||||
changeObservers: EditorGroupChangeCallback[] = []
|
||||
public editors: Editor[] = [];
|
||||
private application: WebApplication;
|
||||
changeObservers: EditorGroupChangeCallback[] = [];
|
||||
|
||||
constructor(application: WebApplication) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
(this.application as any) = undefined;
|
||||
(this.application as unknown) = undefined;
|
||||
for (const editor of this.editors) {
|
||||
this.deleteEditor(editor);
|
||||
}
|
||||
}
|
||||
|
||||
createEditor(
|
||||
async createEditor(
|
||||
noteUuid?: string,
|
||||
noteTitle?: string,
|
||||
noteTag?: UuidString
|
||||
) {
|
||||
const editor = new Editor(this.application, noteUuid, noteTitle, noteTag);
|
||||
await editor.initialize();
|
||||
this.editors.push(editor);
|
||||
this.notifyObservers();
|
||||
}
|
||||
@@ -43,13 +43,13 @@ export class EditorGroup {
|
||||
|
||||
closeActiveEditor() {
|
||||
const activeEditor = this.activeEditor;
|
||||
if(activeEditor) {
|
||||
if (activeEditor) {
|
||||
this.deleteEditor(activeEditor);
|
||||
}
|
||||
}
|
||||
|
||||
closeAllEditors() {
|
||||
for(const editor of this.editors) {
|
||||
for (const editor of this.editors) {
|
||||
this.deleteEditor(editor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,26 @@ import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { autorun, IReactionDisposer, IReactionPublic } from 'mobx';
|
||||
|
||||
export type CtrlState = Partial<Record<string, any>>
|
||||
export type CtrlProps = Partial<Record<string, any>>
|
||||
export type CtrlState = Partial<Record<string, any>>;
|
||||
export type CtrlProps = Partial<Record<string, any>>;
|
||||
|
||||
export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
$timeout: ng.ITimeoutService
|
||||
$timeout: ng.ITimeoutService;
|
||||
/** Passed through templates */
|
||||
application!: WebApplication
|
||||
state: S = {} as any
|
||||
private unsubApp: any
|
||||
private unsubState: any
|
||||
private stateTimeout?: ng.IPromise<void>
|
||||
application!: WebApplication;
|
||||
state: S = {} as any;
|
||||
private unsubApp: any;
|
||||
private unsubState: any;
|
||||
private stateTimeout?: ng.IPromise<void>;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected templateReady = false
|
||||
protected templateReady = false;
|
||||
private reactionDisposers: IReactionDisposer[] = [];
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService,
|
||||
public props: P = {} as any
|
||||
) {
|
||||
constructor($timeout: ng.ITimeoutService, public props: P = {} as any) {
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
@@ -91,8 +88,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
|
||||
/** @override */
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
afterStateChange(): void {
|
||||
}
|
||||
afterStateChange(): void {}
|
||||
|
||||
/** @returns a promise that resolves after the UI has been updated. */
|
||||
flushUI(): angular.IPromise<void> {
|
||||
@@ -129,22 +125,24 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
if (this.application!.isLaunched()) {
|
||||
this.onAppLaunch();
|
||||
}
|
||||
this.unsubApp = this.application!.addEventObserver(async (eventName, data: any) => {
|
||||
this.onAppEvent(eventName, data);
|
||||
if (eventName === ApplicationEvent.Started) {
|
||||
await this.onAppStart();
|
||||
} else if (eventName === ApplicationEvent.Launched) {
|
||||
await this.onAppLaunch();
|
||||
} else if (eventName === ApplicationEvent.CompletedIncrementalSync) {
|
||||
this.onAppIncrementalSync();
|
||||
} else if (eventName === ApplicationEvent.CompletedFullSync) {
|
||||
this.onAppFullSync();
|
||||
} else if (eventName === ApplicationEvent.KeyStatusChanged) {
|
||||
this.onAppKeyChange();
|
||||
} else if (eventName === ApplicationEvent.LocalDataLoaded) {
|
||||
this.onLocalDataLoaded();
|
||||
this.unsubApp = this.application!.addEventObserver(
|
||||
async (eventName, data: any) => {
|
||||
this.onAppEvent(eventName, data);
|
||||
if (eventName === ApplicationEvent.Started) {
|
||||
await this.onAppStart();
|
||||
} else if (eventName === ApplicationEvent.Launched) {
|
||||
await this.onAppLaunch();
|
||||
} else if (eventName === ApplicationEvent.CompletedIncrementalSync) {
|
||||
this.onAppIncrementalSync();
|
||||
} else if (eventName === ApplicationEvent.CompletedFullSync) {
|
||||
this.onAppFullSync();
|
||||
} else if (eventName === ApplicationEvent.KeyStatusChanged) {
|
||||
this.onAppKeyChange();
|
||||
} else if (eventName === ApplicationEvent.LocalDataLoaded) {
|
||||
this.onLocalDataLoaded();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
onAppEvent(eventName: ApplicationEvent, data?: any) {
|
||||
@@ -175,5 +173,4 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
onAppFullSync() {
|
||||
/** Optional override */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -115,14 +115,17 @@ class ApplicationViewCtrl extends PureViewCtrl<
|
||||
/** @override */
|
||||
async onAppEvent(eventName: ApplicationEvent) {
|
||||
super.onAppEvent(eventName);
|
||||
if (eventName === ApplicationEvent.LocalDatabaseReadError) {
|
||||
alertDialog({
|
||||
text: 'Unable to load local database. Please restart the app and try again.',
|
||||
});
|
||||
} else if (eventName === ApplicationEvent.LocalDatabaseWriteError) {
|
||||
alertDialog({
|
||||
text: 'Unable to write to local database. Please restart the app and try again.',
|
||||
});
|
||||
switch (eventName) {
|
||||
case ApplicationEvent.LocalDatabaseReadError:
|
||||
alertDialog({
|
||||
text: 'Unable to load local database. Please restart the app and try again.',
|
||||
});
|
||||
break;
|
||||
case ApplicationEvent.LocalDatabaseWriteError:
|
||||
alertDialog({
|
||||
text: 'Unable to write to local database. Please restart the app and try again.',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ class ChallengeModalCtrl extends PureViewCtrl<unknown, ChallengeModalState> {
|
||||
|
||||
$onDestroy() {
|
||||
render(<></>, this.$element[0]);
|
||||
super.$onDestroy();
|
||||
}
|
||||
|
||||
private render() {
|
||||
|
||||
@@ -34,10 +34,8 @@
|
||||
)
|
||||
.title.overflow-auto
|
||||
input#note-title-editor.input(
|
||||
ng-blur='self.onTitleBlur()',
|
||||
ng-change='self.onTitleChange()',
|
||||
ng-disabled='self.noteLocked',
|
||||
ng-focus='self.onTitleFocus()',
|
||||
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
|
||||
ng-model='self.editorValues.title',
|
||||
select-on-focus='true',
|
||||
@@ -76,7 +74,7 @@
|
||||
callback='self.editorMenuOnSelect',
|
||||
current-item='self.note',
|
||||
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'
|
||||
)
|
||||
.sk-app-bar-item(
|
||||
@@ -114,9 +112,10 @@
|
||||
property="'left'"
|
||||
)
|
||||
component-view.component-view(
|
||||
component-uuid='self.state.editorComponent.uuid',
|
||||
ng-if='self.state.editorComponent && !self.state.editorUnloading',
|
||||
component-viewer='self.state.editorComponentViewer',
|
||||
ng-if='self.state.editorComponentViewer',
|
||||
on-load='self.onEditorLoad',
|
||||
request-reload='self.editorComponentViewerRequestsReload'
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
@@ -126,7 +125,7 @@
|
||||
ng-change='self.contentChanged()',
|
||||
ng-click='self.clickedTextArea()',
|
||||
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-options='{ debounce: self.state.editorDebounce}',
|
||||
ng-readonly='self.noteLocked',
|
||||
@@ -156,24 +155,23 @@
|
||||
| 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.
|
||||
#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
|
||||
.sk-app-bar-item(
|
||||
ng-repeat='component in self.state.stackComponents track by component.uuid'
|
||||
ng-click='self.toggleStackComponentForCurrentItem(component)',
|
||||
ng-repeat='component in self.state.availableStackComponents track by component.uuid'
|
||||
ng-click='self.toggleStackComponent(component)',
|
||||
)
|
||||
.sk-app-bar-item-column
|
||||
.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-label {{component.name}}
|
||||
.sn-component
|
||||
component-view.component-view.component-stack-item(
|
||||
ng-repeat='component in self.state.stackComponents track by component.uuid',
|
||||
component-uuid='component.uuid',
|
||||
ng-repeat='viewer in self.state.stackComponentViewers track by viewer.componentUuid',
|
||||
component-viewer='viewer',
|
||||
manual-dealloc='true',
|
||||
ng-show='!self.stackComponentHidden(component)',
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
|
||||
@@ -11,11 +11,14 @@ import {
|
||||
SNComponent,
|
||||
SNNote,
|
||||
NoteMutator,
|
||||
Uuids,
|
||||
ComponentArea,
|
||||
PrefKey,
|
||||
ComponentMutator,
|
||||
PayloadSource,
|
||||
ComponentViewer,
|
||||
ComponentManagerEvent,
|
||||
TransactionalMutation,
|
||||
ItemMutator,
|
||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||
} from '@standardnotes/snjs';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
@@ -52,8 +55,10 @@ type NoteStatus = {
|
||||
};
|
||||
|
||||
type EditorState = {
|
||||
stackComponents: SNComponent[];
|
||||
editorComponent?: SNComponent;
|
||||
availableStackComponents: SNComponent[];
|
||||
stackComponentViewers: ComponentViewer[];
|
||||
editorComponentViewer?: ComponentViewer;
|
||||
editorStateDidLoad: boolean;
|
||||
saveError?: any;
|
||||
noteStatus?: NoteStatus;
|
||||
marginResizersEnabled?: boolean;
|
||||
@@ -64,11 +69,6 @@ type EditorState = {
|
||||
showEditorMenu: boolean;
|
||||
showHistoryMenu: 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
|
||||
* then re-initialized. Used when reloading spellcheck status. */
|
||||
textareaUnloading: boolean;
|
||||
@@ -97,7 +97,6 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
|
||||
private leftPanelPuppet?: PanelPuppet;
|
||||
private rightPanelPuppet?: PanelPuppet;
|
||||
private unregisterComponent: any;
|
||||
private saveTimeout?: ng.IPromise<void>;
|
||||
private statusTimeout?: ng.IPromise<void>;
|
||||
private lastEditorFocusEventSource?: EventSource;
|
||||
@@ -105,10 +104,11 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
onEditorLoad?: () => void;
|
||||
|
||||
private scrollPosition = 0;
|
||||
private removeTrashKeyObserver?: any;
|
||||
private removeTabObserver?: any;
|
||||
private removeTrashKeyObserver?: () => void;
|
||||
private removeTabObserver?: () => void;
|
||||
private removeComponentStreamObserver?: () => void;
|
||||
private removeComponentManagerObserver?: () => void;
|
||||
|
||||
private removeComponentsObserver!: () => void;
|
||||
private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/* @ngInject */
|
||||
@@ -125,25 +125,26 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this);
|
||||
this.setScrollPosition = this.setScrollPosition.bind(this);
|
||||
this.resetScrollPosition = this.resetScrollPosition.bind(this);
|
||||
this.editorComponentViewerRequestsReload =
|
||||
this.editorComponentViewerRequestsReload.bind(this);
|
||||
this.onEditorLoad = () => {
|
||||
this.application.getDesktopService().redoSearch();
|
||||
};
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.clearNoteProtectionInactivityTimer();
|
||||
this.editor.clearNoteChangeListener();
|
||||
this.removeComponentsObserver();
|
||||
(this.removeComponentsObserver as unknown) = undefined;
|
||||
this.removeTrashKeyObserver();
|
||||
this.removeComponentStreamObserver?.();
|
||||
(this.removeComponentStreamObserver as unknown) = undefined;
|
||||
this.removeComponentManagerObserver?.();
|
||||
(this.removeComponentManagerObserver as unknown) = undefined;
|
||||
this.removeTrashKeyObserver?.();
|
||||
this.removeTrashKeyObserver = undefined;
|
||||
this.removeTabObserver && this.removeTabObserver();
|
||||
this.clearNoteProtectionInactivityTimer();
|
||||
this.removeTabObserver?.();
|
||||
this.removeTabObserver = undefined;
|
||||
this.leftPanelPuppet = undefined;
|
||||
this.rightPanelPuppet = undefined;
|
||||
this.onEditorLoad = undefined;
|
||||
this.unregisterComponent();
|
||||
this.unregisterComponent = undefined;
|
||||
this.saveTimeout = undefined;
|
||||
this.statusTimeout = undefined;
|
||||
(this.onPanelResizeFinish as unknown) = undefined;
|
||||
@@ -162,55 +163,82 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
this.registerKeyboardShortcuts();
|
||||
this.editor.onNoteChange(() => {
|
||||
this.handleEditorNoteChange();
|
||||
});
|
||||
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.editor.setOnNoteValueChange((note, source) => {
|
||||
this.onNoteChanges(note, source);
|
||||
});
|
||||
this.autorun(() => {
|
||||
this.setState({
|
||||
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 */
|
||||
getInitialState() {
|
||||
return {
|
||||
stackComponents: [],
|
||||
availableStackComponents: [],
|
||||
stackComponentViewers: [],
|
||||
editorStateDidLoad: false,
|
||||
editorDebounce: EDITOR_DEBOUNCE,
|
||||
isDesktop: isDesktopApplication(),
|
||||
spellcheck: true,
|
||||
@@ -219,7 +247,6 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
showEditorMenu: false,
|
||||
showHistoryMenu: false,
|
||||
noteStatus: undefined,
|
||||
editorUnloading: false,
|
||||
textareaUnloading: false,
|
||||
showProtectedWarning: false,
|
||||
} as EditorState;
|
||||
@@ -228,7 +255,7 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
async onAppLaunch() {
|
||||
await super.onAppLaunch();
|
||||
this.streamItems();
|
||||
this.registerComponentHandler();
|
||||
this.registerComponentManagerEventObserver();
|
||||
}
|
||||
|
||||
/** @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() {
|
||||
let showNoteContents = true;
|
||||
if (this.application.hasProtectionSources()) {
|
||||
@@ -366,20 +365,45 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
streamItems() {
|
||||
this.removeComponentsObserver = this.application.streamItems(
|
||||
this.removeComponentStreamObserver = this.application.streamItems(
|
||||
ContentType.Component,
|
||||
async (_items, source) => {
|
||||
if (isPayloadSourceInternalChange(source!)) {
|
||||
if (
|
||||
isPayloadSourceInternalChange(source) ||
|
||||
source === PayloadSource.InitialObserverRegistrationPush
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!this.note) return;
|
||||
this.reloadStackComponents();
|
||||
this.reloadEditor();
|
||||
await this.reloadStackComponents();
|
||||
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(
|
||||
this.note
|
||||
);
|
||||
@@ -387,22 +411,29 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
if (newEditor && this.editor.isTemplateNote) {
|
||||
await this.editor.insertTemplatedNote();
|
||||
}
|
||||
const currentEditor = this.state.editorComponent;
|
||||
if (currentEditor?.uuid !== newEditor?.uuid) {
|
||||
const currentComponentViewer = this.state.editorComponentViewer;
|
||||
|
||||
if (currentComponentViewer?.componentUuid !== newEditor?.uuid) {
|
||||
if (currentComponentViewer) {
|
||||
this.application.componentManager.destroyComponentViewer(
|
||||
currentComponentViewer
|
||||
);
|
||||
}
|
||||
await this.setState({
|
||||
/** Unload current component view so that we create a new one */
|
||||
editorUnloading: true,
|
||||
});
|
||||
await this.setState({
|
||||
/** Reload component view */
|
||||
editorComponent: newEditor,
|
||||
editorUnloading: false,
|
||||
editorComponentViewer: undefined,
|
||||
});
|
||||
if (newEditor) {
|
||||
await this.setState({
|
||||
editorComponentViewer: this.createComponentViewer(newEditor),
|
||||
editorStateDidLoad: true,
|
||||
});
|
||||
}
|
||||
this.reloadFont();
|
||||
} else {
|
||||
await this.setState({
|
||||
editorStateDidLoad: true,
|
||||
});
|
||||
}
|
||||
this.application.componentManager.contextItemDidChangeInArea(
|
||||
ComponentArea.Editor
|
||||
);
|
||||
}
|
||||
|
||||
setMenuState(menu: string, state: boolean) {
|
||||
@@ -429,6 +460,8 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
async editorMenuOnSelect(component?: SNComponent) {
|
||||
const transactions: TransactionalMutation[] = [];
|
||||
|
||||
this.setMenuState('showEditorMenu', false);
|
||||
if (this.appState.getActiveEditor()?.isTemplateNote) {
|
||||
await this.appState.getActiveEditor().insertTemplatedNote();
|
||||
@@ -439,43 +472,56 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
if (!component) {
|
||||
if (!this.note.prefersPlainEditor) {
|
||||
await this.application.changeItem(this.note.uuid, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator;
|
||||
noteMutator.prefersPlainEditor = true;
|
||||
transactions.push({
|
||||
itemUuid: this.note.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
const noteMutator = m as NoteMutator;
|
||||
noteMutator.prefersPlainEditor = true;
|
||||
},
|
||||
});
|
||||
this.reloadEditor();
|
||||
}
|
||||
if (
|
||||
this.state.editorComponent?.isExplicitlyEnabledForItem(this.note.uuid)
|
||||
this.state.editorComponentViewer?.component.isExplicitlyEnabledForItem(
|
||||
this.note.uuid
|
||||
)
|
||||
) {
|
||||
await this.disassociateComponentWithCurrentNote(
|
||||
this.state.editorComponent
|
||||
transactions.push(
|
||||
this.transactionForDisassociateComponentWithCurrentNote(
|
||||
this.state.editorComponentViewer.component
|
||||
)
|
||||
);
|
||||
}
|
||||
this.reloadFont();
|
||||
} else if (component.area === ComponentArea.Editor) {
|
||||
const currentEditor = this.state.editorComponent;
|
||||
const currentEditor = this.state.editorComponentViewer?.component;
|
||||
if (currentEditor && component.uuid !== currentEditor.uuid) {
|
||||
await this.disassociateComponentWithCurrentNote(currentEditor);
|
||||
transactions.push(
|
||||
this.transactionForDisassociateComponentWithCurrentNote(currentEditor)
|
||||
);
|
||||
}
|
||||
const prefersPlain = this.note.prefersPlainEditor;
|
||||
if (prefersPlain) {
|
||||
await this.application.changeItem(this.note.uuid, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator;
|
||||
noteMutator.prefersPlainEditor = false;
|
||||
transactions.push({
|
||||
itemUuid: this.note.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
const noteMutator = m as NoteMutator;
|
||||
noteMutator.prefersPlainEditor = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
await this.associateComponentWithCurrentNote(component);
|
||||
} else if (component.area === ComponentArea.EditorStack) {
|
||||
await this.toggleStackComponentForCurrentItem(component);
|
||||
transactions.push(
|
||||
this.transactionForAssociateComponentWithCurrentNote(component)
|
||||
);
|
||||
}
|
||||
|
||||
await this.application.runTransactionalMutations(transactions);
|
||||
/** Dirtying can happen above */
|
||||
this.application.sync();
|
||||
}
|
||||
|
||||
hasAvailableExtensions() {
|
||||
return (
|
||||
this.application.actionsManager!.extensionsInContextOfItem(this.note)
|
||||
this.application.actionsManager.extensionsInContextOfItem(this.note)
|
||||
.length > 0
|
||||
);
|
||||
}
|
||||
@@ -534,7 +580,9 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
const truncate = noteText.length > NOTE_PREVIEW_CHAR_LIMIT;
|
||||
const substring = noteText.substring(0, NOTE_PREVIEW_CHAR_LIMIT);
|
||||
const previewPlain = substring + (truncate ? STRING_ELLIPSES : '');
|
||||
// eslint-disable-next-line camelcase
|
||||
noteMutator.preview_plain = previewPlain;
|
||||
// eslint-disable-next-line camelcase
|
||||
noteMutator.preview_html = undefined;
|
||||
}
|
||||
},
|
||||
@@ -643,12 +691,6 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.closeAllMenus();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onTitleFocus() {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onTitleBlur() {}
|
||||
|
||||
onContentFocus() {
|
||||
this.application
|
||||
.getAppState()
|
||||
@@ -662,11 +704,11 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
|
||||
async deleteNote(permanently: boolean) {
|
||||
if (this.editor.isTemplateNote) {
|
||||
this.application.alertService!.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT);
|
||||
this.application.alertService.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT);
|
||||
return;
|
||||
}
|
||||
if (this.note.locked) {
|
||||
this.application.alertService!.alert(STRING_DELETE_LOCKED_ATTEMPT);
|
||||
this.application.alertService.alert(STRING_DELETE_LOCKED_ATTEMPT);
|
||||
return;
|
||||
}
|
||||
const title = this.note.safeTitle().length
|
||||
@@ -782,25 +824,15 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
|
||||
/** @components */
|
||||
|
||||
registerComponentHandler() {
|
||||
this.unregisterComponent =
|
||||
this.application.componentManager.registerHandler({
|
||||
identifier: 'editor',
|
||||
areas: [ComponentArea.EditorStack, ComponentArea.Editor],
|
||||
contextRequestHandler: (componentUuid) => {
|
||||
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) {
|
||||
registerComponentManagerEventObserver() {
|
||||
this.removeComponentManagerObserver =
|
||||
this.application.componentManager.addEventObserver((eventName, data) => {
|
||||
if (eventName === ComponentManagerEvent.ViewerDidFocus) {
|
||||
const viewer = data?.componentViewer;
|
||||
if (viewer?.component.isEditor) {
|
||||
this.closeAllMenus();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -810,58 +842,98 @@ export class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
.componentsForArea(ComponentArea.EditorStack)
|
||||
.filter((component) => component.active)
|
||||
);
|
||||
if (this.note) {
|
||||
for (const component of stackComponents) {
|
||||
if (component.active) {
|
||||
this.application.componentManager.setComponentHidden(
|
||||
component,
|
||||
!component.isExplicitlyEnabledForItem(this.note.uuid)
|
||||
);
|
||||
}
|
||||
const enabledComponents = stackComponents.filter((component) => {
|
||||
return component.isExplicitlyEnabledForItem(this.note.uuid);
|
||||
});
|
||||
|
||||
const needsNewViewer = enabledComponents.filter((component) => {
|
||||
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(
|
||||
ComponentArea.EditorStack
|
||||
|
||||
for (const viewer of needsDestroyViewer) {
|
||||
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) {
|
||||
return this.application.componentManager?.isComponentHidden(component);
|
||||
}
|
||||
|
||||
async toggleStackComponentForCurrentItem(component: SNComponent) {
|
||||
const hidden =
|
||||
this.application.componentManager.isComponentHidden(component);
|
||||
if (hidden || !component.active) {
|
||||
this.application.componentManager.setComponentHidden(component, false);
|
||||
async toggleStackComponent(component: SNComponent) {
|
||||
if (!component.isExplicitlyEnabledForItem(this.note.uuid)) {
|
||||
await this.associateComponentWithCurrentNote(component);
|
||||
this.application.componentManager.contextItemDidChangeInArea(
|
||||
ComponentArea.EditorStack
|
||||
);
|
||||
} else {
|
||||
this.application.componentManager.setComponentHidden(component, true);
|
||||
await this.disassociateComponentWithCurrentNote(component);
|
||||
}
|
||||
this.application.sync();
|
||||
}
|
||||
|
||||
async disassociateComponentWithCurrentNote(component: SNComponent) {
|
||||
return this.application.runTransactionalMutation(
|
||||
this.transactionForDisassociateComponentWithCurrentNote(component)
|
||||
);
|
||||
}
|
||||
|
||||
transactionForDisassociateComponentWithCurrentNote(component: SNComponent) {
|
||||
const note = this.note;
|
||||
return this.application.changeItem(component.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.removeAssociatedItemId(note.uuid);
|
||||
mutator.disassociateWithItem(note.uuid);
|
||||
});
|
||||
const transaction: TransactionalMutation = {
|
||||
itemUuid: component.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.removeAssociatedItemId(note.uuid);
|
||||
mutator.disassociateWithItem(note.uuid);
|
||||
},
|
||||
};
|
||||
return transaction;
|
||||
}
|
||||
|
||||
async associateComponentWithCurrentNote(component: SNComponent) {
|
||||
return this.application.runTransactionalMutation(
|
||||
this.transactionForAssociateComponentWithCurrentNote(component)
|
||||
);
|
||||
}
|
||||
|
||||
transactionForAssociateComponentWithCurrentNote(component: SNComponent) {
|
||||
const note = this.note;
|
||||
return this.application.changeItem(component.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.removeDisassociatedItemId(note.uuid);
|
||||
mutator.associateWithItem(note.uuid);
|
||||
});
|
||||
const transaction: TransactionalMutation = {
|
||||
itemUuid: component.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.removeDisassociatedItemId(note.uuid);
|
||||
mutator.associateWithItem(note.uuid);
|
||||
},
|
||||
};
|
||||
return transaction;
|
||||
}
|
||||
|
||||
registerKeyboardShortcuts() {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
ApplicationEvent,
|
||||
ContentType,
|
||||
SNTheme,
|
||||
ComponentArea,
|
||||
CollectionSort,
|
||||
} from '@standardnotes/snjs';
|
||||
import template from './footer-view.pug';
|
||||
@@ -43,7 +42,6 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
> {
|
||||
private $rootScope: ng.IRootScopeService;
|
||||
private showSyncResolution = false;
|
||||
private unregisterComponent: any;
|
||||
private rootScopeListener2: any;
|
||||
public arbitraryStatusMessage?: string;
|
||||
public user?: any;
|
||||
@@ -73,8 +71,6 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
deinit() {
|
||||
for (const remove of this.observerRemovers) remove();
|
||||
this.observerRemovers.length = 0;
|
||||
this.unregisterComponent();
|
||||
this.unregisterComponent = undefined;
|
||||
this.rootScopeListener2();
|
||||
this.rootScopeListener2 = undefined;
|
||||
(this.closeAccountMenu as unknown) = undefined;
|
||||
@@ -146,7 +142,6 @@ class FooterViewCtrl extends PureViewCtrl<
|
||||
this.updateOfflineStatus();
|
||||
this.findErrors();
|
||||
this.streamItems();
|
||||
this.registerComponentHandler();
|
||||
}
|
||||
|
||||
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() {
|
||||
const statusManager = this.application.getStatusManager();
|
||||
const syncStatus = this.application.getSyncStatus();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#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-uuid='self.component.uuid',
|
||||
component-viewer='self.state.componentViewer',
|
||||
application='self.application'
|
||||
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
|
||||
.section-title-bar-header
|
||||
.sk-h3.title
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
ApplicationEvent,
|
||||
ComponentAction,
|
||||
ComponentArea,
|
||||
ComponentViewer,
|
||||
ContentType,
|
||||
isPayloadSourceInternalChange,
|
||||
MessageData,
|
||||
PayloadSource,
|
||||
PrefKey,
|
||||
SNComponent,
|
||||
SNSmartTag,
|
||||
@@ -22,14 +26,14 @@ type TagState = {
|
||||
smartTags: SNSmartTag[];
|
||||
noteCounts: NoteCounts;
|
||||
selectedTag?: SNTag;
|
||||
componentViewer?: ComponentViewer;
|
||||
};
|
||||
|
||||
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
/** Passed through template */
|
||||
readonly application!: WebApplication;
|
||||
private readonly panelPuppet: PanelPuppet;
|
||||
private unregisterComponent?: any;
|
||||
component?: SNComponent;
|
||||
private unregisterComponent?: () => void;
|
||||
/** The original name of the edtingTag before it began editing */
|
||||
formData: { tagTitle?: string } = {};
|
||||
titles: Partial<Record<UuidString, string>> = {};
|
||||
@@ -46,9 +50,9 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
|
||||
deinit() {
|
||||
this.removeTagsObserver?.();
|
||||
(this.removeTagsObserver as any) = undefined;
|
||||
(this.removeFoldersObserver as any) = undefined;
|
||||
this.unregisterComponent();
|
||||
(this.removeTagsObserver as unknown) = undefined;
|
||||
(this.removeFoldersObserver as unknown) = undefined;
|
||||
this.unregisterComponent?.();
|
||||
this.unregisterComponent = undefined;
|
||||
super.deinit();
|
||||
}
|
||||
@@ -64,15 +68,10 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
async onAppStart() {
|
||||
super.onAppStart();
|
||||
this.registerComponentHandler();
|
||||
}
|
||||
|
||||
async onAppLaunch() {
|
||||
super.onAppLaunch();
|
||||
this.loadPreferences();
|
||||
this.beginStreamingItems();
|
||||
this.streamForFoldersComponent();
|
||||
|
||||
const smartTags = this.application.getSmartTags();
|
||||
this.setState({ smartTags });
|
||||
@@ -85,13 +84,78 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
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(
|
||||
[ContentType.Component],
|
||||
async () => {
|
||||
this.component = this.application.componentManager
|
||||
.componentsForArea(ComponentArea.TagsList).find((component) => component.active);
|
||||
});
|
||||
async (items, source) => {
|
||||
if (
|
||||
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(
|
||||
[ContentType.Tag, ContentType.SmartTag],
|
||||
@@ -200,41 +264,6 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
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) {
|
||||
if (tag.conflictOf) {
|
||||
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
@@ -3,7 +3,7 @@
|
||||
.sn-component
|
||||
.sk-panel
|
||||
.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
|
||||
.sk-panel-content
|
||||
.sk-panel-section
|
||||
@@ -14,8 +14,8 @@
|
||||
| {{ctrl.permissionsString}}
|
||||
.sk-panel-row
|
||||
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(
|
||||
href='https://standardnotes.com/permissions',
|
||||
rel='noopener',
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
.sk-panel-header-title Preview
|
||||
.sk-subtitle.neutral.mt-1(
|
||||
ng-if="ctrl.title"
|
||||
) {{ctrl.title}}
|
||||
) {{ctrl.title}}
|
||||
.sk-horizontal-group
|
||||
a.sk-a.info.close-button(
|
||||
ng-click="ctrl.restore(false)"
|
||||
@@ -20,18 +20,18 @@
|
||||
a.sk-a.info.close-button(
|
||||
ng-click="ctrl.dismiss(); $event.stopPropagation()"
|
||||
) Close
|
||||
.sk-panel-content.selectable(ng-if="!ctrl.state.editor")
|
||||
.sk-panel-content.selectable(ng-if="!ctrl.state.componentViewer")
|
||||
.sk-h2 {{ctrl.content.title}}
|
||||
p.normal.sk-p(
|
||||
style="white-space: pre-wrap; font-size: 16px;"
|
||||
) {{ctrl.content.text}}
|
||||
.sk-panel-content.sk-h2(
|
||||
ng-if="ctrl.state.editor"
|
||||
ng-if="ctrl.state.componentViewer"
|
||||
style="height: auto; flex-grow: 0"
|
||||
) {{ctrl.content.title}}
|
||||
component-view.component-view(
|
||||
ng-if="ctrl.state.editor",
|
||||
template-component="ctrl.state.editor",
|
||||
ng-if="ctrl.state.componentViewer",
|
||||
component-viewer="ctrl.state.componentViewer",
|
||||
application='ctrl.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"@standardnotes/features": "1.10.2",
|
||||
"@reach/tooltip": "^0.16.2",
|
||||
"@standardnotes/sncrypto-web": "1.5.3",
|
||||
"@standardnotes/snjs": "2.25.0",
|
||||
"@standardnotes/snjs": "2.29.0",
|
||||
"mobx": "^6.3.5",
|
||||
"mobx-react-lite": "^3.2.2",
|
||||
"preact": "^10.5.15",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
55
yarn.lock
55
yarn.lock
@@ -2597,6 +2597,13 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "3.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.8.3.tgz#6e627c1a1a9ebf91d97f52950d099bf7704382e3"
|
||||
@@ -2604,15 +2611,15 @@
|
||||
dependencies:
|
||||
"@standardnotes/common" "^1.2.1"
|
||||
|
||||
"@standardnotes/common@^1.2.1":
|
||||
"@standardnotes/common@1.2.1", "@standardnotes/common@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.2.1.tgz#9db212db86ccbf08b347da02549b3dbe4bedbb02"
|
||||
integrity sha512-HilBxS50CBlC6TJvy1mrnhGVDzOH63M/Jf+hyMxQ0Vt1nYzpd0iyxVEUrgMh7ZiyO1b9CLnCDED99Jy9rnZWVQ==
|
||||
|
||||
"@standardnotes/domain-events@^2.5.1":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.10.0.tgz#719c430d1736daffcb4233aa3381b58280564dc0"
|
||||
integrity sha512-8jvkhNjoYrXN81RA8Q4vGEKH9R002Y/aEK29GyxmQmijT5+JwlA4f0ySycz5sJxWGULohL1k96RueYPs97hV3g==
|
||||
"@standardnotes/domain-events@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.5.1.tgz#e6433e940ae616683d1c24f76133c70755504c44"
|
||||
integrity sha512-p0VB4Al/ZcVqcj9ztU7TNqzc3jjjG6/U7x9lBW/QURHxpB+PnwJq3kFU5V5JA9QpCOYlXLT71CMERMf/O5QX6g==
|
||||
dependencies:
|
||||
"@standardnotes/auth" "^3.8.1"
|
||||
|
||||
@@ -2624,20 +2631,20 @@
|
||||
"@standardnotes/auth" "3.8.3"
|
||||
"@standardnotes/common" "^1.2.1"
|
||||
|
||||
"@standardnotes/features@^1.10.3":
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.10.3.tgz#f5824342446e69f006ea8ac8916203d1d3992f21"
|
||||
integrity sha512-PU4KthoDr6NL1bOfKnYV1WXYqRu1/IcdkZkJa2LHcYMPduUjDUKO6qRK73dF0+EEI1U+YXY/9rHyfadGwd0Ymg==
|
||||
"@standardnotes/features@1.11.0":
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.11.0.tgz#66e960a20358c5f58b6be4e19226b34df6f4efbf"
|
||||
integrity sha512-KMP60C1lf5C141s5VVOs7mISS1IUCioJfYsbsxtPycx9Q1mgbDB3xJv/GuCK/avsQOiGrB7QN06CQJ7fw4XV3Q==
|
||||
dependencies:
|
||||
"@standardnotes/auth" "3.8.3"
|
||||
"@standardnotes/common" "^1.2.1"
|
||||
|
||||
"@standardnotes/settings@^1.2.1":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.4.0.tgz#b8f43383fa1b469d609f5ac6c2ce571efb8bfe71"
|
||||
integrity sha512-mGptrIaM/3UWOkc9xmzuRRM2A75caX6vyqCeKhyqPdM3ZR/YpYH7I6qYDsO6wpkoF3soD2nRJ6pLV7HBjGdGag==
|
||||
"@standardnotes/settings@^1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.8.0.tgz#d7bd1f35c3b500d12ba73f5f385b1019baae3efc"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.5.2.tgz#be9404689d94f953c68302609a4f76751eaa82cd"
|
||||
integrity sha512-+OQ6gajTcVSHruw33T52MHyBDKL1vRCfQBXQn4tt4+bCfBAe+PFLkEQMHp35bg5twCfg9+wUf2KhmNNSNyBBZw==
|
||||
@@ -2651,17 +2658,17 @@
|
||||
buffer "^6.0.3"
|
||||
libsodium-wrappers "^0.7.9"
|
||||
|
||||
"@standardnotes/snjs@2.25.0":
|
||||
version "2.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.25.0.tgz#742ee451547c1f36d29bb3e58678068d6a455520"
|
||||
integrity sha512-Sb2sZItuxWAFepbNyqGvH8CIC436VirEjAqc0NK9+1CK0wqPLfpCiDBEkTzsQa2UovnoKu/p4tpTrMnx3FvK2A==
|
||||
"@standardnotes/snjs@2.29.0":
|
||||
version "2.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.29.0.tgz#6c7c6ccd983df4a1a5e2063647eb731304002fd9"
|
||||
integrity sha512-Y+GpNiFyJtVr2W3nVbC2zljtXpBlqe3cB4+R1REE0V4hnQBaq/HE6PaUd80TnFj99Kl8lowyH/o4bNV3+CjGgg==
|
||||
dependencies:
|
||||
"@standardnotes/auth" "^3.8.1"
|
||||
"@standardnotes/common" "^1.2.1"
|
||||
"@standardnotes/domain-events" "^2.5.1"
|
||||
"@standardnotes/features" "^1.10.3"
|
||||
"@standardnotes/settings" "^1.2.1"
|
||||
"@standardnotes/sncrypto-common" "^1.5.2"
|
||||
"@standardnotes/auth" "3.8.1"
|
||||
"@standardnotes/common" "1.2.1"
|
||||
"@standardnotes/domain-events" "2.5.1"
|
||||
"@standardnotes/features" "1.11.0"
|
||||
"@standardnotes/settings" "^1.8.0"
|
||||
"@standardnotes/sncrypto-common" "1.5.2"
|
||||
|
||||
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
|
||||
version "5.4.0"
|
||||
|
||||
Reference in New Issue
Block a user