From a46f93856f92718d93c165ae26f1ad65078a6f01 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sat, 27 May 2023 00:54:58 +0530 Subject: [PATCH 01/66] feat: initial conflict resolution modal --- .../Display/DisplayOptionsToFilters.ts | 2 + .../ApplicationView/ApplicationView.tsx | 2 +- .../NoteView/ConflictedNotesModal.tsx | 93 +++++++++++++++++++ .../Components/NoteView/NoteView.tsx | 75 ++++++++++++++- .../Controllers/NoteSyncController.ts | 4 +- 5 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 packages/web/src/javascripts/Components/NoteView/ConflictedNotesModal.tsx diff --git a/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts b/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts index 5143b71b115..f774416ea9f 100644 --- a/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts +++ b/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts @@ -74,5 +74,7 @@ export function computeFiltersForDisplayOptions( filters.push((item) => itemMatchesQuery(item, query, collection)) } + filters.push((item) => !item.conflictOf) + return filters } diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index 190b9f24aa2..f748f994b3a 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -143,7 +143,7 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio const removeObserver = application.addWebEventObserver(async (eventName) => { if (eventName === WebAppEvent.WindowDidFocus) { if (!(await application.isLocked())) { - application.sync.sync().catch(console.error) + // application.sync.sync().catch(console.error) } } }) diff --git a/packages/web/src/javascripts/Components/NoteView/ConflictedNotesModal.tsx b/packages/web/src/javascripts/Components/NoteView/ConflictedNotesModal.tsx new file mode 100644 index 00000000000..251d2e279c3 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/ConflictedNotesModal.tsx @@ -0,0 +1,93 @@ +import { SNNote, classNames } from '@standardnotes/snjs' +import Modal, { ModalAction } from '../Modal/Modal' +import { ReactNode, useMemo, useState } from 'react' +import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' +import RadioIndicator from '../Radio/RadioIndicator' + +const ListItem = ({ + children, + isSelected, + onClick, +}: { + isSelected: boolean + onClick: () => void + children?: ReactNode +}) => { + return ( + + ) +} + +type Props = { + currentNote: SNNote + conflictedNotes: Set + close: () => void +} + +const ConflictedNotesModal = ({ currentNote, conflictedNotes, close }: Props) => { + const [selectedVersion, setSelectedVersion] = useState(currentNote.uuid) + + const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) + const actions = useMemo( + (): ModalAction[] => [ + { + label: 'Cancel', + onClick: close, + type: 'cancel', + mobileSlot: 'left', + }, + { + label: isMobileScreen ? 'Choose' : 'Choose version', + onClick: close, + type: 'primary', + mobileSlot: 'right', + }, + ], + [close, isMobileScreen], + ) + + return ( + +
+ setSelectedVersion(currentNote.uuid)} + > + Current version + + {[...conflictedNotes].map((note, index) => ( + setSelectedVersion(note.uuid)} + key={note.uuid} + > + Version {index + 1} + + ))} +
+
.
+
+ ) +} + +export default ConflictedNotesModal diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 938c194402e..ccdfd7d2b4b 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -9,7 +9,7 @@ import { PrefDefaults } from '@/Constants/PrefDefaults' import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings' import { log, LoggingDomain } from '@/Logging' import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils' -import { classNames } from '@standardnotes/utils' +import { classNames, pluralize } from '@standardnotes/utils' import { ApplicationEvent, ComponentArea, @@ -45,6 +45,9 @@ import { SuperEditorContentId } from '../SuperEditor/Constants' import { NoteViewController } from './Controller/NoteViewController' import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor' import { EditorMargins, EditorMaxWidths } from '../EditorWidthSelectionModal/EditorWidths' +import Button from '../Button/Button' +import ModalOverlay from '../Modal/ModalOverlay' +import ConflictedNotesModal from './ConflictedNotesModal' const MinimumStatusDuration = 400 @@ -74,6 +77,9 @@ type State = { updateSavingIndicator?: boolean editorFeatureIdentifier?: string noteType?: NoteType + + conflictedNotes: Set + showConflictResolutionModal: boolean } class NoteView extends AbstractComponent { @@ -84,6 +90,7 @@ class NoteView extends AbstractComponent { private removeTrashKeyObserver?: () => void private removeComponentStreamObserver?: () => void + private removeNoteStreamObserver?: () => void private removeComponentManagerObserver?: () => void private removeInnerNoteObserver?: () => void @@ -120,6 +127,8 @@ class NoteView extends AbstractComponent { syncTakingTooLong: false, editorFeatureIdentifier: this.controller.item.editorIdentifier, noteType: this.controller.item.noteType, + conflictedNotes: new Set(), + showConflictResolutionModal: false, } this.noteViewElementRef = createRef() @@ -133,6 +142,9 @@ class NoteView extends AbstractComponent { this.removeComponentStreamObserver?.() ;(this.removeComponentStreamObserver as unknown) = undefined + this.removeNoteStreamObserver?.() + ;(this.removeNoteStreamObserver as unknown) = undefined + this.removeInnerNoteObserver?.() ;(this.removeInnerNoteObserver as unknown) = undefined @@ -418,6 +430,47 @@ class NoteView extends AbstractComponent { await this.reloadStackComponents() this.debounceReloadEditorComponent() }) + + this.removeNoteStreamObserver = this.application.streamItems( + ContentType.Note, + async ({ inserted, changed, removed }) => { + const insertedOrChanged = inserted.concat(changed) + + for (const note of insertedOrChanged) { + if (!(note instanceof SNNote)) { + continue + } + + if (note.conflictOf === this.note.uuid) { + this.setState((state) => ({ + conflictedNotes: state.conflictedNotes.add(note), + })) + } else { + this.setState((state) => { + const conflictedNotes = new Set(state.conflictedNotes) + conflictedNotes.delete(note) + return { + conflictedNotes, + } + }) + } + } + + for (const note of removed) { + if (!(note instanceof SNNote)) { + continue + } + + this.setState((state) => { + const conflictedNotes = new Set(state.conflictedNotes) + conflictedNotes.delete(note) + return { + conflictedNotes, + } + }) + } + }, + ) } private createComponentViewer(component: SNComponent) { @@ -776,6 +829,12 @@ class NoteView extends AbstractComponent { this.setState({ plainEditorFocused: false }) } + toggleConflictResolutionModal = () => { + this.setState((state) => ({ + showConflictResolutionModal: !state.showConflictResolutionModal, + })) + } + override render() { if (this.controller.dealloced) { return null @@ -850,6 +909,12 @@ class NoteView extends AbstractComponent { updateSavingIndicator={this.state.updateSavingIndicator} /> + {this.state.conflictedNotes.size > 0 && ( + + )} {renderHeaderOptions && (
{ })}
+ + + + ) } diff --git a/packages/web/src/javascripts/Controllers/NoteSyncController.ts b/packages/web/src/javascripts/Controllers/NoteSyncController.ts index 225c1c17af0..1ad6f9bcf70 100644 --- a/packages/web/src/javascripts/Controllers/NoteSyncController.ts +++ b/packages/web/src/javascripts/Controllers/NoteSyncController.ts @@ -109,9 +109,9 @@ export class NoteSyncController { params.isUserModified, ) - void this.application.sync.sync().then(() => { + /* void this.application.sync.sync().then(() => { params.onRemoteSyncComplete?.() - }) + }) */ params.onLocalPropagationComplete?.() } From 7152eeaca4658e81b09339e0c073a6d905aab1de Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 1 Jun 2023 22:02:05 +0530 Subject: [PATCH 02/66] chore: stuff --- .../ApplicationView/ApplicationView.tsx | 2 +- .../LinkedItems/LinkedItemBubble.tsx | 9 +- .../LinkedItemBubblesContainer.tsx | 39 ++-- .../NoteView/ConflictedNotesModal.tsx | 93 --------- .../NoteView/NoteConflictResolutionModal.tsx | 181 ++++++++++++++++++ .../Components/NoteView/NoteView.tsx | 4 +- 6 files changed, 215 insertions(+), 113 deletions(-) delete mode 100644 packages/web/src/javascripts/Components/NoteView/ConflictedNotesModal.tsx create mode 100644 packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal.tsx diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index f748f994b3a..190b9f24aa2 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -143,7 +143,7 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio const removeObserver = application.addWebEventObserver(async (eventName) => { if (eventName === WebAppEvent.WindowDidFocus) { if (!(await application.isLocked())) { - // application.sync.sync().catch(console.error) + application.sync.sync().catch(console.error) } } }) diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx index 3ce8b0faaf3..dc8e3aafb35 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemBubble.tsx @@ -23,6 +23,7 @@ type Props = { isBidirectional: boolean inlineFlex?: boolean className?: string + readonly?: boolean } const LinkedItemBubble = ({ @@ -36,6 +37,7 @@ const LinkedItemBubble = ({ isBidirectional, inlineFlex, className, + readonly, }: Props) => { const ref = useRef(null) const application = useApplication() @@ -60,6 +62,9 @@ const LinkedItemBubble = ({ const onClick: MouseEventHandler = (event) => { if (wasClicked && event.target !== unlinkButtonRef.current) { setWasClicked(false) + if (readonly) { + return + } void activateItem?.(link.item) } else { setWasClicked(true) @@ -112,7 +117,7 @@ const LinkedItemBubble = ({ onKeyDown={onKeyDown} > - + {tagTitle && {tagTitle.titlePrefix}} {link.type === 'linked-by' && link.item.content_type !== ContentType.Tag && ( @@ -121,7 +126,7 @@ const LinkedItemBubble = ({ {getItemTitleInContextOfLinkBubble(link.item)} - {showUnlinkButton && ( + {showUnlinkButton && !readonly && ( { +const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = false, readonly = false }: Props) => { const { toggleAppPane } = useResponsiveAppPane() const commandService = useCommandService() @@ -113,18 +114,23 @@ const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = fals const nonVisibleItems = itemsToDisplay.length - visibleItems.length const [canShowContainerToggle, setCanShowContainerToggle] = useState(false) - const linkInputRef = useRef(null) const linkContainerRef = useRef(null) useEffect(() => { const container = linkContainerRef.current - const linkInput = linkInputRef.current - - if (!container || !linkInput) { + if (!container) { return } const resizeObserver = new ResizeObserver(() => { - if (container.clientHeight > linkInput.clientHeight) { + const firstChild = container.firstElementChild + if (!firstChild) { + return + } + + const threshold = firstChild.clientHeight + 4 + const didWrap = container.clientHeight > threshold + + if (didWrap) { setCanShowContainerToggle(true) } else { setCanShowContainerToggle(false) @@ -167,18 +173,20 @@ const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = fals focusedId={focusedId} setFocusedId={setFocusedId} isBidirectional={isItemBidirectionallyLinked(link)} + readonly={readonly} /> ))} {isCollapsed && nonVisibleItems > 0 && and {nonVisibleItems} more...} - + {!readonly && ( + + )} {itemsToDisplay.length > 0 && !shouldHideToggle && ( !isCollapsed) }} icon={isCollapsed ? 'chevron-down' : 'chevron-left'} + className="ml-2" /> )} diff --git a/packages/web/src/javascripts/Components/NoteView/ConflictedNotesModal.tsx b/packages/web/src/javascripts/Components/NoteView/ConflictedNotesModal.tsx deleted file mode 100644 index 251d2e279c3..00000000000 --- a/packages/web/src/javascripts/Components/NoteView/ConflictedNotesModal.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { SNNote, classNames } from '@standardnotes/snjs' -import Modal, { ModalAction } from '../Modal/Modal' -import { ReactNode, useMemo, useState } from 'react' -import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' -import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' -import RadioIndicator from '../Radio/RadioIndicator' - -const ListItem = ({ - children, - isSelected, - onClick, -}: { - isSelected: boolean - onClick: () => void - children?: ReactNode -}) => { - return ( - - ) -} - -type Props = { - currentNote: SNNote - conflictedNotes: Set - close: () => void -} - -const ConflictedNotesModal = ({ currentNote, conflictedNotes, close }: Props) => { - const [selectedVersion, setSelectedVersion] = useState(currentNote.uuid) - - const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) - const actions = useMemo( - (): ModalAction[] => [ - { - label: 'Cancel', - onClick: close, - type: 'cancel', - mobileSlot: 'left', - }, - { - label: isMobileScreen ? 'Choose' : 'Choose version', - onClick: close, - type: 'primary', - mobileSlot: 'right', - }, - ], - [close, isMobileScreen], - ) - - return ( - -
- setSelectedVersion(currentNote.uuid)} - > - Current version - - {[...conflictedNotes].map((note, index) => ( - setSelectedVersion(note.uuid)} - key={note.uuid} - > - Version {index + 1} - - ))} -
-
.
-
- ) -} - -export default ConflictedNotesModal diff --git a/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal.tsx b/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal.tsx new file mode 100644 index 00000000000..8ec9fa32f7b --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal.tsx @@ -0,0 +1,181 @@ +import { ContentType, NoteType, SNNote, classNames } from '@standardnotes/snjs' +import Modal, { ModalAction } from '../Modal/Modal' +import { ReactNode, useEffect, useMemo, useState } from 'react' +import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' +import RadioIndicator from '../Radio/RadioIndicator' +import { useApplication } from '../ApplicationProvider' +import ComponentView from '../ComponentView/ComponentView' +import { ErrorBoundary } from '@/Utils/ErrorBoundary' +import { BlocksEditor } from '../SuperEditor/BlocksEditor' +import { BlocksEditorComposer } from '../SuperEditor/BlocksEditorComposer' +import { useLinkingController } from '@/Controllers/LinkingControllerProvider' +import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer' + +const ListItem = ({ + children, + isSelected, + onClick, +}: { + isSelected: boolean + onClick: () => void + children?: ReactNode +}) => { + return ( + + ) +} + +const NoteContent = ({ note }: { note: SNNote }) => { + const application = useApplication() + const linkingController = useLinkingController() + + const componentViewer = useMemo(() => { + const editorForCurrentNote = note ? application.componentManager.editorForNote(note) : undefined + + if (!editorForCurrentNote) { + return undefined + } + + const templateNoteForRevision = application.mutator.createTemplateItem(ContentType.Note, note.content) as SNNote + + const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote) + componentViewer.setReadonly(true) + componentViewer.lockReadonly = true + componentViewer.overrideContextItem = templateNoteForRevision + return componentViewer + }, [application.componentManager, application.mutator, note]) + + useEffect(() => { + return () => { + if (componentViewer) { + application.componentManager.destroyComponentViewer(componentViewer) + } + } + }, [application, componentViewer]) + + return ( +
+
+
{note.title}
+
+
+ +
+ {componentViewer ? ( +
+ +
+ ) : note?.noteType === NoteType.Super ? ( + +
+ + + +
+
+ ) : ( +
+ {note.text.length ? ( +