From 396ee3f449c612600bbbe3294c61dc8be46ea365 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sat, 20 Jan 2024 15:22:09 +0530 Subject: [PATCH] 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) --- packages/icons/src/Icons/ic-clock.svg | 3 + packages/icons/src/Icons/index.ts | 2 + .../src/Domain/Utilities/Icon/IconType.ts | 1 + .../services/src/Domain/Event/WebAppEvent.ts | 2 +- packages/services/src/Domain/Sync/SyncMode.ts | 1 + .../snjs/lib/Services/Sync/SyncService.ts | 6 + .../Application/VisibilityObserver.ts | 6 + .../ApplicationView/ApplicationView.tsx | 2 +- .../ChangeEditor/ChangeEditorButton.tsx | 8 +- .../javascripts/Components/Footer/Footer.tsx | 2 +- .../Components/Icon/IconNameToSvgMapping.tsx | 1 + .../LinkedItems/LinkedItemsButton.tsx | 8 +- .../Controller/EditorSaveTimeoutDebounce.ts | 3 + .../Controller/ItemGroupController.ts | 3 + .../Controller/NoteViewController.spec.ts | 3 + .../NoteView/Controller/NoteViewController.ts | 25 +++ .../NoteView/NoteStatusIndicator.tsx | 126 ++++++++++----- .../Components/NoteView/NoteView.tsx | 105 +++++------- .../NoteView/PlainEditor/PlainEditor.tsx | 11 +- .../NotesOptions/NoteAttributes.tsx | 15 +- .../NotesOptions/NoteSizeWarning.tsx | 8 +- .../NotesOptions/NotesOptionsPanel.tsx | 8 +- .../Popover/PositionedPopoverContent.tsx | 2 + .../javascripts/Components/Popover/Types.ts | 1 + .../Components/SuperEditor/BlocksEditor.tsx | 6 + .../Components/SuperEditor/SuperEditor.tsx | 7 + .../src/javascripts/Constants/Constants.ts | 3 + .../src/javascripts/Constants/ElementIDs.ts | 1 + .../ItemList/ItemListController.ts | 10 +- .../Controllers/NoteSyncController.ts | 152 ++++++++++++++++-- .../Utils/GetRelativeTimeString.ts | 28 ++++ packages/web/src/stylesheets/_focused.scss | 12 +- 32 files changed, 408 insertions(+), 163 deletions(-) create mode 100644 packages/icons/src/Icons/ic-clock.svg create mode 100644 packages/web/src/javascripts/Utils/GetRelativeTimeString.ts diff --git a/packages/icons/src/Icons/ic-clock.svg b/packages/icons/src/Icons/ic-clock.svg new file mode 100644 index 000000000..86614f344 --- /dev/null +++ b/packages/icons/src/Icons/ic-clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/src/Icons/index.ts b/packages/icons/src/Icons/index.ts index 2820c656a..b258178fc 100644 --- a/packages/icons/src/Icons/index.ts +++ b/packages/icons/src/Icons/index.ts @@ -43,6 +43,7 @@ import ChevronUpIcon from './ic-chevron-up.svg' import CircleIcon from './circle-55.svg' import ClearCircleFilledIcon from './ic-clear-circle-filled.svg' import CloseCircleFilledIcon from './ic-close-circle-filled.svg' +import ClockIcon from './ic-clock.svg' import CloseIcon from './ic-close.svg' import CloudOffIcon from './ic-cloud-off.svg' import CodeIcon from './ic-code.svg' @@ -263,6 +264,7 @@ export { CircleIcon, ClearCircleFilledIcon, CloseCircleFilledIcon, + ClockIcon, CloseIcon, CloudOffIcon, CodeIcon, diff --git a/packages/models/src/Domain/Utilities/Icon/IconType.ts b/packages/models/src/Domain/Utilities/Icon/IconType.ts index 615ca7840..1098fc6df 100644 --- a/packages/models/src/Domain/Utilities/Icon/IconType.ts +++ b/packages/models/src/Domain/Utilities/Icon/IconType.ts @@ -37,6 +37,7 @@ export type IconType = | 'chevron-right' | 'chevron-up' | 'clear-circle-filled' + | 'clock' | 'close' | 'cloud-off' | 'code-2' diff --git a/packages/services/src/Domain/Event/WebAppEvent.ts b/packages/services/src/Domain/Event/WebAppEvent.ts index 1bcce80ce..265f4ee35 100644 --- a/packages/services/src/Domain/Event/WebAppEvent.ts +++ b/packages/services/src/Domain/Event/WebAppEvent.ts @@ -1,6 +1,6 @@ export enum WebAppEvent { NewUpdateAvailable = 'NewUpdateAvailable', - EditorFocused = 'EditorFocused', + EditorDidFocus = 'EditorDidFocus', BeganBackupDownload = 'BeganBackupDownload', EndedBackupDownload = 'EndedBackupDownload', PanelResized = 'PanelResized', diff --git a/packages/services/src/Domain/Sync/SyncMode.ts b/packages/services/src/Domain/Sync/SyncMode.ts index 7a813c672..0b5f05441 100644 --- a/packages/services/src/Domain/Sync/SyncMode.ts +++ b/packages/services/src/Domain/Sync/SyncMode.ts @@ -11,4 +11,5 @@ export enum SyncMode { * all data to see if user has an items key, and if not, only then create a new one. */ DownloadFirst = 'DownloadFirst', + LocalOnly = 'LocalOnly', } diff --git a/packages/snjs/lib/Services/Sync/SyncService.ts b/packages/snjs/lib/Services/Sync/SyncService.ts index dca3b962f..12963133d 100644 --- a/packages/snjs/lib/Services/Sync/SyncService.ts +++ b/packages/snjs/lib/Services/Sync/SyncService.ts @@ -896,6 +896,12 @@ export class SyncService const { items, beginDate, frozenDirtyIndex, neverSyncedDeleted } = await this.prepareForSync(options) + if (options.mode === SyncMode.LocalOnly) { + this.logger.debug('Syncing local only, skipping remote sync request') + releaseLock() + return + } + const inTimeResolveQueue = this.getPendingRequestsMadeInTimeToPiggyBackOnCurrentRequest() if (!shouldExecuteSync) { diff --git a/packages/web/src/javascripts/Application/VisibilityObserver.ts b/packages/web/src/javascripts/Application/VisibilityObserver.ts index f610b39f2..8a26afeee 100644 --- a/packages/web/src/javascripts/Application/VisibilityObserver.ts +++ b/packages/web/src/javascripts/Application/VisibilityObserver.ts @@ -11,6 +11,7 @@ export class VisibilityObserver { */ document.addEventListener('visibilitychange', this.onVisibilityChange) window.addEventListener('focus', this.onFocusEvent, false) + window.addEventListener('blur', this.onBlurEvent, false) } onVisibilityChange = () => { @@ -23,6 +24,10 @@ export class VisibilityObserver { this.notifyEvent(WebAppEvent.WindowDidFocus) } + onBlurEvent = () => { + this.notifyEvent(WebAppEvent.WindowDidBlur) + } + private notifyEvent(event: WebAppEvent): void { if (this.raceTimeout) { clearTimeout(this.raceTimeout) @@ -35,6 +40,7 @@ export class VisibilityObserver { deinit(): void { document.removeEventListener('visibilitychange', this.onVisibilityChange) window.removeEventListener('focus', this.onFocusEvent) + window.removeEventListener('blur', this.onBlurEvent) ;(this.onEvent as unknown) = undefined } } diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index 2f968342d..e31c2547b 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -168,7 +168,7 @@ const ApplicationView: FunctionComponent = ({ 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) } diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx index a20ca4b4b..23e8443d3 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx @@ -11,10 +11,11 @@ import { NoteType, noteTypeForEditorIdentifier } from '@standardnotes/snjs' type Props = { noteViewController?: NoteViewController + onClick?: () => void onClickPreprocessing?: () => Promise } -const ChangeEditorButton: FunctionComponent = ({ noteViewController, onClickPreprocessing }: Props) => { +const ChangeEditorButton: FunctionComponent = ({ noteViewController, onClick, onClickPreprocessing }: Props) => { const application = useApplication() const note = application.notesController.firstSelectedNote @@ -48,7 +49,10 @@ const ChangeEditorButton: FunctionComponent = ({ noteViewController, onCl await onClickPreprocessing() } setIsOpen(willMenuOpen) - }, [onClickPreprocessing, isOpen]) + if (onClick) { + onClick() + } + }, [isOpen, onClickPreprocessing, onClick]) useEffect(() => { return application.keyboardService.addCommandHandler({ diff --git a/packages/web/src/javascripts/Components/Footer/Footer.tsx b/packages/web/src/javascripts/Components/Footer/Footer.tsx index af70cd5c7..29ae3ed79 100644 --- a/packages/web/src/javascripts/Components/Footer/Footer.tsx +++ b/packages/web/src/javascripts/Components/Footer/Footer.tsx @@ -70,7 +70,7 @@ class Footer extends AbstractComponent { 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() diff --git a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx index 89a152698..93cec6fba 100644 --- a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx +++ b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx @@ -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, diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsButton.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsButton.tsx index 1c5e0da82..03a15f967 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsButton.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsButton.tsx @@ -7,10 +7,11 @@ import LinkedItemsPanel from './LinkedItemsPanel' type Props = { linkingController: LinkingController + onClick?: () => void onClickPreprocessing?: () => Promise } -const LinkedItemsButton = ({ linkingController, onClickPreprocessing }: Props) => { +const LinkedItemsButton = ({ linkingController, onClick, onClickPreprocessing }: Props) => { const { activeItem, isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController const buttonRef = useRef(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 diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/EditorSaveTimeoutDebounce.ts b/packages/web/src/javascripts/Components/NoteView/Controller/EditorSaveTimeoutDebounce.ts index 836d6fc6f..d3295a9e2 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/EditorSaveTimeoutDebounce.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/EditorSaveTimeoutDebounce.ts @@ -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, } diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts index 35faa2dbe..101c80404 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts @@ -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) diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts index 48d2a5549..169a50a9a 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts @@ -28,6 +28,9 @@ describe('note view controller', () => { createTemplateItem: jest.fn().mockReturnValue({} as SNNote), } as unknown as jest.Mocked, mutator: {} as jest.Mocked, + sessions: { + isSignedIn: jest.fn().mockReturnValue(true), + }, } as unknown as jest.Mocked application.isNativeMobileWeb = jest.fn().mockReturnValue(false) diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts index 9cbf27808..615f60d5f 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts @@ -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() + } } diff --git a/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx b/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx index 2aee949aa..c78e7dd94 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx @@ -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> children: ReactNode animateIcon?: boolean -}) => ( -
- -
- {children} +}) => { + const buttonRef = useRef(null) + + return ( +
+ + 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} +
-
-) + ) +} 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 (
{status.message}
- {status.desc &&
{status.desc}
} + {status.description &&
{status.description}
}
) } @@ -94,15 +110,15 @@ const NoteStatusIndicator = ({ return ( {status ? ( <>
{status.message}
- {status.desc &&
{status.desc}
} + {status.description &&
{status.description}
} ) : (
Sync taking too long
@@ -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} >
{status.message}
- {status.desc &&
{status.desc}
} + {status.description &&
{status.description}
} + {status.type === 'waiting' && note.lastSyncEnd && ( +
Last synced {getRelativeTimeString(note.lastSyncEnd)}
+ )} + {status.type === 'waiting' ? ( + + ) : ( + + )}
) } @@ -133,15 +169,17 @@ const NoteStatusIndicator = ({ return (
Note status updates are disabled
-
Click to enable.
+
) } -export default NoteStatusIndicator +export default observer(NoteStatusIndicator) diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index ced79bf7f..776cef8e3 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -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 { readonly controller!: NoteViewController - private statusTimeout?: NodeJS.Timeout onEditorComponentLoad?: () => void private removeTrashKeyObserver?: () => void @@ -167,8 +165,6 @@ class NoteView extends AbstractComponent { ;(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 { }) 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 { } 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 { 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 { * 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 { 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 = ({ key, currentTarget }) => { if (key !== KeyboardKey.Enter) { return @@ -839,6 +797,10 @@ class NoteView extends AbstractComponent { })) } + triggerSyncOnAction = () => { + this.controller.syncNow() + } + override render() { if (this.controller.dealloced) { return null @@ -923,6 +885,7 @@ class NoteView extends AbstractComponent { />
{ {shouldShowConflictsButton && (