feat: component viewer (#781)

* wip: component viewer

* feat: get component status from component viewer

* fix: remove unused property

* chore(deps): snjs 2.29.0

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

View File

@@ -51,7 +51,6 @@ import {
import {
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())

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '=',
});

View File

@@ -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';

View File

@@ -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 (

View File

@@ -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);
}
};

View File

@@ -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);

View File

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

View File

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

View File

@@ -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: '=',
};
}
}

View File

@@ -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: '=',
};
}
}

View File

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

View File

@@ -1,12 +1,13 @@
import { PreferencesGroup, PreferencesSegment } from "@/preferences/components";
import { 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>
);
});
}
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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[] = [];

View File

@@ -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;

View File

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

View File

@@ -3,37 +3,36 @@ import {
ContentType,
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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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 */
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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'
)

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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

View File

@@ -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) => {

View File

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

View File

@@ -3,7 +3,7 @@
.sn-component
.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',

View File

@@ -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'
)

View File

@@ -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",

View File

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

View File

@@ -2597,6 +2597,13 @@
dependencies:
"@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"