fix: super note improvements (#1991)

* fix: super note previews

* fix; checkmark size

* fix: top padding

* fix: prevent delete shortcut

* fix: spellcheck control

* fix: only embed file if uploaded to current note

* fix: ability to create new tag from editor autocomplete

* feat: protected file embed handling

* fix: event payload
This commit is contained in:
Mo
2022-11-10 09:35:53 -06:00
committed by GitHub
parent 0cbc23f740
commit 2dbc89594e
25 changed files with 261 additions and 90 deletions

View File

@@ -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<BlocksEditorProps> = ({
onChange,
className,
children,
previewLength,
spellcheck,
}) => {
const handleChange = useCallback(
(editorState: EditorState, _editor: LexicalEditor) => {
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);
onChange(stringifiedEditorState, previewText);
});
},
[onChange],
);
@@ -67,7 +85,9 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
<div id="blocks-editor" className="editor-scroller">
<div className="editor" ref={onRef}>
<ContentEditable
id={SuperEditorContentId}
className={`ContentEditable__root ${className}`}
spellCheck={spellcheck}
/>
</div>
</div>
@@ -85,7 +105,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
]}
/>
<TablePlugin />
<OnChangePlugin onChange={handleChange} />
<OnChangePlugin onChange={handleChange} ignoreSelectionChange={true} />
<HistoryPlugin />
<HorizontalRulePlugin />
<AutoFocusPlugin />

View File

@@ -0,0 +1 @@
export const SuperEditorContentId = 'super-editor-content';

View File

@@ -0,0 +1,7 @@
export function truncateString(string: string, limit: number) {
if (string.length <= limit) {
return string;
} else {
return string.substring(0, limit) + '...';
}
}

View File

@@ -6,8 +6,6 @@
*
*/
@import 'https://fonts.googleapis.com/css?family=Reenie+Beanie';
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

View File

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

View File

@@ -1,2 +1,3 @@
export * from './Editor/BlocksEditor';
export * from './Editor/BlocksEditorComposer';
export * from './Editor/Constants';

View File

@@ -366,4 +366,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return this.mobileWebReceiver.addReactListener(listener)
}
showAccountMenu(): void {
this.getViewControllerManager().accountMenuController.setShow(true)
}
}

View File

