feat: Editing large notes (greater than 1.5MB) will result in more optimized syncing, in which changes are saved locally immediately, but sync with the server less frequently (roughly every 30 seconds rather than after every change). (#2768)

This commit is contained in:
Aman Harwara
2024-01-20 15:22:09 +05:30
committed by GitHub
parent c6060aaab3
commit 396ee3f449
32 changed files with 408 additions and 163 deletions

View File

@@ -168,7 +168,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
useEffect(() => {
const removeObserver = application.addWebEventObserver(async (eventName) => {
if (eventName === WebAppEvent.WindowDidFocus) {
if (eventName === WebAppEvent.WindowDidFocus || eventName === WebAppEvent.WindowDidBlur) {
if (!(await application.protections.isLocked())) {
application.sync.sync().catch(console.error)
}

View File

@@ -11,10 +11,11 @@ import { NoteType, noteTypeForEditorIdentifier } from '@standardnotes/snjs'
type Props = {
noteViewController?: NoteViewController
onClick?: () => void
onClickPreprocessing?: () => Promise<void>
}
const ChangeEditorButton: FunctionComponent<Props> = ({ noteViewController, onClickPreprocessing }: Props) => {
const ChangeEditorButton: FunctionComponent<Props> = ({ noteViewController, onClick, onClickPreprocessing }: Props) => {
const application = useApplication()
const note = application.notesController.firstSelectedNote
@@ -48,7 +49,10 @@ const ChangeEditorButton: FunctionComponent<Props> = ({ noteViewController, onCl
await onClickPreprocessing()
}
setIsOpen(willMenuOpen)
}, [onClickPreprocessing, isOpen])
if (onClick) {
onClick()
}
}, [isOpen, onClickPreprocessing, onClick])
useEffect(() => {
return application.keyboardService.addCommandHandler({

View File

@@ -70,7 +70,7 @@ class Footer extends AbstractComponent<Props, State> {
case WebAppEvent.NewUpdateAvailable:
this.onNewUpdateAvailable()
break
case WebAppEvent.EditorFocused:
case WebAppEvent.EditorDidFocus:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((data as any).eventSource === EditorEventSource.UserInteraction) {
this.closeAccountMenu()

View File

@@ -84,6 +84,7 @@ export const IconNameToSvgMapping = {
bold: icons.BoldIcon,
camera: icons.CameraIcon,
check: icons.CheckIcon,
clock: icons.ClockIcon,
close: icons.CloseIcon,
code: icons.CodeIcon,
comment: icons.FeedbackIcon,

View File

@@ -7,10 +7,11 @@ import LinkedItemsPanel from './LinkedItemsPanel'
type Props = {
linkingController: LinkingController
onClick?: () => void
onClickPreprocessing?: () => Promise<void>
}
const LinkedItemsButton = ({ linkingController, onClickPreprocessing }: Props) => {
const LinkedItemsButton = ({ linkingController, onClick, onClickPreprocessing }: Props) => {
const { activeItem, isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController
const buttonRef = useRef<HTMLButtonElement>(null)
@@ -20,7 +21,10 @@ const LinkedItemsButton = ({ linkingController, onClickPreprocessing }: Props) =
await onClickPreprocessing()
}
setIsLinkingPanelOpen(willMenuOpen)
}, [isLinkingPanelOpen, onClickPreprocessing, setIsLinkingPanelOpen])
if (onClick) {
onClick()
}
}, [isLinkingPanelOpen, onClick, onClickPreprocessing, setIsLinkingPanelOpen])
if (!activeItem) {
return null

View File

@@ -1,5 +1,8 @@
import { MILLISECONDS_IN_A_SECOND } from '@/Constants/Constants'
export const EditorSaveTimeoutDebounce = {
Desktop: 350,
ImmediateChange: 100,
NativeMobileWeb: 700,
LargeNote: 60 * MILLISECONDS_IN_A_SECOND,
}

View File

@@ -104,6 +104,9 @@ export class ItemGroupController {
controller: NoteViewController | FileViewController,
{ notify = true }: { notify: boolean } = { notify: true },
): void {
if (controller instanceof NoteViewController) {
controller.syncOnlyIfLargeNote()
}
controller.deinit()
removeFromArray(this.itemControllers, controller)

View File

@@ -28,6 +28,9 @@ describe('note view controller', () => {
createTemplateItem: jest.fn().mockReturnValue({} as SNNote),
} as unknown as jest.Mocked<ItemManagerInterface>,
mutator: {} as jest.Mocked<MutatorClientInterface>,
sessions: {
isSignedIn: jest.fn().mockReturnValue(true),
},
} as unknown as jest.Mocked<WebApplication>
application.isNativeMobileWeb = jest.fn().mockReturnValue(false)

View File

@@ -25,6 +25,7 @@ import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerO
import { log, LoggingDomain } from '@/Logging'
import { NoteSaveFunctionParams, NoteSyncController } from '../../../Controllers/NoteSyncController'
import { IsNativeMobileWeb } from '@standardnotes/ui-services'
import { NoteStatus } from '../NoteStatusIndicator'
export type EditorValues = {
title: string
@@ -219,4 +220,28 @@ export class NoteViewController implements ItemViewControllerInterface {
await this.syncController.saveAndAwaitLocalPropagation(params)
}
public get syncStatus(): NoteStatus | undefined {
return this.syncController.status
}
public showSavingStatus(): void {
this.syncController.showSavingStatus()
}
public showAllChangesSavedStatus(): void {
this.syncController.showAllChangesSavedStatus()
}
public showErrorSyncStatus(error?: NoteStatus): void {
this.syncController.showErrorStatus(error)
}
public syncNow(): void {
this.sync.sync().catch(console.error)
}
public syncOnlyIfLargeNote(): void {
this.syncController.syncOnlyIfLargeNote()
}
}

View File

@@ -1,63 +1,77 @@
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@standardnotes/utils'
import { ReactNode, useCallback, useState } from 'react'
import { IconType, PrefKey, PrefDefaults } from '@standardnotes/snjs'
import { ReactNode, useCallback, useRef, useState } from 'react'
import { IconType, PrefKey, PrefDefaults, SNNote } from '@standardnotes/snjs'
import Icon from '../Icon/Icon'
import { useApplication } from '../ApplicationProvider'
import { observer } from 'mobx-react-lite'
import { VisuallyHidden } from '@ariakit/react'
import Button from '../Button/Button'
import Popover from '../Popover/Popover'
import { getRelativeTimeString } from '@/Utils/GetRelativeTimeString'
export type NoteStatus = {
type: 'saving' | 'saved' | 'error'
type: 'saving' | 'saved' | 'error' | 'waiting'
message: string
desc?: string
description?: ReactNode
}
const IndicatorWithTooltip = ({
className,
onClick,
onBlur,
icon,
isTooltipVisible,
setIsTooltipVisible,
children,
animateIcon = false,
}: {
className: string
onClick: () => void
onBlur: () => void
icon: IconType
isTooltipVisible: boolean
setIsTooltipVisible: React.Dispatch<React.SetStateAction<boolean>>
children: ReactNode
animateIcon?: boolean
}) => (
<div className="note-status-tooltip-container relative">
<button
className={classNames('peer flex h-5 w-5 items-center justify-center rounded-full', className)}
onClick={onClick}
onBlur={onBlur}
aria-describedby={ElementIds.NoteStatusTooltip}
>
<Icon className={animateIcon ? 'animate-spin' : ''} type={icon} size="small" />
<span className="sr-only">Note sync status</span>
</button>
<div
id={ElementIds.NoteStatusTooltip}
className={classNames(
isTooltipVisible ? '' : 'hidden',
'absolute right-0 top-full min-w-[90vw] translate-x-2 translate-y-1 select-none rounded border border-border',
'bg-default px-3 py-1.5 text-left peer-hover:block peer-focus:block md:min-w-max',
)}
>
{children}
}) => {
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<div className="note-status-tooltip-container">
<button
className={classNames('peer flex h-5 w-5 cursor-pointer items-center justify-center rounded-full', className)}
onClick={onClick}
ref={buttonRef}
>
<Icon className={animateIcon ? 'animate-spin' : ''} type={icon} size="small" />
<VisuallyHidden>Note sync status</VisuallyHidden>
</button>
<Popover
title="Note sync status"
open={isTooltipVisible}
togglePopover={() => setIsTooltipVisible((visible) => !visible)}
className="px-3 py-2"
containerClassName="!min-w-0 !w-auto max-w-[90vw]"
anchorElement={buttonRef}
side="bottom"
align="center"
offset={6}
disableMobileFullscreenTakeover
disableApplyingMobileWidth
>
{children}
</Popover>
</div>
</div>
)
)
}
type Props = {
note: SNNote
status: NoteStatus | undefined
syncTakingTooLong: boolean
updateSavingIndicator?: boolean
}
const NoteStatusIndicator = ({
note,
status,
syncTakingTooLong,
updateSavingIndicator = PrefDefaults[PrefKey.UpdateSavingStatusIndicator],
@@ -65,7 +79,9 @@ const NoteStatusIndicator = ({
const application = useApplication()
const [isTooltipVisible, setIsTooltipVisible] = useState(false)
const onBlur = () => setIsTooltipVisible(false)
const toggleTooltip = useCallback(() => {
setIsTooltipVisible((visible) => !visible)
}, [])
const toggleShowPreference = useCallback(() => {
void application.setPreference(PrefKey.UpdateSavingStatusIndicator, !updateSavingIndicator)
@@ -79,13 +95,13 @@ const NoteStatusIndicator = ({
return (
<IndicatorWithTooltip
className="bg-danger text-danger-contrast"
onClick={toggleShowPreference}
onBlur={onBlur}
onClick={toggleTooltip}
icon="warning"
isTooltipVisible={isTooltipVisible}
setIsTooltipVisible={setIsTooltipVisible}
>
<div className="text-sm font-bold text-danger">{status.message}</div>
{status.desc && <div className="mt-0.5">{status.desc}</div>}
{status.description && <div className="mt-0.5">{status.description}</div>}
</IndicatorWithTooltip>
)
}
@@ -94,15 +110,15 @@ const NoteStatusIndicator = ({
return (
<IndicatorWithTooltip
className="bg-warning text-warning-contrast"
onClick={toggleShowPreference}
onBlur={onBlur}
onClick={toggleTooltip}
icon={status && status.type === 'saving' ? 'sync' : 'warning'}
isTooltipVisible={isTooltipVisible}
setIsTooltipVisible={setIsTooltipVisible}
>
{status ? (
<>
<div className="text-sm font-bold text-warning">{status.message}</div>
{status.desc && <div className="mt-0.5">{status.desc}</div>}
{status.description && <div className="mt-0.5">{status.description}</div>}
</>
) : (
<div className="text-sm font-bold text-warning">Sync taking too long</div>
@@ -117,15 +133,35 @@ const NoteStatusIndicator = ({
className={classNames(
status.type === 'saving' && 'bg-contrast',
status.type === 'saved' && 'bg-success text-success-contrast',
status.type === 'waiting' && 'bg-warning text-warning-contrast',
)}
onClick={toggleShowPreference}
onBlur={onBlur}
icon={status.type === 'saving' ? 'sync' : 'check'}
onClick={toggleTooltip}
icon={status.type === 'saving' ? 'sync' : status.type === 'waiting' ? 'clock' : 'check'}
animateIcon={status.type === 'saving'}
isTooltipVisible={isTooltipVisible}
setIsTooltipVisible={setIsTooltipVisible}
>
<div className="text-sm font-bold">{status.message}</div>
{status.desc && <div className="mt-0.5">{status.desc}</div>}
{status.description && <div className="mt-0.5">{status.description}</div>}
{status.type === 'waiting' && note.lastSyncEnd && (
<div className="mt-0.5">Last synced {getRelativeTimeString(note.lastSyncEnd)}</div>
)}
{status.type === 'waiting' ? (
<Button
small
className="mt-1"
onClick={() => {
application.sync.sync().catch(console.error)
toggleTooltip()
}}
>
Sync now
</Button>
) : (
<Button small className="mt-1" onClick={toggleShowPreference}>
Disable status updates
</Button>
)}
</IndicatorWithTooltip>
)
}
@@ -133,15 +169,17 @@ const NoteStatusIndicator = ({
return (
<IndicatorWithTooltip
className="bg-contrast text-passive-1"
onClick={toggleShowPreference}
onBlur={onBlur}
onClick={toggleTooltip}
icon="info"
isTooltipVisible={isTooltipVisible}
setIsTooltipVisible={setIsTooltipVisible}
>
<div className="text-sm font-bold">Note status updates are disabled</div>
<div className="mt-0.5">Click to enable.</div>
<Button small className="mt-1" onClick={toggleShowPreference}>
Enable status updates
</Button>
</IndicatorWithTooltip>
)
}
export default NoteStatusIndicator
export default observer(NoteStatusIndicator)

View File

@@ -54,8 +54,6 @@ import ModalOverlay from '../Modal/ModalOverlay'
import NoteConflictResolutionModal from './NoteConflictResolutionModal/NoteConflictResolutionModal'
import Icon from '../Icon/Icon'
const MinimumStatusDuration = 400
function sortAlphabetically(array: ComponentInterface[]): ComponentInterface[] {
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
}
@@ -83,6 +81,7 @@ type State = {
updateSavingIndicator?: boolean
editorFeatureIdentifier?: string
noteType?: NoteType
focusModeEnabled?: boolean
conflictedNotes: SNNote[]
showConflictResolutionModal: boolean
@@ -91,7 +90,6 @@ type State = {
class NoteView extends AbstractComponent<NoteViewProps, State> {
readonly controller!: NoteViewController
private statusTimeout?: NodeJS.Timeout
onEditorComponentLoad?: () => void
private removeTrashKeyObserver?: () => void
@@ -167,8 +165,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
this.onEditorComponentLoad = undefined
this.statusTimeout = undefined
;(this.onPanelResizeFinish as unknown) = undefined
;(this.authorizeAndDismissProtectedWarning as unknown) = undefined
;(this.editorComponentViewerRequestsReload as unknown) = undefined
@@ -235,9 +231,22 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
})
this.autorun(() => {
const syncStatus = this.controller.syncStatus
const isFocusModeEnabled = this.application.paneController.focusModeEnabled
const didFocusModeChange = this.state.focusModeEnabled !== isFocusModeEnabled
this.setState({
showProtectedWarning: this.application.notesController.showProtectedWarning,
noteStatus: syncStatus,
saveError: syncStatus?.type === 'error',
syncTakingTooLong: false,
focusModeEnabled: isFocusModeEnabled,
})
if (!isFocusModeEnabled && didFocusModeChange) {
this.controller.syncOnlyIfLargeNote()
}
})
this.reloadEditorComponent().catch(console.error)
@@ -327,16 +336,18 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
}
if (note.lastSyncBegan || note.dirty) {
const currentStatus = this.controller.syncStatus
const isWaitingToSyncLargeNote = currentStatus?.type === 'waiting'
if (note.lastSyncEnd) {
const shouldShowSavingStatus = note.lastSyncBegan && note.lastSyncBegan.getTime() > note.lastSyncEnd.getTime()
const hasStartedNewSync = note.lastSyncBegan && note.lastSyncBegan.getTime() > note.lastSyncEnd.getTime()
const shouldShowSavedStatus = note.lastSyncBegan && note.lastSyncEnd.getTime() > note.lastSyncBegan.getTime()
if (note.dirty || shouldShowSavingStatus) {
this.showSavingStatus()
} else if (this.state.noteStatus && shouldShowSavedStatus) {
this.showAllChangesSavedStatus()
if (hasStartedNewSync) {
this.controller.showSavingStatus()
} else if (this.state.noteStatus && shouldShowSavedStatus && !isWaitingToSyncLargeNote) {
this.controller.showAllChangesSavedStatus()
}
} else {
this.showSavingStatus()
} else if (note.lastSyncBegan) {
this.controller.showSavingStatus()
}
}
}
@@ -372,7 +383,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
const isInErrorState = this.state.saveError
/** if we're still dirty, don't change status, a sync is likely upcoming. */
if (!this.note.dirty && isInErrorState) {
this.showAllChangesSavedStatus()
this.controller.showAllChangesSavedStatus()
}
break
}
@@ -383,14 +394,14 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
* and we don't want to display an error here.
*/
if (this.note.dirty) {
this.showErrorStatus()
this.controller.showErrorSyncStatus()
}
break
case ApplicationEvent.LocalDatabaseWriteError:
this.showErrorStatus({
this.controller.showErrorSyncStatus({
type: 'error',
message: 'Offline Saving Issue',
desc: 'Changes not saved',
description: 'Changes not saved',
})
break
case ApplicationEvent.UnprotectedSessionBegan: {
@@ -551,59 +562,6 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
return this.application.actions.extensionsInContextOfItem(this.note).length > 0
}
showSavingStatus() {
this.setStatus({ type: 'saving', message: 'Saving…' }, false)
}
showAllChangesSavedStatus() {
this.setState({
saveError: false,
syncTakingTooLong: false,
})
this.setStatus({
type: 'saved',
message: 'All changes saved' + (this.application.sessions.isSignedOut() ? ' offline' : ''),
})
}
showErrorStatus(error?: NoteStatus) {
if (!error) {
error = {
type: 'error',
message: 'Sync Unreachable',
desc: 'Changes saved offline',
}
}
this.setState({
saveError: true,
syncTakingTooLong: false,
})
this.setStatus(error)
}
setStatus(status: NoteStatus, wait = true) {
if (this.statusTimeout) {
clearTimeout(this.statusTimeout)
}
if (wait) {
this.statusTimeout = setTimeout(() => {
this.setState({
noteStatus: status,
})
}, MinimumStatusDuration)
} else {
this.setState({
noteStatus: status,
})
}
}
cancelPendingSetStatus() {
if (this.statusTimeout) {
clearTimeout(this.statusTimeout)
}
}
onTitleEnter: KeyboardEventHandler<HTMLInputElement> = ({ key, currentTarget }) => {
if (key !== KeyboardKey.Enter) {
return
@@ -839,6 +797,10 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
}))
}
triggerSyncOnAction = () => {
this.controller.syncNow()
}
override render() {
if (this.controller.dealloced) {
return null
@@ -923,6 +885,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
/>
</div>
<NoteStatusIndicator
note={this.note}
status={this.state.noteStatus}
syncTakingTooLong={this.state.syncTakingTooLong}
updateSavingIndicator={this.state.updateSavingIndicator}
@@ -930,6 +893,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
</div>
{shouldShowConflictsButton && (
<Button
id={ElementIds.ConflictResolutionButton}
className="flex items-center"
primary
colorStyle="warning"
@@ -946,10 +910,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
<>
<LinkedItemsButton
linkingController={this.application.linkingController}
onClick={this.triggerSyncOnAction}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<ChangeEditorButton
noteViewController={this.controller}
onClick={this.triggerSyncOnAction}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<PinNoteButton
@@ -960,6 +926,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
)}
<NotesOptionsPanel
notesController={this.application.notesController}
onClick={this.triggerSyncOnAction}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
onButtonBlur={() => {
this.setState({

View File

@@ -122,23 +122,20 @@ export const PlainEditor = forwardRef<PlainEditorInterface, Props>(
needsAdjustMobileCursor.current = true
}
if (lastEditorFocusEventSource.current) {
application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: lastEditorFocusEventSource })
}
application.notifyWebEvent(WebAppEvent.EditorDidFocus, { eventSource: lastEditorFocusEventSource.current })
lastEditorFocusEventSource.current = undefined
onFocus()
}, [application, isAdjustingMobileCursor, lastEditorFocusEventSource, onFocus])
const onContentBlur = useCallback(
(event: FocusEvent) => {
if (lastEditorFocusEventSource.current) {
application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: lastEditorFocusEventSource })
}
lastEditorFocusEventSource.current = undefined
onBlur(event)
},
[application, lastEditorFocusEventSource, onBlur],
[lastEditorFocusEventSource, onBlur],
)
const scrollMobileCursorIntoViewAfterWebviewResize = useCallback(() => {

View File

@@ -4,6 +4,7 @@ import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { calculateReadTime } from './Utils/calculateReadTime'
import { countNoteAttributes } from './Utils/countNoteAttributes'
import { WebApplicationInterface } from '@standardnotes/ui-services'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
export const useNoteAttributes = (application: WebApplicationInterface, note: SNNote) => {
const { words, characters, paragraphs } = useMemo(() => countNoteAttributes(note.text), [note.text])
@@ -15,10 +16,13 @@ export const useNoteAttributes = (application: WebApplicationInterface, note: SN
const dateCreated = useMemo(() => formatDateForContextMenu(note.created_at), [note.created_at])
const size = useMemo(() => new Blob([note.text]).size, [note.text])
const editor = application.componentManager.editorForNote(note)
const format = editor.fileType
return {
size,
words,
characters,
paragraphs,
@@ -35,14 +39,16 @@ export const NoteAttributes: FunctionComponent<{
note: SNNote
className?: string
}> = ({ application, note, className }) => {
const { words, characters, paragraphs, readTime, userModifiedDate, dateCreated, format } = useNoteAttributes(
const { size, words, characters, paragraphs, readTime, userModifiedDate, dateCreated, format } = useNoteAttributes(
application,
note,
)
const canShowWordCount = typeof words === 'number' && (format === 'txt' || format === 'md')
return (
<div className={classNames('select-text px-3 py-1.5 text-sm font-medium text-neutral lg:text-xs', className)}>
{typeof words === 'number' && (format === 'txt' || format === 'md') ? (
{canShowWordCount ? (
<>
<div className="mb-1">
{words} words · {characters} characters · {paragraphs} paragraphs
@@ -58,9 +64,12 @@ export const NoteAttributes: FunctionComponent<{
<div className="mb-1">
<span className="font-semibold">Created:</span> {dateCreated}
</div>
<div>
<div className="mb-1">
<span className="font-semibold">Note ID:</span> {note.uuid}
</div>
<div>
<span className="font-semibold">Size:</span> {formatSizeToReadableString(size)}
</div>
</div>
)
}

View File

@@ -1,17 +1,15 @@
import Icon from '@/Components/Icon/Icon'
import { FunctionComponent } from 'react'
import { SNNote } from '@standardnotes/snjs'
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants'
import { LargeNoteThreshold } from '@/Constants/Constants'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
export const NOTE_SIZE_WARNING_THRESHOLD = 0.5 * BYTES_IN_ONE_MEGABYTE
export const NoteSizeWarning: FunctionComponent<{
note: SNNote
}> = ({ note }) => {
return new Blob([note.text]).size > NOTE_SIZE_WARNING_THRESHOLD ? (
return new Blob([note.text]).size > LargeNoteThreshold ? (
<>
<HorizontalSeparator classes="my-2" />
<HorizontalSeparator classes="mt-2" />
<div className="bg-warning-faded relative flex items-center px-3 py-3.5">
<Icon type="warning" className="mr-3 flex-shrink-0 text-accessory-tint-3" />
<div className="leading-140% max-w-80% select-none text-warning">

View File

@@ -9,11 +9,12 @@ import { ElementIds } from '@/Constants/ElementIDs'
type Props = {
notesController: NotesController
onClick?: () => void
onClickPreprocessing?: () => Promise<void>
onButtonBlur?: (event: FocusEvent) => void
}
const NotesOptionsPanel = ({ notesController, onClickPreprocessing, onButtonBlur }: Props) => {
const NotesOptionsPanel = ({ notesController, onClick, onClickPreprocessing, onButtonBlur }: Props) => {
const [isOpen, setIsOpen] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
@@ -23,7 +24,10 @@ const NotesOptionsPanel = ({ notesController, onClickPreprocessing, onButtonBlur
await onClickPreprocessing()
}
setIsOpen(willMenuOpen)
}, [onClickPreprocessing, isOpen])
if (onClick) {
onClick()
}
}, [isOpen, onClickPreprocessing, onClick])
const [disableClickOutside, setDisableClickOutside] = useState(false)
const handleDisableClickOutsideRequest = useCallback((disabled: boolean) => {

View File

@@ -27,6 +27,7 @@ const PositionedPopoverContent = ({
disableClickOutside,
disableMobileFullscreenTakeover,
disableFlip,
disableApplyingMobileWidth,
maxHeight,
portal = true,
offset,
@@ -57,6 +58,7 @@ const PositionedPopoverContent = ({
side,
disableMobileFullscreenTakeover,
disableFlip,
disableApplyingMobileWidth,
maxHeightFunction: maxHeight,
offset,
})

View File

@@ -44,6 +44,7 @@ type CommonPopoverProps = {
togglePopover?: () => void
disableMobileFullscreenTakeover?: boolean
disableFlip?: boolean
disableApplyingMobileWidth?: boolean
forceFullHeightOnMobile?: boolean
title: string
portal?: boolean

View File

@@ -44,6 +44,8 @@ type BlocksEditorProps = {
spellcheck?: boolean
ignoreFirstChange?: boolean
readonly?: boolean
onFocus?: () => void
onBlur?: () => void
}
export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
@@ -54,6 +56,8 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
spellcheck,
ignoreFirstChange = false,
readonly,
onFocus,
onBlur,
}) => {
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false)
const handleChange = useCallback(
@@ -95,6 +99,8 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
className,
)}
spellCheck={spellcheck}
onFocus={onFocus}
onBlur={onBlur}
/>
<div className="search-highlight-container pointer-events-none absolute left-0 top-0 h-full w-full" />
</div>

View File

@@ -6,6 +6,7 @@ import {
FeatureStatus,
GetSuperNoteFeature,
EditorLineHeightValues,
WebAppEvent,
} from '@standardnotes/snjs'
import { CSSProperties, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { BlocksEditor } from './BlocksEditor'
@@ -37,6 +38,7 @@ import NotEntitledBanner from '../ComponentView/NotEntitledBanner'
import AutoFocusPlugin from './Plugins/AutoFocusPlugin'
import usePreference from '@/Hooks/usePreference'
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
import { EditorEventSource } from '@/Types/EditorEventSource'
export const SuperNotePreviewCharLimit = 160
@@ -179,6 +181,10 @@ export const SuperEditor: FunctionComponent<Props> = ({
}
}, [])
const onFocus = useCallback(() => {
application.notifyWebEvent(WebAppEvent.EditorDidFocus, { eventSource: EditorEventSource.UserInteraction })
}, [application])
return (
<div
className="font-editor relative flex h-full w-full flex-col"
@@ -203,6 +209,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
previewLength={SuperNotePreviewCharLimit}
spellcheck={spellcheck}
readonly={note.current.locked || readonly}
onFocus={onFocus}
>
<ItemSelectionPlugin currentNote={note.current} />
<FilePlugin currentNote={note.current} />