From 95e5c2563e1d57d71e078eb91986501afd590031 Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Thu, 6 Nov 2025 00:45:16 +0200 Subject: [PATCH] client/read only note info bar: refactor --- .../widgets/FloatingButtonsDefinitions.tsx | 40 ++------- apps/client/src/widgets/react/hooks.tsx | 82 +++++++++++++++---- ...fo_bar.css => read_only_note_info_bar.css} | 0 .../src/widgets/read_only_note_info_bar.tsx | 57 +------------ 4 files changed, 77 insertions(+), 102 deletions(-) rename apps/client/src/widgets/{read-only-note-info_bar.css => read_only_note_info_bar.css} (100%) diff --git a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx index 8f4288eff..346697ff5 100644 --- a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx +++ b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx @@ -4,7 +4,7 @@ import Component from "../components/component"; import NoteContext from "../components/note_context"; import FNote from "../entities/fnote"; import ActionButton, { ActionButtonProps } from "./react/ActionButton"; -import { useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks"; +import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks"; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils"; import server from "../services/server"; @@ -13,8 +13,6 @@ import toast from "../services/toast"; import { t } from "../services/i18n"; import { copyImageReferenceToClipboard } from "../services/image"; import tree from "../services/tree"; -import protected_session_holder from "../services/protected_session_holder"; -import options from "../services/options"; import { getHelpUrlForNote } from "../services/in_app_help"; import froca from "../services/froca"; import NoteLink from "./react/NoteLink"; @@ -102,47 +100,25 @@ function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingBut } function EditButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) { - const [ animationClass, setAnimationClass ] = useState(""); - const [ isEnabled, setIsEnabled ] = useState(false); - - useEffect(() => { - noteContext.isReadOnly().then(isReadOnly => { - setIsEnabled( - isDefaultViewMode - && (!note.isProtected || protected_session_holder.isProtectedSessionAvailable()) - && !options.is("databaseReadonly") - && isReadOnly - ); - }); - }, [ note ]); - - useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { - if (noteContext?.ntxId === eventNoteContext.ntxId) { - setIsEnabled(false); - } - }); - + const [animationClass, setAnimationClass] = useState(""); + const {isReadOnly, enableEditing} = useIsNoteReadOnly(); + // make the edit button stand out on the first display, otherwise // it's difficult to notice that the note is readonly useEffect(() => { - if (isEnabled) { + if (isReadOnly) { setAnimationClass("bx-tada bx-lg"); setTimeout(() => { setAnimationClass(""); }, 1700); } - }, [ isEnabled ]); + }, [ isReadOnly ]); - return isEnabled && { - if (noteContext.viewScope) { - noteContext.viewScope.readOnlyTemporarilyDisabled = true; - appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext }); - } - }} + onClick={() => enableEditing()} /> } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index a8a549edb..b28f642cf 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -1,25 +1,26 @@ -import { Inputs, MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; -import { CommandListenerData, EventData, EventNames } from "../../components/app_context"; -import { ParentComponent } from "./react_utils"; -import SpacedUpdate from "../../services/spaced_update"; +import { CSSProperties } from "preact/compat"; +import { DragData } from "../note_tree"; import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons"; -import options, { type OptionValue } from "../../services/options"; -import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils"; -import NoteContext from "../../components/note_context"; -import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; -import FNote from "../../entities/fnote"; -import attributes from "../../services/attributes"; -import FBlob from "../../entities/fblob"; -import NoteContextAwareWidget from "../note_context_aware_widget"; +import { Inputs, MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; +import { ParentComponent } from "./react_utils"; import { RefObject, VNode } from "preact"; import { Tooltip } from "bootstrap"; -import { CSSProperties } from "preact/compat"; +import { ViewMode } from "../../services/link"; +import appContext, { CommandListenerData, EventData, EventNames } from "../../components/app_context"; +import attributes from "../../services/attributes"; +import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; +import Component from "../../components/component"; +import FBlob from "../../entities/fblob"; +import FNote from "../../entities/fnote"; import keyboard_actions from "../../services/keyboard_actions"; import Mark from "mark.js"; -import { DragData } from "../note_tree"; -import Component from "../../components/component"; +import NoteContext from "../../components/note_context"; +import NoteContextAwareWidget from "../note_context_aware_widget"; +import options, { type OptionValue } from "../../services/options"; +import protected_session_holder from "../../services/protected_session_holder"; +import SpacedUpdate from "../../services/spaced_update"; import toast, { ToastOptions } from "../../services/toast"; -import { ViewMode } from "../../services/link"; +import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils"; export function useTriliumEvent(eventName: T, handler: (data: EventData) => void) { const parentComponent = useContext(ParentComponent); @@ -701,3 +702,52 @@ export function useResizeObserver(ref: RefObject, callback: () => v return () => observer.disconnect(); }, [ callback, ref ]); } + +/** + * Indicates that the current note is in read-only mode, while an editing mode is available, + * and provides a way to switch to editing mode. + */ +export function useIsNoteReadOnly() { + const {note, noteContext} = useNoteContext(); + const [isReadOnly, setIsReadOnly] = useState(false); + + const enableEditing = useCallback(() => { + if (noteContext?.viewScope) { + noteContext.viewScope.readOnlyTemporarilyDisabled = true; + appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext}); + } + }, [noteContext]); + + useEffect(() => { + if (note && noteContext) { + isNoteReadOnly(note, noteContext).then((readOnly) => { + setIsReadOnly(readOnly); + }); + } + }, [note, noteContext]); + + useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => { + if (noteContext?.ntxId === eventNoteContext.ntxId) { + setIsReadOnly(false); + } + }); + + return {isReadOnly, enableEditing}; +} + +async function isNoteReadOnly(note: FNote, noteContext: NoteContext) { + + if (note.isProtected && !protected_session_holder.isProtectedSessionAvailable()) { + return false; + } + + if (options.is("databaseReadonly")) { + return false; + } + + if (noteContext.viewScope?.viewMode !== "default" || !await noteContext.isReadOnly()) { + return false; + } + + return true; +} \ No newline at end of file diff --git a/apps/client/src/widgets/read-only-note-info_bar.css b/apps/client/src/widgets/read_only_note_info_bar.css similarity index 100% rename from apps/client/src/widgets/read-only-note-info_bar.css rename to apps/client/src/widgets/read_only_note_info_bar.css diff --git a/apps/client/src/widgets/read_only_note_info_bar.tsx b/apps/client/src/widgets/read_only_note_info_bar.tsx index b34ed6099..9ec9b264f 100644 --- a/apps/client/src/widgets/read_only_note_info_bar.tsx +++ b/apps/client/src/widgets/read_only_note_info_bar.tsx @@ -1,16 +1,10 @@ -import "./read-only-note-info-bar.css"; +import "./read_only_note_info_bar.css"; import { t } from "../services/i18n"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { useNoteContext, useTriliumEvent } from "./react/hooks" -import appContext from "../components/app_context"; +import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks" import Button from "./react/Button"; -import FNote from "../entities/fnote"; -import NoteContext from "../components/note_context"; -import options from "../services/options"; -import protected_session_holder from "../services/protected_session_holder"; export default function ReadOnlyNoteInfoBar() { - const {isReadOnly, enableEditing} = useIsReadOnly(); + const {isReadOnly, enableEditing} = useIsNoteReadOnly(); const {note} = useNoteContext(); return
@@ -33,49 +27,4 @@ export default function ReadOnlyNoteInfoBar() { icon="bx-pencil" onClick={() => enableEditing()} /> }
; -} - -export function useIsReadOnly() { - const {note, noteContext} = useNoteContext(); - const [isReadOnly, setIsReadOnly] = useState(false); - - const enableEditing = useCallback(() => { - if (noteContext?.viewScope) { - noteContext.viewScope.readOnlyTemporarilyDisabled = true; - appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext}); - } - }, [noteContext]); - - useEffect(() => { - if (note && noteContext) { - isNoteReadOnly(note, noteContext).then((readOnly) => { - setIsReadOnly(readOnly); - }); - } - }, [note, noteContext]); - - useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => { - if (noteContext?.ntxId === eventNoteContext.ntxId) { - setIsReadOnly(false); - } - }); - - return {isReadOnly, enableEditing}; -} - -async function isNoteReadOnly(note: FNote, noteContext: NoteContext) { - - if (note.isProtected && !protected_session_holder.isProtectedSessionAvailable()) { - return false; - } - - if (options.is("databaseReadonly")) { - return false; - } - - if (noteContext.viewScope?.viewMode !== "default" || !await noteContext.isReadOnly()) { - return false; - } - - return true; } \ No newline at end of file