@@ -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<Props> = ({ note, application, linkingController, filesController }) => {
export const BlockEditor: FunctionComponent<Props> = ({
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<Props> = ({ note, application, linki
)
return (
<div className="relative h-full w-full p-5">
<div className="relative h-full w-full px-5 py-4">
<ErrorBoundary>
<LinkingControllerProvider controller={linkingController}>
<FilesControllerProvider controller={filesController}>
@@ -59,6 +61,8 @@ export const BlockEditor: FunctionComponent<Props> = ({ note, application, linki
<BlocksEditor
onChange={handleChange}
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
previewLength={NotePreviewCharLimit}
spellcheck={spellcheck}
>
<ItemSelectionPlugin currentNote={note} />
<FilePlugin />

View File

@@ -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<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
},
) {
super(item.title || '')
this.key = item.uuid
super(label || '')
this.key = item?.uuid || label
}
}

View File

@@ -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}
>
<LinkedItemMeta item={option.item} searchQuery={searchQuery} />
{option.item && <LinkedItemMeta item={option.item} searchQuery={searchQuery} />}
{!option.item && (
<LinkedItemSearchResultsAddTagOption
searchQuery={searchQuery}
onClickCallback={onClick}
isFocused={isSelected}
/>
)}
</li>
)
}

View File

@@ -46,14 +46,19 @@ export const ItemSelectionPlugin: FunctionComponent<Props> = ({ currentNote }) =
)
const options = useMemo(() => {
const results = getLinkingSearchResults(queryString || '', application, currentNote, {
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<Props> = ({ 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 (

View File

@@ -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<Uint8Array>()
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 (
<ProtectedItemOverlay
showAccountMenu={application.showAccountMenu}
itemType={'file'}
onViewItem={() => application.protections.authorizeItemAccess(file)}
hasProtectionSources={application.hasProtectionSources()}
/>
)
}
return isDownloading ? (

View File

@@ -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 ? (
<ProtectedItemOverlay
viewControllerManager={viewControllerManager}
showAccountMenu={application.showAccountMenu}
hasProtectionSources={application.hasProtectionSources()}
onViewItem={dismissProtectedOverlay}
itemType={'file'}

View File

@@ -1,11 +1,12 @@
import { LinkingController } from '@/Controllers/LinkingController'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { observer } from 'mobx-react-lite'
import { SNNote } from '@standardnotes/snjs'
import Icon from '../Icon/Icon'
import { PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import LinkedItemMeta from './LinkedItemMeta'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { LinkedItemSearchResultsAddTagOption } from './LinkedItemSearchResultsAddTagOption'
import { useCallback } from 'react'
type Props = {
createAndAddNewTag: LinkingController['createAndAddNewTag']
@@ -26,7 +27,13 @@ const LinkedItemSearchResults = ({
onClickCallback,
isEntitledToNoteLinking,
}: Props) => {
const premiumModal = usePremiumModal()
const onClickAddNew = useCallback(
(searchQuery: string) => {
void createAndAddNewTag(searchQuery)
onClickCallback?.()
},
[createAndAddNewTag, onClickCallback],
)
return (
<div className="my-1">
@@ -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?.()
}
}}
>
<LinkedItemMeta item={result} searchQuery={searchQuery} />
@@ -51,19 +54,7 @@ const LinkedItemSearchResults = ({
)
})}
{shouldShowCreateTag && (
<button
className="group flex w-full items-center gap-2 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground focus:bg-info-backdrop"
onClick={() => {
void createAndAddNewTag(searchQuery)
onClickCallback?.()
}}
>
<span className="flex-shrink-0 align-middle">Create &amp; add tag</span>{' '}
<span className="inline-flex min-w-0 items-center gap-1 rounded bg-contrast py-1 pl-1 pr-2 align-middle text-xs text-text group-hover:bg-info group-hover:text-info-contrast">
<Icon type="hashtag" className="flex-shrink-0 text-info group-hover:text-info-contrast" size="small" />
<span className="min-w-0 overflow-hidden text-ellipsis">{searchQuery}</span>
</span>
</button>
<LinkedItemSearchResultsAddTagOption searchQuery={searchQuery} onClickCallback={onClickAddNew} />
)}
</div>
)

View File

@@ -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 (
<button
className={classNames(
'group flex w-full items-center gap-2 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground',
'focus:bg-info-backdrop',
isFocused ? 'bg-contrast bg-info-backdrop text-foreground' : '',
)}
onClick={() => {
onClickCallback(searchQuery)
}}
>
<span className="flex-shrink-0 align-middle text-sm lg:text-xs">Create &amp; add tag</span>{' '}
<span
className={classNames(
'inline-flex min-w-0 items-center gap-1 rounded py-1 pl-1 pr-2 align-middle text-xs ',
'group-hover:bg-info group-hover:text-info-contrast',
isFocused ? 'bg-info text-info-contrast' : 'bg-contrast text-text',
)}
>
<Icon
type="hashtag"
className={classNames(
'flex-shrink-0 group-hover:text-info-contrast',
isFocused ? 'text-info-contrast' : 'text-info',
)}
size="small"
/>
<span className="min-w-0 overflow-hidden text-ellipsis">{searchQuery}</span>
</span>
</button>
)
}

View File

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

View File

@@ -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<NoteViewProps, State> {
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<NoteViewProps, State> {
}
}
dismissProtectedWarning = async () => {
authorizeAndDismissProtectedWarning = async () => {
let showNoteContents = true
if (this.application.hasProtectionSources()) {
@@ -893,6 +894,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
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<NoteViewProps, State> {
if (this.state.showProtectedWarning || !this.application.isAuthorizedToRenderItem(this.note)) {
return (
<ProtectedItemOverlay
viewControllerManager={this.viewControllerManager}
showAccountMenu={() => this.application.showAccountMenu()}
hasProtectionSources={this.application.hasProtectionSources()}
onViewItem={this.dismissProtectedWarning}
onViewItem={this.authorizeAndDismissProtectedWarning}
itemType={'note'}
/>
)
@@ -1009,6 +1011,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
<NoteViewFileDropTarget
note={this.note}
linkingController={this.viewControllerManager.linkingController}
filesController={this.viewControllerManager.filesController}
noteViewElement={this.noteViewElementRef.current}
/>
)}
@@ -1155,6 +1158,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
note={this.note}
linkingController={this.viewControllerManager.linkingController}
filesController={this.viewControllerManager.filesController}
spellcheck={this.state.spellcheck}
/>
</div>
)}

View File

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

View File

@@ -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 (
<div aria-label="Protected overlay" className="section editor sn-component">
<div aria-label="Protected overlay" className="section editor sn-component p-5">
<div className="flex h-full flex-grow flex-col justify-center md:flex-row md:items-center">
<div className="mb-auto p-4 md:hidden">
<MobileItemsListButton />
@@ -29,7 +28,7 @@ const ProtectedItemOverlay = ({ viewControllerManager, onViewItem, hasProtection
primary
small
onClick={() => {
viewControllerManager.accountMenuController.setShow(true)
showAccountMenu()
}}
>
Open account menu

View File

@@ -12,8 +12,8 @@ export abstract class AbstractViewController<Event = void, EventData = void> {
constructor(public application: WebApplication, protected eventBus: InternalEventBus) {}
protected async publishEventSync(name: CrossControllerEvent): Promise<void> {
await this.eventBus.publishSync({ type: name, payload: undefined }, InternalEventPublishStrategy.SEQUENCE)
protected async publishCrossControllerEventSync(name: CrossControllerEvent, data?: unknown): Promise<void> {
await this.eventBus.publishSync({ type: name, payload: data }, InternalEventPublishStrategy.SEQUENCE)
}
deinit(): void {
@@ -38,7 +38,7 @@ export abstract class AbstractViewController<Event = void, EventData = void> {
}
}
notifyEvent(event: Event, data: EventData): void {
protected notifyEvent(event: Event, data: EventData): void {
this.eventObservers.forEach((observer) => observer(event, data))
}
}

View File

@@ -3,4 +3,5 @@ export enum CrossControllerEvent {
ActiveEditorChanged = 'ActiveEditorChanged',
HydrateFromPersistedValues = 'HydrateFromPersistedValues',
RequestValuePersistence = 'RequestValuePersistence',
DisplayPremiumModal = 'DisplayPremiumModal',
}

View File

@@ -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<void> {
if (event.type === CrossControllerEvent.DisplayPremiumModal) {
const payload = event.payload as { featureName: string }
void this.showPremiumAlert(payload.featureName)
}
}
public async showPremiumAlert(featureName: string): Promise<void> {
this.premiumAlertFeatureName = featureName
return when(() => this.premiumAlertFeatureName === undefined)

View File

@@ -139,7 +139,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
}
}
attachFileToNote = async (file: FileItem) => {
attachFileToSelectedNote = async (file: FileItem) => {
const note = this.notesController.firstSelectedNote
if (!note) {
addToast({
@@ -207,7 +207,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
switch (action.type) {
case PopoverFileItemActionType.AttachFileToNote:
await this.attachFileToNote(file)
await this.attachFileToSelectedNote(file)
break
case PopoverFileItemActionType.DetachFileToNote:
await this.detachFileFromNote(file)
@@ -398,10 +398,6 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
type: ToastType.Success,
message: `Uploaded file "${uploadedFile.name}"`,
})
this.notifyEvent(FilesControllerEvent.FileUploadedToNote, {
[FilesControllerEvent.FileUploadedToNote]: { uuid: uploadedFile.uuid },
})
}
return uploadedFiles
@@ -420,6 +416,12 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
return undefined
}
notifyObserversOfUploadedFileLinkingToCurrentNote(fileUuid: string) {
this.notifyEvent(FilesControllerEvent.FileUploadedToNote, {
[FilesControllerEvent.FileUploadedToNote]: { uuid: fileUuid },
})
}
deleteFilesPermanently = async (files: FileItem[]) => {
const title = Strings.trashItemsTitle
const text = files.length === 1 ? StringUtils.deleteFile(files[0].name) : Strings.deleteMultipleFiles

View File

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

View File

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