perf: avoid uneccessary notes list item rerenders (#1904)
This commit is contained in:
@@ -7,7 +7,7 @@ import { Component } from 'react'
|
|||||||
export type PureComponentState = Partial<Record<string, any>>
|
export type PureComponentState = Partial<Record<string, any>>
|
||||||
export type PureComponentProps = Partial<Record<string, any>>
|
export type PureComponentProps = Partial<Record<string, any>>
|
||||||
|
|
||||||
export abstract class PureComponent<P = PureComponentProps, S = PureComponentState> extends Component<P, S> {
|
export abstract class AbstractComponent<P = PureComponentProps, S = PureComponentState> extends Component<P, S> {
|
||||||
private unsubApp!: () => void
|
private unsubApp!: () => void
|
||||||
private reactionDisposers: IReactionDisposer[] = []
|
private reactionDisposers: IReactionDisposer[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@stan
|
|||||||
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
||||||
import { alertDialog, RouteType } from '@standardnotes/ui-services'
|
import { alertDialog, RouteType } from '@standardnotes/ui-services'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import Navigation from '@/Components/Navigation/Navigation'
|
import Navigation from '@/Components/Tags/Navigation'
|
||||||
import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView'
|
import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView'
|
||||||
import Footer from '@/Components/Footer/Footer'
|
import Footer from '@/Components/Footer/Footer'
|
||||||
import SessionsModal from '@/Components/SessionsModal/SessionsModal'
|
import SessionsModal from '@/Components/SessionsModal/SessionsModal'
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const ContentList: FunctionComponent<Props> = ({
|
|||||||
const { selectPreviousItem, selectNextItem } = selectionController
|
const { selectPreviousItem, selectNextItem } = selectionController
|
||||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = itemListController.webDisplayOptions
|
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = itemListController.webDisplayOptions
|
||||||
const { sortBy } = itemListController.displayOptions
|
const { sortBy } = itemListController.displayOptions
|
||||||
|
const selectedTag = navigationController.selected
|
||||||
|
|
||||||
const onScroll: UIEventHandler = useCallback(
|
const onScroll: UIEventHandler = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -72,25 +73,27 @@ const ContentList: FunctionComponent<Props> = ({
|
|||||||
[selectionController],
|
[selectionController],
|
||||||
)
|
)
|
||||||
|
|
||||||
const getTagsForItem = (item: ListableContentItem) => {
|
const getTagsForItem = useCallback(
|
||||||
if (hideTags) {
|
(item: ListableContentItem) => {
|
||||||
return []
|
if (hideTags) {
|
||||||
}
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const selectedTag = navigationController.selected
|
if (!selectedTag) {
|
||||||
if (!selectedTag) {
|
return []
|
||||||
return []
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const tags = application.getItemTags(item)
|
const tags = application.getItemTags(item)
|
||||||
|
|
||||||
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
||||||
if (isNavigatingOnlyTag) {
|
if (isNavigatingOnlyTag) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
}
|
},
|
||||||
|
[hideTags, selectedTag, application],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ContentType } from '@standardnotes/snjs'
|
import { ContentType } from '@standardnotes/snjs'
|
||||||
import { FunctionComponent } from 'react'
|
import React, { FunctionComponent } from 'react'
|
||||||
import FileListItem from './FileListItem'
|
import FileListItem from './FileListItem'
|
||||||
import NoteListItem from './NoteListItem'
|
import NoteListItem from './NoteListItem'
|
||||||
import { AbstractListItemProps } from './Types/AbstractListItemProps'
|
import { AbstractListItemProps, doListItemPropsMeritRerender } from './Types/AbstractListItemProps'
|
||||||
|
|
||||||
const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
||||||
switch (props.item.content_type) {
|
switch (props.item.content_type) {
|
||||||
@@ -15,4 +15,4 @@ const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContentListItem
|
export default React.memo(ContentListItem, (a, b) => !doListItemPropsMeritRerender(a, b))
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
|||||||
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||||
import ListItemNotePreviewText from './ListItemNotePreviewText'
|
import ListItemNotePreviewText from './ListItemNotePreviewText'
|
||||||
import { ListItemTitle } from './ListItemTitle'
|
import { ListItemTitle } from './ListItemTitle'
|
||||||
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
|
|
||||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||||
application,
|
application,
|
||||||
@@ -70,6 +71,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
|
|
||||||
useContextMenuEvent(listItemRef, openContextMenu)
|
useContextMenuEvent(listItemRef, openContextMenu)
|
||||||
|
|
||||||
|
log(LoggingDomain.ItemsList, 'Rendering note list item', item.title)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={listItemRef}
|
ref={listItemRef}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { FilesController } from '@/Controllers/FilesController'
|
import { FilesController } from '@/Controllers/FilesController'
|
||||||
import { NotesController } from '@/Controllers/NotesController'
|
import { NotesController } from '@/Controllers/NotesController'
|
||||||
import { SortableItem, SNTag } from '@standardnotes/snjs'
|
import { SortableItem, SNTag, Uuids } from '@standardnotes/snjs'
|
||||||
import { ListableContentItem } from './ListableContentItem'
|
import { ListableContentItem } from './ListableContentItem'
|
||||||
|
|
||||||
export type AbstractListItemProps = {
|
export type AbstractListItemProps = {
|
||||||
@@ -18,3 +18,89 @@ export type AbstractListItemProps = {
|
|||||||
sortBy: keyof SortableItem | undefined
|
sortBy: keyof SortableItem | undefined
|
||||||
tags: SNTag[]
|
tags: SNTag[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doListItemPropsMeritRerender(previous: AbstractListItemProps, next: AbstractListItemProps): boolean {
|
||||||
|
const simpleComparison: (keyof AbstractListItemProps)[] = [
|
||||||
|
'onSelect',
|
||||||
|
'hideDate',
|
||||||
|
'hideIcon',
|
||||||
|
'hideTags',
|
||||||
|
'hidePreview',
|
||||||
|
'selected',
|
||||||
|
'sortBy',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const key of simpleComparison) {
|
||||||
|
if (previous[key] !== next[key]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous['item'] !== next['item']) {
|
||||||
|
if (doesItemChangeMeritRerender(previous['item'], next['item'])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doesTagsChangeMeritRerender(previous['tags'], next['tags'])
|
||||||
|
}
|
||||||
|
|
||||||
|
function doesTagsChangeMeritRerender(previous: SNTag[], next: SNTag[]): boolean {
|
||||||
|
if (previous === next) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous.length !== next.length) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous.length === 0 && next.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Uuids(previous).sort().join() !== Uuids(next).sort().join()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
previous
|
||||||
|
.map((t) => t.title)
|
||||||
|
.sort()
|
||||||
|
.join() !==
|
||||||
|
next
|
||||||
|
.map((t) => t.title)
|
||||||
|
.sort()
|
||||||
|
.join()
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function doesItemChangeMeritRerender(previous: ListableContentItem, next: ListableContentItem): boolean {
|
||||||
|
if (previous.uuid !== next.uuid) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertiesMeritingRerender: (keyof ListableContentItem)[] = [
|
||||||
|
'title',
|
||||||
|
'protected',
|
||||||
|
'updatedAtString',
|
||||||
|
'createdAtString',
|
||||||
|
'hidePreview',
|
||||||
|
'preview_html',
|
||||||
|
'preview_plain',
|
||||||
|
'archived',
|
||||||
|
'starred',
|
||||||
|
'pinned',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const key of propertiesMeritingRerender) {
|
||||||
|
if (previous[key] !== next[key]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||||
import { destroyAllObjectProperties, preventRefreshing } from '@/Utils'
|
import { destroyAllObjectProperties, preventRefreshing } from '@/Utils'
|
||||||
import { ApplicationEvent, ApplicationDescriptor, WebAppEvent } from '@standardnotes/snjs'
|
import { ApplicationEvent, ApplicationDescriptor, WebAppEvent } from '@standardnotes/snjs'
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +41,7 @@ type State = {
|
|||||||
arbitraryStatusMessage?: string
|
arbitraryStatusMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class Footer extends PureComponent<Props, State> {
|
class Footer extends AbstractComponent<Props, State> {
|
||||||
public user?: unknown
|
public user?: unknown
|
||||||
private didCheckForOffline = false
|
private didCheckForOffline = false
|
||||||
private completedInitialSync = false
|
private completedInitialSync = false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileItem, FileViewController, NoteViewController } from '@standardnotes/snjs'
|
import { FileItem, FileViewController, NoteViewController } from '@standardnotes/snjs'
|
||||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes'
|
||||||
import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles'
|
import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles'
|
||||||
@@ -22,7 +22,7 @@ type Props = {
|
|||||||
application: WebApplication
|
application: WebApplication
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoteGroupView extends PureComponent<Props, State> {
|
class NoteGroupView extends AbstractComponent<Props, State> {
|
||||||
private removeChangeObserver!: () => void
|
private removeChangeObserver!: () => void
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||||
|
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
|
||||||
|
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||||
|
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||||
|
import PanelResizer, { PanelResizeType, PanelSide } from '@/Components/PanelResizer/PanelResizer'
|
||||||
|
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||||
|
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||||
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
|
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 '@/Utils/ConcatenateClassNames'
|
||||||
import {
|
import {
|
||||||
ApplicationEvent,
|
ApplicationEvent,
|
||||||
isPayloadSourceRetrieved,
|
|
||||||
isPayloadSourceInternalChange,
|
|
||||||
ContentType,
|
|
||||||
SNComponent,
|
|
||||||
SNNote,
|
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
PrefKey,
|
|
||||||
ComponentViewerInterface,
|
ComponentViewerInterface,
|
||||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
ContentType,
|
||||||
|
EditorFontSize,
|
||||||
|
EditorLineHeight,
|
||||||
|
isPayloadSourceInternalChange,
|
||||||
|
isPayloadSourceRetrieved,
|
||||||
|
NoteType,
|
||||||
NoteViewController,
|
NoteViewController,
|
||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
|
PrefKey,
|
||||||
|
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||||
|
SNComponent,
|
||||||
|
SNNote,
|
||||||
WebAppEvent,
|
WebAppEvent,
|
||||||
EditorLineHeight,
|
|
||||||
EditorFontSize,
|
|
||||||
NoteType,
|
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
|
import { confirmDialog, KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
|
||||||
|
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
|
||||||
import { EditorEventSource } from '../../Types/EditorEventSource'
|
import { EditorEventSource } from '../../Types/EditorEventSource'
|
||||||
import { confirmDialog, KeyboardModifier, KeyboardKey } from '@standardnotes/ui-services'
|
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||||
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Constants/Strings'
|
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
||||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
||||||
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
||||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
|
||||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
|
||||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
|
||||||
import PanelResizer, { PanelSide, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
|
||||||
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
|
|
||||||
import EditingDisabledBanner from './EditingDisabledBanner'
|
import EditingDisabledBanner from './EditingDisabledBanner'
|
||||||
|
import { reloadFont } from './FontFunctions'
|
||||||
|
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
||||||
|
import NoteViewFileDropTarget from './NoteViewFileDropTarget'
|
||||||
|
import { NoteViewProps } from './NoteViewProps'
|
||||||
import {
|
import {
|
||||||
transactionForAssociateComponentWithCurrentNote,
|
transactionForAssociateComponentWithCurrentNote,
|
||||||
transactionForDisassociateComponentWithCurrentNote,
|
transactionForDisassociateComponentWithCurrentNote,
|
||||||
} from './TransactionFunctions'
|
} from './TransactionFunctions'
|
||||||
import { reloadFont } from './FontFunctions'
|
|
||||||
import { NoteViewProps } from './NoteViewProps'
|
|
||||||
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
|
||||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
|
||||||
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
|
||||||
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
|
|
||||||
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
|
||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
|
||||||
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
|
|
||||||
import NoteViewFileDropTarget from './NoteViewFileDropTarget'
|
|
||||||
|
|
||||||
const MinimumStatusDuration = 400
|
const MinimumStatusDuration = 400
|
||||||
const TextareaDebounce = 100
|
const TextareaDebounce = 100
|
||||||
@@ -98,7 +99,7 @@ const PlaintextFontSizeMapping: Record<EditorFontSize, string> = {
|
|||||||
Large: 'text-xl',
|
Large: 'text-xl',
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoteView extends PureComponent<NoteViewProps, State> {
|
class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||||
readonly controller!: NoteViewController
|
readonly controller!: NoteViewController
|
||||||
|
|
||||||
private statusTimeout?: NodeJS.Timeout
|
private statusTimeout?: NodeJS.Timeout
|
||||||
@@ -193,7 +194,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
;(this.onPanelResizeFinish as unknown) = undefined
|
;(this.onPanelResizeFinish as unknown) = undefined
|
||||||
;(this.stackComponentExpanded as unknown) = undefined
|
;(this.stackComponentExpanded as unknown) = undefined
|
||||||
;(this.toggleStackComponent as unknown) = undefined
|
;(this.toggleStackComponent as unknown) = undefined
|
||||||
;(this.onSystemEditorLoad as unknown) = undefined
|
;(this.onSystemEditorRef as unknown) = undefined
|
||||||
;(this.debounceReloadEditorComponent as unknown) = undefined
|
;(this.debounceReloadEditorComponent as unknown) = undefined
|
||||||
;(this.textAreaChangeDebounceSave as unknown) = undefined
|
;(this.textAreaChangeDebounceSave as unknown) = undefined
|
||||||
;(this.editorContentRef as unknown) = undefined
|
;(this.editorContentRef as unknown) = undefined
|
||||||
@@ -207,6 +208,24 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
return this.controller.item
|
return this.controller.item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override shouldComponentUpdate(_nextProps: Readonly<NoteViewProps>, nextState: Readonly<State>): boolean {
|
||||||
|
const complexObjects: (keyof State)[] = ['availableStackComponents', 'stackComponentViewers']
|
||||||
|
for (const key of Object.keys(nextState) as (keyof State)[]) {
|
||||||
|
if (complexObjects.includes(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prevValue = this.state[key]
|
||||||
|
const nextValue = nextState[key]
|
||||||
|
|
||||||
|
if (prevValue !== nextValue) {
|
||||||
|
log(LoggingDomain.NoteView, 'Rendering due to state change', key, prevValue, nextValue)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override componentDidMount(): void {
|
override componentDidMount(): void {
|
||||||
super.componentDidMount()
|
super.componentDidMount()
|
||||||
|
|
||||||
@@ -253,6 +272,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onNoteInnerChange(note: SNNote, source: PayloadEmitSource): void {
|
onNoteInnerChange(note: SNNote, source: PayloadEmitSource): void {
|
||||||
|
log(LoggingDomain.NoteView, 'On inner note change', PayloadEmitSource[source])
|
||||||
|
|
||||||
if (note.uuid !== this.note.uuid) {
|
if (note.uuid !== this.note.uuid) {
|
||||||
throw Error('Editor received changes for non-current note')
|
throw Error('Editor received changes for non-current note')
|
||||||
}
|
}
|
||||||
@@ -431,12 +452,15 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
streamItems() {
|
streamItems() {
|
||||||
this.removeComponentStreamObserver = this.application.streamItems(ContentType.Component, async ({ source }) => {
|
this.removeComponentStreamObserver = this.application.streamItems(ContentType.Component, async ({ source }) => {
|
||||||
|
log(LoggingDomain.NoteView, 'On component stream observer', PayloadEmitSource[source])
|
||||||
if (isPayloadSourceInternalChange(source) || source === PayloadEmitSource.InitialObserverRegistrationPush) {
|
if (isPayloadSourceInternalChange(source) || source === PayloadEmitSource.InitialObserverRegistrationPush) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.note) {
|
if (!this.note) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.reloadStackComponents()
|
await this.reloadStackComponents()
|
||||||
this.debounceReloadEditorComponent()
|
this.debounceReloadEditorComponent()
|
||||||
})
|
})
|
||||||
@@ -454,6 +478,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
if (this.state.editorComponentViewerDidAlreadyReload && !force) {
|
if (this.state.editorComponentViewerDidAlreadyReload && !force) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const component = viewer.component
|
const component = viewer.component
|
||||||
this.application.componentManager.destroyComponentViewer(viewer)
|
this.application.componentManager.destroyComponentViewer(viewer)
|
||||||
this.setState(
|
this.setState(
|
||||||
@@ -489,6 +514,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reloadEditorComponent() {
|
async reloadEditorComponent() {
|
||||||
|
log(LoggingDomain.NoteView, 'Reload editor component')
|
||||||
if (this.state.showProtectedWarning) {
|
if (this.state.showProtectedWarning) {
|
||||||
this.destroyCurrentEditorComponent()
|
this.destroyCurrentEditorComponent()
|
||||||
return
|
return
|
||||||
@@ -581,13 +607,16 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
onTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> = ({ currentTarget }) => {
|
onTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> = ({ currentTarget }) => {
|
||||||
const text = currentTarget.value
|
const text = currentTarget.value
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorText: text,
|
editorText: text,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.textAreaChangeDebounceSave()
|
this.textAreaChangeDebounceSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
textAreaChangeDebounceSave = () => {
|
textAreaChangeDebounceSave = () => {
|
||||||
|
log(LoggingDomain.NoteView, 'Performing save after debounce')
|
||||||
this.controller
|
this.controller
|
||||||
.save({
|
.save({
|
||||||
editorValues: {
|
editorValues: {
|
||||||
@@ -609,10 +638,14 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTitleChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
onTitleChange: ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||||
|
log(LoggingDomain.NoteView, 'Performing save after title change')
|
||||||
|
|
||||||
const title = currentTarget.value
|
const title = currentTarget.value
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorTitle: title,
|
editorTitle: title,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.controller
|
this.controller
|
||||||
.save({
|
.save({
|
||||||
editorValues: {
|
editorValues: {
|
||||||
@@ -662,10 +695,12 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
this.application.alertService.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT).catch(console.error)
|
this.application.alertService.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT).catch(console.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.note.locked) {
|
if (this.note.locked) {
|
||||||
this.application.alertService.alert(STRING_DELETE_LOCKED_ATTEMPT).catch(console.error)
|
this.application.alertService.alert(STRING_DELETE_LOCKED_ATTEMPT).catch(console.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = this.note.title.length ? `'${this.note.title}'` : 'this note'
|
const title = this.note.title.length ? `'${this.note.title}'` : 'this note'
|
||||||
const text = StringDeleteNote(title, permanently)
|
const text = StringDeleteNote(title, permanently)
|
||||||
if (
|
if (
|
||||||
@@ -727,6 +762,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reloadPreferences() {
|
async reloadPreferences() {
|
||||||
|
log(LoggingDomain.NoteView, 'Reload preferences')
|
||||||
const monospaceFont = this.application.getPreference(
|
const monospaceFont = this.application.getPreference(
|
||||||
PrefKey.EditorMonospaceEnabled,
|
PrefKey.EditorMonospaceEnabled,
|
||||||
PrefDefaults[PrefKey.EditorMonospaceEnabled],
|
PrefDefaults[PrefKey.EditorMonospaceEnabled],
|
||||||
@@ -776,9 +812,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @components */
|
|
||||||
|
|
||||||
async reloadStackComponents() {
|
async reloadStackComponents() {
|
||||||
|
log(LoggingDomain.NoteView, 'Reload stack components')
|
||||||
const stackComponents = sortAlphabetically(
|
const stackComponents = sortAlphabetically(
|
||||||
this.application.componentManager
|
this.application.componentManager
|
||||||
.componentsForArea(ComponentArea.EditorStack)
|
.componentsForArea(ComponentArea.EditorStack)
|
||||||
@@ -844,10 +879,13 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onSystemEditorLoad = (ref: HTMLTextAreaElement | null) => {
|
onSystemEditorRef = (ref: HTMLTextAreaElement | null) => {
|
||||||
if (this.removeTabObserver || !ref) {
|
if (this.removeTabObserver || !ref) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(LoggingDomain.NoteView, 'On system editor ref')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert 4 spaces when a tab key is pressed,
|
* Insert 4 spaces when a tab key is pressed,
|
||||||
* only used when inside of the text editor.
|
* only used when inside of the text editor.
|
||||||
@@ -1070,7 +1108,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
onFocus={this.onContentFocus}
|
onFocus={this.onContentFocus}
|
||||||
onBlur={this.onContentBlur}
|
onBlur={this.onContentBlur}
|
||||||
readOnly={this.state.noteLocked}
|
readOnly={this.state.noteLocked}
|
||||||
ref={(ref) => ref && this.onSystemEditorLoad(ref)}
|
ref={(ref) => ref && this.onSystemEditorRef(ref)}
|
||||||
spellCheck={this.state.spellcheck}
|
spellCheck={this.state.spellcheck}
|
||||||
value={this.state.editorText}
|
value={this.state.editorText}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { createRef } from 'react'
|
import { createRef } from 'react'
|
||||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
||||||
import ModalDialog from '../Shared/ModalDialog'
|
import ModalDialog from '../Shared/ModalDialog'
|
||||||
@@ -38,7 +38,7 @@ type FormData = {
|
|||||||
status?: string
|
status?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class PasswordWizard extends PureComponent<Props, State> {
|
class PasswordWizard extends AbstractComponent<Props, State> {
|
||||||
private currentPasswordInput = createRef<HTMLInputElement>()
|
private currentPasswordInput = createRef<HTMLInputElement>()
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncResolutionMenu extends PureComponent<Props> {
|
class SyncResolutionMenu extends AbstractComponent<Props> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props, props.application)
|
super(props, props.application)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { mergeRefs } from '@/Hooks/mergeRefs'
|
|||||||
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
import { TagListSectionType } from './TagListSection'
|
import { TagListSectionType } from './TagListSection'
|
||||||
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tag: SNTag
|
tag: SNTag
|
||||||
@@ -237,6 +238,8 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
}
|
}
|
||||||
}, [addDragTarget, linkingController, removeDragTarget, tag])
|
}, [addDragTarget, linkingController, removeDragTarget, tag])
|
||||||
|
|
||||||
|
log(LoggingDomain.NavigationList, 'Rendering TagsListItem')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { log as utilsLog } from '@standardnotes/utils'
|
import { log as utilsLog } from '@standardnotes/utils'
|
||||||
|
import { isDev } from './Utils'
|
||||||
|
|
||||||
export enum LoggingDomain {
|
export enum LoggingDomain {
|
||||||
DailyNotes,
|
DailyNotes,
|
||||||
|
NoteView,
|
||||||
|
ItemsList,
|
||||||
|
NavigationList,
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||||
[LoggingDomain.DailyNotes]: false,
|
[LoggingDomain.DailyNotes]: false,
|
||||||
|
[LoggingDomain.NoteView]: false,
|
||||||
|
[LoggingDomain.ItemsList]: false,
|
||||||
|
[LoggingDomain.NavigationList]: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function log(domain: LoggingDomain, ...args: any[]): void {
|
export function log(domain: LoggingDomain, ...args: any[]): void {
|
||||||
if (!LoggingStatus[domain]) {
|
if (!isDev || !LoggingStatus[domain]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user