diff --git a/packages/blocks-editor/src/Editor/BlocksEditor.tsx b/packages/blocks-editor/src/Editor/BlocksEditor.tsx index fae06450b..0c87c8173 100644 --- a/packages/blocks-editor/src/Editor/BlocksEditor.tsx +++ b/packages/blocks-editor/src/Editor/BlocksEditor.tsx @@ -18,7 +18,7 @@ import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin'; import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin'; import {LinkPlugin} from '@lexical/react/LexicalLinkPlugin'; import {ListPlugin} from '@lexical/react/LexicalListPlugin'; -import {EditorState, LexicalEditor} from 'lexical'; +import {$getRoot, EditorState, LexicalEditor} from 'lexical'; import HorizontalRulePlugin from '../Lexical/Plugins/HorizontalRulePlugin'; import TwitterPlugin from '../Lexical/Plugins/TwitterPlugin'; import YouTubePlugin from '../Lexical/Plugins/YouTubePlugin'; @@ -28,24 +28,42 @@ import DraggableBlockPlugin from '../Lexical/Plugins/DraggableBlockPlugin'; import CodeHighlightPlugin from '../Lexical/Plugins/CodeHighlightPlugin'; import FloatingTextFormatToolbarPlugin from '../Lexical/Plugins/FloatingTextFormatToolbarPlugin'; import FloatingLinkEditorPlugin from '../Lexical/Plugins/FloatingLinkEditorPlugin'; +import {truncateString} from './Utils'; +import {SuperEditorContentId} from './Constants'; const BlockDragEnabled = false; type BlocksEditorProps = { - onChange: (value: string) => void; + onChange: (value: string, preview: string) => void; className?: string; children: React.ReactNode; + previewLength: number; + spellcheck?: boolean; }; export const BlocksEditor: FunctionComponent = ({ onChange, className, children, + previewLength, + spellcheck, }) => { const handleChange = useCallback( (editorState: EditorState, _editor: LexicalEditor) => { - const stringifiedEditorState = JSON.stringify(editorState.toJSON()); - onChange(stringifiedEditorState); + editorState.read(() => { + const childrenNodes = $getRoot().getAllTextNodes().slice(0, 2); + let previewText = ''; + childrenNodes.forEach((node, index) => { + previewText += node.getTextContent(); + if (index !== childrenNodes.length - 1) { + previewText += '\n'; + } + }); + previewText = truncateString(previewText, previewLength); + + const stringifiedEditorState = JSON.stringify(editorState.toJSON()); + onChange(stringifiedEditorState, previewText); + }); }, [onChange], ); @@ -67,7 +85,9 @@ export const BlocksEditor: FunctionComponent = ({
@@ -85,7 +105,7 @@ export const BlocksEditor: FunctionComponent = ({ ]} /> - + diff --git a/packages/blocks-editor/src/Editor/Constants.ts b/packages/blocks-editor/src/Editor/Constants.ts new file mode 100644 index 000000000..8fc5ec40a --- /dev/null +++ b/packages/blocks-editor/src/Editor/Constants.ts @@ -0,0 +1 @@ +export const SuperEditorContentId = 'super-editor-content'; diff --git a/packages/blocks-editor/src/Editor/Utils.ts b/packages/blocks-editor/src/Editor/Utils.ts new file mode 100644 index 000000000..ae4fec479 --- /dev/null +++ b/packages/blocks-editor/src/Editor/Utils.ts @@ -0,0 +1,7 @@ +export function truncateString(string: string, limit: number) { + if (string.length <= limit) { + return string; + } else { + return string.substring(0, limit) + '...'; + } +} diff --git a/packages/blocks-editor/src/Lexical/Theme/base.scss b/packages/blocks-editor/src/Lexical/Theme/base.scss index f915b9318..b50ba09f5 100644 --- a/packages/blocks-editor/src/Lexical/Theme/base.scss +++ b/packages/blocks-editor/src/Lexical/Theme/base.scss @@ -6,8 +6,6 @@ * */ -@import 'https://fonts.googleapis.com/css?family=Reenie+Beanie'; - body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; diff --git a/packages/blocks-editor/src/Lexical/Theme/editor.scss b/packages/blocks-editor/src/Lexical/Theme/editor.scss index 8ea50f20d..864ed5f96 100644 --- a/packages/blocks-editor/src/Lexical/Theme/editor.scss +++ b/packages/blocks-editor/src/Lexical/Theme/editor.scss @@ -352,22 +352,22 @@ border-radius: 2px; } .Lexical__listItemChecked:before { - border: 1px solid rgb(61, 135, 245); + border: 1px solid var(--sn-stylekit-info-color); border-radius: 2px; - background-color: #3d87f5; + background-color: var(--sn-stylekit-info-color); background-repeat: no-repeat; } .Lexical__listItemChecked:after { content: ''; cursor: pointer; - border-color: #fff; + border-color: var(--sn-stylekit-info-contrast-color); border-style: solid; position: absolute; display: block; - top: 6px; - width: 3px; - left: 7px; - height: 6px; + top: 7px; + width: 5px; + left: 6px; + height: 10px; transform: rotate(45deg); border-width: 0 2px 2px 0; } diff --git a/packages/blocks-editor/src/index.ts b/packages/blocks-editor/src/index.ts index 2ff947f34..c62acbea7 100644 --- a/packages/blocks-editor/src/index.ts +++ b/packages/blocks-editor/src/index.ts @@ -1,2 +1,3 @@ export * from './Editor/BlocksEditor'; export * from './Editor/BlocksEditorComposer'; +export * from './Editor/Constants'; diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 12a55c467..2dbb9c3db 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -366,4 +366,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter return this.mobileWebReceiver.addReactListener(listener) } + + showAccountMenu(): void { + this.getViewControllerManager().accountMenuController.setShow(true) + } } diff --git a/packages/web/src/javascripts/Components/BlockEditor/BlockEditorComponent.tsx b/packages/web/src/javascripts/Components/BlockEditor/BlockEditorComponent.tsx index 1aa2a1af6..27a08b8d2 100644 --- a/packages/web/src/javascripts/Components/BlockEditor/BlockEditorComponent.tsx +++ b/packages/web/src/javascripts/Components/BlockEditor/BlockEditorComponent.tsx @@ -16,7 +16,6 @@ import { NodeObserverPlugin } from './Plugins/NodeObserverPlugin/NodeObserverPlu import { FilesController } from '@/Controllers/FilesController' import FilesControllerProvider from '@/Controllers/FilesControllerProvider' -const StringEllipses = '...' const NotePreviewCharLimit = 160 type Props = { @@ -24,18 +23,21 @@ type Props = { note: SNNote linkingController: LinkingController filesController: FilesController + spellcheck: boolean } -export const BlockEditor: FunctionComponent = ({ note, application, linkingController, filesController }) => { +export const BlockEditor: FunctionComponent = ({ + note, + application, + linkingController, + filesController, + spellcheck, +}) => { const controller = useRef(new BlockEditorController(note, application)) const handleChange = useCallback( - (value: string) => { - const content = value - const truncate = content.length > NotePreviewCharLimit - const substring = content.substring(0, NotePreviewCharLimit) - const previewPlain = substring + (truncate ? StringEllipses : '') - void controller.current.save({ text: content, previewPlain: previewPlain, previewHtml: undefined }) + (value: string, preview: string) => { + void controller.current.save({ text: value, previewPlain: preview, previewHtml: undefined }) }, [controller], ) @@ -51,7 +53,7 @@ export const BlockEditor: FunctionComponent = ({ note, application, linki ) return ( -
+
@@ -59,6 +61,8 @@ export const BlockEditor: FunctionComponent = ({ note, application, linki diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemOption.ts b/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemOption.ts index c536ee12f..8480786f0 100644 --- a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemOption.ts +++ b/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemOption.ts @@ -3,14 +3,15 @@ import { LinkableItem } from '@/Utils/Items/Search/LinkableItem' export class ItemOption extends TypeaheadOption { constructor( - public item: LinkableItem, + public item: LinkableItem | undefined, + public label: string, public options: { keywords?: Array keyboardShortcut?: string onSelect: (queryString: string) => void }, ) { - super(item.title || '') - this.key = item.uuid + super(label || '') + this.key = item?.uuid || label } } diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx b/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx index 940b85b19..760e072da 100644 --- a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx +++ b/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx @@ -1,4 +1,5 @@ import LinkedItemMeta from '@/Components/LinkedItems/LinkedItemMeta' +import { LinkedItemSearchResultsAddTagOption } from '@/Components/LinkedItems/LinkedItemSearchResultsAddTagOption' import { PopoverItemClassNames, PopoverItemSelectedClassNames } from '../ClassNames' import { ItemOption } from './ItemOption' @@ -24,7 +25,14 @@ export function ItemSelectionItemComponent({ index, isSelected, onClick, onMouse onMouseEnter={onMouseEnter} onClick={onClick} > - + {option.item && } + {!option.item && ( + + )} ) } diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx b/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx index ba20b06f0..da39c9754 100644 --- a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx +++ b/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx @@ -46,14 +46,19 @@ export const ItemSelectionPlugin: FunctionComponent = ({ currentNote }) = ) const options = useMemo(() => { - const results = getLinkingSearchResults(queryString || '', application, currentNote, { - returnEmptyIfQueryEmpty: false, - }) + const { linkedItems, unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults( + queryString || '', + application, + currentNote, + { + returnEmptyIfQueryEmpty: false, + }, + ) - const items = [...results.linkedItems, ...results.unlinkedItems] + const items = [...linkedItems, ...unlinkedItems] - return items.map((item) => { - return new ItemOption(item, { + const options = items.map((item) => { + return new ItemOption(item, item.title || '', { onSelect: (_queryString: string) => { void linkingController.linkItems(currentNote, item) if (item.content_type === ContentType.File) { @@ -64,6 +69,19 @@ export const ItemSelectionPlugin: FunctionComponent = ({ currentNote }) = }, }) }) + + if (shouldShowCreateTag) { + options.push( + new ItemOption(undefined, '', { + onSelect: async (queryString: string) => { + const newTag = await linkingController.createAndAddNewTag(queryString || '') + editor.dispatchCommand(INSERT_BUBBLE_COMMAND, newTag.uuid) + }, + }), + ) + } + + return options }, [application, editor, currentNote, queryString, linkingController]) return ( diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx index 504c88dc8..b86a2506c 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx @@ -1,11 +1,12 @@ import { WebApplication } from '@/Application/Application' import { concatenateUint8Arrays } from '@/Utils' -import { FileItem } from '@standardnotes/snjs' +import { ApplicationEvent, FileItem } from '@standardnotes/snjs' import { useEffect, useMemo, useState } from 'react' import Spinner from '@/Components/Spinner/Spinner' import FilePreviewError from './FilePreviewError' import { isFileTypePreviewable } from './isFilePreviewable' import PreviewComponent from './PreviewComponent' +import ProtectedItemOverlay from '../ProtectedItemOverlay/ProtectedItemOverlay' type Props = { application: WebApplication @@ -13,6 +14,8 @@ type Props = { } const FilePreview = ({ file, application }: Props) => { + const [isAuthorized, setIsAuthorized] = useState(application.isAuthorizedToRenderItem(file)) + const isFilePreviewable = useMemo(() => { return isFileTypePreviewable(file.mimeType) }, [file.mimeType]) @@ -22,7 +25,23 @@ const FilePreview = ({ file, application }: Props) => { const [downloadedBytes, setDownloadedBytes] = useState() useEffect(() => { - if (!isFilePreviewable) { + setIsAuthorized(application.isAuthorizedToRenderItem(file)) + }, [file.protected, application, file]) + + useEffect(() => { + const disposer = application.addEventObserver(async (event) => { + if (event === ApplicationEvent.UnprotectedSessionBegan) { + setIsAuthorized(true) + } else if (event === ApplicationEvent.UnprotectedSessionExpired) { + setIsAuthorized(application.isAuthorizedToRenderItem(file)) + } + }) + + return disposer + }, [application, file]) + + useEffect(() => { + if (!isFilePreviewable || !isAuthorized) { setIsDownloading(false) setDownloadProgress(0) setDownloadedBytes(undefined) @@ -55,10 +74,17 @@ const FilePreview = ({ file, application }: Props) => { } void downloadFileForPreview() - }, [application.files, downloadedBytes, file, isFilePreviewable]) + }, [application.files, downloadedBytes, file, isFilePreviewable, isAuthorized]) - if (!application.isAuthorizedToRenderItem(file)) { - return null + if (!isAuthorized) { + return ( + application.protections.authorizeItemAccess(file)} + hasProtectionSources={application.hasProtectionSources()} + /> + ) } return isDownloading ? ( diff --git a/packages/web/src/javascripts/Components/FileView/FileView.tsx b/packages/web/src/javascripts/Components/FileView/FileView.tsx index 5e6ea5626..b1d9ae9ca 100644 --- a/packages/web/src/javascripts/Components/FileView/FileView.tsx +++ b/packages/web/src/javascripts/Components/FileView/FileView.tsx @@ -3,13 +3,14 @@ import { useCallback, useEffect, useState } from 'react' import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay' import FileViewWithoutProtection from './FileViewWithoutProtection' import { FileViewProps } from './FileViewProps' +import { ApplicationEvent } from '@standardnotes/snjs' const FileView = ({ application, viewControllerManager, file }: FileViewProps) => { const [shouldShowProtectedOverlay, setShouldShowProtectedOverlay] = useState(false) useEffect(() => { - viewControllerManager.filesController.setShowProtectedOverlay(file.protected && !application.hasProtectionSources()) - }, [application, file.protected, viewControllerManager.filesController]) + viewControllerManager.filesController.setShowProtectedOverlay(!application.isAuthorizedToRenderItem(file)) + }, [application, file, viewControllerManager.filesController]) useEffect(() => { setShouldShowProtectedOverlay(viewControllerManager.filesController.showProtectedOverlay) @@ -27,9 +28,21 @@ const FileView = ({ application, viewControllerManager, file }: FileViewProps) = } }, [application, file]) + useEffect(() => { + const disposer = application.addEventObserver(async (event) => { + if (event === ApplicationEvent.UnprotectedSessionBegan) { + setShouldShowProtectedOverlay(false) + } else if (event === ApplicationEvent.UnprotectedSessionExpired) { + setShouldShowProtectedOverlay(!application.isAuthorizedToRenderItem(file)) + } + }) + + return disposer + }, [application, file]) + return shouldShowProtectedOverlay ? ( { - const premiumModal = usePremiumModal() + const onClickAddNew = useCallback( + (searchQuery: string) => { + void createAndAddNewTag(searchQuery) + onClickCallback?.() + }, + [createAndAddNewTag, onClickCallback], + ) return (
@@ -37,12 +44,8 @@ const LinkedItemSearchResults = ({ key={result.uuid} className="flex w-full items-center justify-between gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground focus:bg-info-backdrop" onClick={() => { - if (cannotLinkItem) { - premiumModal.activate('Note linking') - } else { - void linkItemToSelectedItem(result) - onClickCallback?.() - } + void linkItemToSelectedItem(result) + onClickCallback?.() }} > @@ -51,19 +54,7 @@ const LinkedItemSearchResults = ({ ) })} {shouldShowCreateTag && ( - + )}
) diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemSearchResultsAddTagOption.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemSearchResultsAddTagOption.tsx new file mode 100644 index 000000000..d6bbb7e23 --- /dev/null +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemSearchResultsAddTagOption.tsx @@ -0,0 +1,42 @@ +import { classNames } from '@/Utils/ConcatenateClassNames' +import Icon from '../Icon/Icon' + +type Props = { + searchQuery: string + onClickCallback: (searchQuery: string) => void + isFocused?: boolean +} + +export const LinkedItemSearchResultsAddTagOption = ({ searchQuery, onClickCallback, isFocused }: Props) => { + return ( + + ) +} diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts b/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts index b9059d2ef..9509272fd 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts @@ -179,7 +179,7 @@ describe('NoteView', () => { application, }) - await noteView.dismissProtectedWarning() + await noteView.authorizeAndDismissProtectedWarning() expect(notesController.setShowProtectedWarning).toHaveBeenCalledWith(false) }) @@ -192,7 +192,7 @@ describe('NoteView', () => { application, }) - await noteView.dismissProtectedWarning() + await noteView.authorizeAndDismissProtectedWarning() expect(notesController.setShowProtectedWarning).not.toHaveBeenCalled() }) @@ -207,7 +207,7 @@ describe('NoteView', () => { application, }) - await noteView.dismissProtectedWarning() + await noteView.authorizeAndDismissProtectedWarning() expect(notesController.setShowProtectedWarning).toHaveBeenCalledWith(false) }) diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index edbcf09ae..9310a217c 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -47,6 +47,7 @@ import { transactionForAssociateComponentWithCurrentNote, transactionForDisassociateComponentWithCurrentNote, } from './TransactionFunctions' +import { SuperEditorContentId } from '@standardnotes/blocks-editor' const MinimumStatusDuration = 400 const TextareaDebounce = 100 @@ -202,7 +203,7 @@ class NoteView extends AbstractComponent { this.statusTimeout = undefined ;(this.onPanelResizeFinish as unknown) = undefined - ;(this.dismissProtectedWarning as unknown) = undefined + ;(this.authorizeAndDismissProtectedWarning as unknown) = undefined ;(this.editorComponentViewerRequestsReload as unknown) = undefined ;(this.onTextAreaChange as unknown) = undefined ;(this.onTitleEnter as unknown) = undefined @@ -452,7 +453,7 @@ class NoteView extends AbstractComponent { } } - dismissProtectedWarning = async () => { + authorizeAndDismissProtectedWarning = async () => { let showNoteContents = true if (this.application.hasProtectionSources()) { @@ -893,6 +894,7 @@ class NoteView extends AbstractComponent { this.removeTrashKeyObserver = this.application.io.addKeyObserver({ key: KeyboardKey.Backspace, notTags: ['INPUT', 'TEXTAREA'], + notElementIds: [SuperEditorContentId], modifiers: [KeyboardModifier.Meta], onKeyDown: () => { this.deleteNote(false).catch(console.error) @@ -984,9 +986,9 @@ class NoteView extends AbstractComponent { if (this.state.showProtectedWarning || !this.application.isAuthorizedToRenderItem(this.note)) { return ( this.application.showAccountMenu()} hasProtectionSources={this.application.hasProtectionSources()} - onViewItem={this.dismissProtectedWarning} + onViewItem={this.authorizeAndDismissProtectedWarning} itemType={'note'} /> ) @@ -1009,6 +1011,7 @@ class NoteView extends AbstractComponent { )} @@ -1155,6 +1158,7 @@ class NoteView extends AbstractComponent { note={this.note} linkingController={this.viewControllerManager.linkingController} filesController={this.viewControllerManager.filesController} + spellcheck={this.state.spellcheck} />
)} diff --git a/packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx b/packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx index 93c7f360a..afaff9f12 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx @@ -1,3 +1,4 @@ +import { FilesController } from '@/Controllers/FilesController' import { LinkingController } from '@/Controllers/LinkingController' import { SNNote } from '@standardnotes/snjs' import { useEffect } from 'react' @@ -6,10 +7,11 @@ import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider type Props = { note: SNNote linkingController: LinkingController + filesController: FilesController noteViewElement: HTMLElement | null } -const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement }: Props) => { +const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement, filesController }: Props) => { const { isDraggingFiles, addDragTarget, removeDragTarget } = useFileDragNDrop() useEffect(() => { @@ -21,6 +23,7 @@ const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement }: Pr callback: (files) => { files.forEach(async (uploadedFile) => { await linkingController.linkItems(note, uploadedFile) + filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid) }) }, }) @@ -31,7 +34,7 @@ const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement }: Pr removeDragTarget(target) } } - }, [addDragTarget, linkingController, note, noteViewElement, removeDragTarget]) + }, [addDragTarget, linkingController, note, noteViewElement, removeDragTarget, filesController]) return isDraggingFiles ? ( // Required to block drag events to editor iframe diff --git a/packages/web/src/javascripts/Components/ProtectedItemOverlay/ProtectedItemOverlay.tsx b/packages/web/src/javascripts/Components/ProtectedItemOverlay/ProtectedItemOverlay.tsx index f7617ec56..7b3083364 100644 --- a/packages/web/src/javascripts/Components/ProtectedItemOverlay/ProtectedItemOverlay.tsx +++ b/packages/web/src/javascripts/Components/ProtectedItemOverlay/ProtectedItemOverlay.tsx @@ -1,21 +1,20 @@ -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import Button from '@/Components/Button/Button' import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton' type Props = { - viewControllerManager: ViewControllerManager + showAccountMenu: () => void onViewItem: () => void hasProtectionSources: boolean itemType: 'note' | 'file' } -const ProtectedItemOverlay = ({ viewControllerManager, onViewItem, hasProtectionSources, itemType }: Props) => { +const ProtectedItemOverlay = ({ showAccountMenu, onViewItem, hasProtectionSources, itemType }: Props) => { const instructionText = hasProtectionSources ? `Authenticate to view this ${itemType}.` : `Add a passcode or create an account to require authentication to view this ${itemType}.` return ( -
+
@@ -29,7 +28,7 @@ const ProtectedItemOverlay = ({ viewControllerManager, onViewItem, hasProtection primary small onClick={() => { - viewControllerManager.accountMenuController.setShow(true) + showAccountMenu() }} > Open account menu diff --git a/packages/web/src/javascripts/Controllers/Abstract/AbstractViewController.ts b/packages/web/src/javascripts/Controllers/Abstract/AbstractViewController.ts index c9c37c199..7344ecc1c 100644 --- a/packages/web/src/javascripts/Controllers/Abstract/AbstractViewController.ts +++ b/packages/web/src/javascripts/Controllers/Abstract/AbstractViewController.ts @@ -12,8 +12,8 @@ export abstract class AbstractViewController { constructor(public application: WebApplication, protected eventBus: InternalEventBus) {} - protected async publishEventSync(name: CrossControllerEvent): Promise { - await this.eventBus.publishSync({ type: name, payload: undefined }, InternalEventPublishStrategy.SEQUENCE) + protected async publishCrossControllerEventSync(name: CrossControllerEvent, data?: unknown): Promise { + await this.eventBus.publishSync({ type: name, payload: data }, InternalEventPublishStrategy.SEQUENCE) } deinit(): void { @@ -38,7 +38,7 @@ export abstract class AbstractViewController { } } - notifyEvent(event: Event, data: EventData): void { + protected notifyEvent(event: Event, data: EventData): void { this.eventObservers.forEach((observer) => observer(event, data)) } } diff --git a/packages/web/src/javascripts/Controllers/CrossControllerEvent.ts b/packages/web/src/javascripts/Controllers/CrossControllerEvent.ts index 064be502c..7baad84fc 100644 --- a/packages/web/src/javascripts/Controllers/CrossControllerEvent.ts +++ b/packages/web/src/javascripts/Controllers/CrossControllerEvent.ts @@ -3,4 +3,5 @@ export enum CrossControllerEvent { ActiveEditorChanged = 'ActiveEditorChanged', HydrateFromPersistedValues = 'HydrateFromPersistedValues', RequestValuePersistence = 'RequestValuePersistence', + DisplayPremiumModal = 'DisplayPremiumModal', } diff --git a/packages/web/src/javascripts/Controllers/FeaturesController.ts b/packages/web/src/javascripts/Controllers/FeaturesController.ts index 157cfab55..b94c9c70b 100644 --- a/packages/web/src/javascripts/Controllers/FeaturesController.ts +++ b/packages/web/src/javascripts/Controllers/FeaturesController.ts @@ -1,8 +1,15 @@ import { WebApplication } from '@/Application/Application' import { destroyAllObjectProperties } from '@/Utils' -import { ApplicationEvent, FeatureIdentifier, FeatureStatus, InternalEventBus } from '@standardnotes/snjs' +import { + ApplicationEvent, + FeatureIdentifier, + FeatureStatus, + InternalEventBus, + InternalEventInterface, +} from '@standardnotes/snjs' import { action, makeObservable, observable, runInAction, when } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' +import { CrossControllerEvent } from './CrossControllerEvent' export class FeaturesController extends AbstractViewController { hasFolders: boolean @@ -30,6 +37,8 @@ export class FeaturesController extends AbstractViewController { this.hasFiles = this.isEntitledToFiles() this.premiumAlertFeatureName = undefined + eventBus.addEventHandler(this, CrossControllerEvent.DisplayPremiumModal) + makeObservable(this, { hasFolders: observable, hasSmartViews: observable, @@ -58,6 +67,13 @@ export class FeaturesController extends AbstractViewController { ) } + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === CrossControllerEvent.DisplayPremiumModal) { + const payload = event.payload as { featureName: string } + void this.showPremiumAlert(payload.featureName) + } + } + public async showPremiumAlert(featureName: string): Promise { this.premiumAlertFeatureName = featureName return when(() => this.premiumAlertFeatureName === undefined) diff --git a/packages/web/src/javascripts/Controllers/FilesController.ts b/packages/web/src/javascripts/Controllers/FilesController.ts index 53bf88f16..685b85b12 100644 --- a/packages/web/src/javascripts/Controllers/FilesController.ts +++ b/packages/web/src/javascripts/Controllers/FilesController.ts @@ -139,7 +139,7 @@ export class FilesController extends AbstractViewController { + attachFileToSelectedNote = async (file: FileItem) => { const note = this.notesController.firstSelectedNote if (!note) { addToast({ @@ -207,7 +207,7 @@ export class FilesController extends AbstractViewController { const title = Strings.trashItemsTitle const text = files.length === 1 ? StringUtils.deleteFile(files[0].name) : Strings.deleteMultipleFiles diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts index c4ff7db96..87455431a 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts @@ -259,7 +259,7 @@ export class ItemListController extends AbstractViewController implements Intern this.linkingController.reloadAllLinks() - await this.publishEventSync(CrossControllerEvent.ActiveEditorChanged) + await this.publishCrossControllerEventSync(CrossControllerEvent.ActiveEditorChanged) } async openFile(fileUuid: string): Promise { diff --git a/packages/web/src/javascripts/Controllers/LinkingController.tsx b/packages/web/src/javascripts/Controllers/LinkingController.tsx index 3ec067147..7cee1e5c9 100644 --- a/packages/web/src/javascripts/Controllers/LinkingController.tsx +++ b/packages/web/src/javascripts/Controllers/LinkingController.tsx @@ -20,6 +20,7 @@ import { } from '@standardnotes/snjs' import { action, computed, makeObservable, observable } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' +import { CrossControllerEvent } from './CrossControllerEvent' import { FilesController } from './FilesController' import { ItemListController } from './ItemList/ItemListController' import { NavigationController } from './Navigation/NavigationController' @@ -262,24 +263,35 @@ export class LinkingController extends AbstractViewController { this.reloadAllLinks() } - linkItemToSelectedItem = async (itemToLink: LinkableItem) => { + linkItemToSelectedItem = async (itemToLink: LinkableItem): Promise => { + const cannotLinkItem = !this.isEntitledToNoteLinking && itemToLink instanceof SNNote + if (cannotLinkItem) { + void this.publishCrossControllerEventSync(CrossControllerEvent.DisplayPremiumModal, { + featureName: 'Note linking', + }) + return false + } + await this.ensureActiveItemIsInserted() const activeItem = this.activeItem if (!activeItem) { - return + return false } await this.linkItems(activeItem, itemToLink) + return true } - createAndAddNewTag = async (title: string) => { + createAndAddNewTag = async (title: string): Promise => { await this.ensureActiveItemIsInserted() const activeItem = this.activeItem const newTag = await this.application.mutator.findOrCreateTag(title) if (activeItem) { await this.addTagToItem(newTag, activeItem) } + + return newTag } addTagToItem = async (tag: SNTag, item: FileItem | SNNote) => {