From 8e48079bfb38a9adaec9abc7f47e08e60412497d Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 20 May 2026 02:15:51 +0800 Subject: [PATCH] fix: comment issues --- deps/db/src/logseq/db/frontend/class.cljs | 2 +- deps/db/src/logseq/db/frontend/schema.cljs | 2 +- src/main/frontend/components/avatar.cljs | 37 ++ src/main/frontend/components/block.cljs | 224 ++++++++++-- src/main/frontend/components/block.css | 45 ++- .../frontend/components/block/comments.cljs | 63 +++- .../components/block/comments_model.cljs | 65 ++++ src/main/frontend/components/header.cljs | 16 +- src/main/frontend/components/recycle.cljs | 24 +- src/main/frontend/components/repo.cljs | 14 +- src/main/frontend/handler/comments.cljs | 95 ++++- src/main/frontend/handler/editor.cljs | 16 +- src/main/frontend/worker/db/migrate.cljs | 15 +- src/resources/dicts/en.edn | 2 +- src/resources/dicts/zh-cn.edn | 2 +- .../components/block/comments_model_test.cljs | 328 ------------------ src/test/frontend/handler/comments_test.cljs | 98 ++++++ .../frontend/handler/editor_async_test.cljs | 5 +- src/test/frontend/handler/editor_test.cljs | 20 ++ src/test/frontend/worker/migrate_test.cljs | 42 +++ 20 files changed, 690 insertions(+), 425 deletions(-) create mode 100644 src/main/frontend/components/avatar.cljs delete mode 100644 src/test/frontend/components/block/comments_model_test.cljs create mode 100644 src/test/frontend/handler/comments_test.cljs diff --git a/deps/db/src/logseq/db/frontend/class.cljs b/deps/db/src/logseq/db/frontend/class.cljs index cfc412d9c0..0c66a36dfa 100644 --- a/deps/db/src/logseq/db/frontend/class.cljs +++ b/deps/db/src/logseq/db/frontend/class.cljs @@ -44,7 +44,7 @@ :logseq.class/Comments {:title "Comments" :properties {:logseq.property.class/hide-from-node true - :logseq.property/icon {:type :emoji, :id "speech_balloon"}} + :logseq.property/icon {:type :tabler-icon, :id "message-circle"}} :schema {:properties [:logseq.property.comments/blocks]}} :logseq.class/Comment diff --git a/deps/db/src/logseq/db/frontend/schema.cljs b/deps/db/src/logseq/db/frontend/schema.cljs index 5a095064e4..08d4dab70e 100644 --- a/deps/db/src/logseq/db/frontend/schema.cljs +++ b/deps/db/src/logseq/db/frontend/schema.cljs @@ -30,7 +30,7 @@ (map (juxt :major :minor) [(parse-schema-version x) (parse-schema-version y)]))) -(def version (parse-schema-version "65.28")) +(def version (parse-schema-version "65.29")) (defn major-version "Return a number. diff --git a/src/main/frontend/components/avatar.cljs b/src/main/frontend/components/avatar.cljs new file mode 100644 index 0000000000..34235fd346 --- /dev/null +++ b/src/main/frontend/components/avatar.cljs @@ -0,0 +1,37 @@ +(ns frontend.components.avatar + (:require [clojure.string :as string] + [logseq.shui.ui :as shui] + [logseq.shui.util :as shui-util] + [rum.core :as rum])) + +(defn initials + ([name] + (initials name 2)) + ([name n] + (when (some? name) + (let [name (string/trim (str name))] + (when-not (string/blank? name) + (-> (subs name 0 (min n (count name))) + string/upper-case)))))) + +(rum/defc user-avatar + [{:keys [name title uuid avatar-src class style fallback fallback-length fallback-props image-props] + :or {fallback-length 2}}] + (let [color (when uuid (shui-util/uuid-color uuid)) + fallback-text (or fallback (initials name fallback-length)) + fallback-style (cond-> (:style fallback-props) + (and color (nil? (:background-color (:style fallback-props)))) + (assoc :background-color (str color "50"))) + fallback-props' (cond-> fallback-props + (seq fallback-style) + (assoc :style fallback-style))] + (shui/avatar + (cond-> {} + class (assoc :class class) + style (assoc :style style) + title (assoc :title title)) + (when (seq avatar-src) + (shui/avatar-image (merge {:src avatar-src} image-props))) + (if (seq fallback-props') + (shui/avatar-fallback fallback-props' fallback-text) + (shui/avatar-fallback fallback-text))))) diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index e53058631f..bb513a2062 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -10,6 +10,7 @@ [datascript.impl.entity :as e] [dommy.core :as dom] [electron.ipc :as ipc] + [frontend.components.avatar :as avatar] [frontend.components.block.breadcrumb-model :as breadcrumb-model] [frontend.components.block.comments :as block-comments] [frontend.components.block.comments-model :as comments-model] @@ -92,7 +93,6 @@ [logseq.shui.dialog.core :as shui-dialog] [logseq.shui.hooks :as hooks] [logseq.shui.ui :as shui] - [logseq.shui.util :as shui-util] [medley.core :as medley] [missionary.core :as m] [promesa.core :as p] @@ -108,6 +108,10 @@ (defonce *drag-to-block (atom nil)) (def *move-to (atom nil)) +(def ^:private comment-thread-presence-ttl-ms 30000) +(defonce *comment-thread-presence (atom {})) +(defonce *comment-thread-presence-requests (atom {})) +(defonce *comment-thread-presence-flush-scheduled? (atom false)) ;; TODO: ;; add `key` @@ -2044,12 +2048,144 @@ [block] (string/blank? (:block/title block))) -(defn- user-initials - [user-name] - (when (string? user-name) - (let [name (string/trim user-name)] - (when-not (string/blank? name) - (-> name (subs 0 (min 2 (count name))) string/upper-case))))) +(defn- element-in-viewport? + [^js el] + (when el + (let [rect (.getBoundingClientRect el) + viewport-height (or (.-innerHeight js/window) + (some-> js/document .-documentElement .-clientHeight)) + viewport-width (or (.-innerWidth js/window) + (some-> js/document .-documentElement .-clientWidth))] + (and (< (.-top rect) viewport-height) + (> (.-bottom rect) 0) + (< (.-left rect) viewport-width) + (> (.-right rect) 0))))) + +(defn- comments-area-in-viewport? + [comments-area] + (some-> (:block/uuid comments-area) + (str) + ((fn [uuid] (gdom/getElement (str "ls-block-" uuid)))) + element-in-viewport?)) + +(defn- inline-comment-thread? + [inline-thread target-block-uuid comments-area] + (and comments-area + (= (:target-block-uuid inline-thread) (str target-block-uuid)) + (= (:comments-area-uuid inline-thread) (str (:block/uuid comments-area))))) + +(defn- open-comment-thread! + [target-block-uuid comments-area] + (case (comments-model/comment-thread-click-action (comments-area-in-viewport? comments-area)) + :focus-comments-area + (do + (state/set-state! :comments/inline-thread nil) + (comments-handler/reveal-comments-area! comments-area {:focus-editor? true})) + + :show-inline-comments + (do + (comments-handler/expand-comments-area! comments-area) + (state/set-state! :comments/inline-thread + (comments-model/next-inline-comment-thread + (get @state/state :comments/inline-thread) + target-block-uuid + (:block/uuid comments-area)))))) + +(defn- ui-comment-thread-for-block + [block] + (when-let [uuid (:block/uuid block)] + (comments-model/comment-thread-for-block + block + (db/entity [:block/uuid uuid])))) + +(defn- comment-thread-presence-key + [block] + (when-let [uuid (:block/uuid block)] + (when-let [repo (state/get-current-repo)] + [repo (str uuid)]))) + +(defn- fresh-comment-thread-presence + [cache-key] + (when-let [{:keys [checked-at] :as entry} (get @*comment-thread-presence cache-key)] + (when (< (- (js/Date.now) checked-at) comment-thread-presence-ttl-ms) + entry))) + +(defn- cache-comment-thread-presence! + [cache-key present?] + (swap! *comment-thread-presence + assoc + cache-key + {:present? (boolean present?) + :checked-at (js/Date.now)})) + +(declare flush-comment-thread-presence!) + +(defn- schedule-comment-thread-presence-check! + [state block] + (when-let [cache-key (comment-thread-presence-key block)] + (let [*comment-thread-present? (get state ::comment-thread-present?) + cached-entry (fresh-comment-thread-presence cache-key)] + (cond + (or (comments-model/protected-comment-block? block) + (ui-comment-thread-for-block block)) + (reset! *comment-thread-present? nil) + + cached-entry + (reset! *comment-thread-present? (:present? cached-entry)) + + :else + (do + (swap! *comment-thread-presence-requests + update + cache-key + (fnil conj #{}) + *comment-thread-present?) + (when (compare-and-set! *comment-thread-presence-flush-scheduled? false true) + (util/schedule flush-comment-thread-presence!))))))) + +(defn- flush-comment-thread-presence! + [] + (let [requests @*comment-thread-presence-requests] + (reset! *comment-thread-presence-requests {}) + (reset! *comment-thread-presence-flush-scheduled? false) + (doseq [[repo repo-requests] (group-by (fn [[cache-key _listeners]] (first cache-key)) + requests)] + (let [repo-cache-keys (mapv first repo-requests) + block-uuids (mapv second repo-cache-keys)] + (when (seq block-uuids) + (p/let [commented-block-uuids (comments-handler/ (get state ::comment-thread-present?) + (reset! (boolean thread))) + thread)))) + +(defn- open-comment-thread-for-block! + [state block comment-thread] + (if comment-thread + (open-comment-thread! (:block/uuid block) comment-thread) + (p/let [comment-thread (hydrate-comment-thread! state block)] + (when comment-thread + (open-comment-thread! (:block/uuid block) comment-thread))))) (defn- editing-user-for-block [block-uuid online-users current-user-uuid] @@ -2063,18 +2199,15 @@ (defn- editing-user-avatar [{:user/keys [name uuid]}] - (let [user-name (or name uuid) - initials (user-initials user-name) - color (when uuid (shui-util/uuid-color uuid))] - (when initials + (let [user-name (or name uuid)] + (when (avatar/initials user-name) [:span.block-editing-avatar-wrap - (shui/avatar + (avatar/user-avatar {:class "block-editing-avatar w-4 h-4 flex-none" - :title user-name} - (shui/avatar-fallback - {:style {:background-color (when color (str color "50")) - :font-size 9}} - initials))]))) + :title user-name + :name user-name + :uuid uuid + :fallback-props {:style {:font-size 9}}})]))) (rum/defcs ^:large-vars/cleanup-todo block-control < rum/reactive (rum/local false ::dragging?) @@ -3770,11 +3903,12 @@ config block (ldb/get-children block) - collapsed? - *hide-block-refs? - *show-query? - {:block-content-or-editor block-content-or-editor - :block-reactions block-reactions}) + collapsed? + *hide-block-refs? + *show-query? + {:block-content-or-editor block-content-or-editor + :block-reactions block-reactions} + {}) (block-content-or-editor config parsed-block {:edit-input-id edit-input-id @@ -3823,7 +3957,17 @@ ::show-query? (atom false) ::refs-count *refs-count ::plugin-renderer-error? (atom false) - ::use-plugin-renderer? (atom true))))} + ::use-plugin-renderer? (atom true) + ::hydrated-comment-thread (atom nil) + ::comment-thread-present? (atom nil)))) + :did-mount (fn [state] + (let [[_container-state _repo _config block] (:rum/args state)] + (schedule-comment-thread-presence-check! state block)) + state) + :did-update (fn [state] + (let [[_container-state _repo _config block] (:rum/args state)] + (schedule-comment-thread-presence-check! state block)) + state)} (mixins/event-mixin (fn [state] (let [*ref (::ref state)] @@ -3837,6 +3981,10 @@ show-query? (rum/react *show-query?) *plugin-renderer-error? (get state ::plugin-renderer-error?) *use-plugin-renderer? (get state ::use-plugin-renderer?) + *hydrated-comment-thread (get state ::hydrated-comment-thread) + hydrated-comment-thread (rum/react *hydrated-comment-thread) + *comment-thread-present? (get state ::comment-thread-present?) + comment-thread-present? (rum/react *comment-thread-present?) plugin-renderer-error? (rum/react *plugin-renderer-error?) use-plugin-renderer? (rum/react *use-plugin-renderer?) switch-to-plugin-renderer! (fn [] @@ -3896,7 +4044,13 @@ children (ldb/get-children block) comments-area? (comments-model/comments-area? block) comment-thread (when-not comments-area? - (first (comments-model/comment-threads-for-block block))) + (or (ui-comment-thread-for-block block) + (when (= uuid (:block-uuid hydrated-comment-thread)) + (:thread hydrated-comment-thread)))) + has-comment-thread? (or comment-thread + (true? comment-thread-present?)) + inline-thread (state/sub :comments/inline-thread) + show-inline-comments? (inline-comment-thread? inline-thread uuid comment-thread) protected-comment-block? (comments-model/protected-comment-block? block) page-icon (when (:page-title? config) (let [icon' (get block :logseq.property/icon)] @@ -3986,7 +4140,7 @@ (when (ldb/recycled? block) " line-through opacity-70") (when order-list? " is-order-list") (when comments-area? " is-comments-area") - (when comment-thread " has-comment-thread") + (when has-comment-thread? " has-comment-thread") (when (string/blank? title) " is-blank") (when original-block " embed-block")) :haschild (str (boolean has-child?)) @@ -4111,7 +4265,7 @@ ;; --- Original outline --- outline-view-cp) - (when (and comment-thread (not table?) (not property?)) + (when (and has-comment-thread? (not table?) (not property?)) (shui/button {:variant :ghost :size :icon @@ -4121,9 +4275,25 @@ :on-pointer-down util/stop :on-click (fn [e] (util/stop e) - (comments-handler/reveal-comments-area! comment-thread {:focus-editor? true}))} + (open-comment-thread-for-block! state block comment-thread))} (shui/tabler-icon "message-circle" {:size 15})))]) + (when show-inline-comments? + [:div.ls-inline-comments + (when-not (:page-title? config) + {:style {:padding-left (if (util/mobile?) 12 45)}}) + (block-comments/comments-area-view + (assoc config :container-id (comments-model/inline-comment-container-id (:container-id config))) + comment-thread + (ldb/get-children comment-thread) + false + *hide-block-refs? + *show-query? + {:block-content-or-editor block-content-or-editor + :block-reactions block-reactions} + {:focus-editor? true + :inline? true})]) + (when (and (not (:library? config)) (or (:tag-dialog? config) (and diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index 882e1019b0..820fcb9293 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -288,6 +288,21 @@ color: var(--ls-secondary-text-color); } +.ls-comments-targets-toggle { + margin-left: auto; + border: 0; + background: transparent; + color: var(--ls-secondary-text-color); + cursor: pointer; + font-size: 12px; + opacity: 0.72; +} + +.ls-comments-targets-toggle:hover { + color: var(--ls-primary-text-color); + opacity: 1; +} + .ls-comment-time { color: var(--ls-secondary-text-color); opacity: 0.72; @@ -299,6 +314,32 @@ padding: 4px 8px; } +.ls-comments-targets { + display: grid; + gap: 4px; + padding: 0 10px 8px; +} + +.ls-comments-target { + overflow: hidden; + padding: 4px 7px; + border-left: 2px solid var(--ls-link-text-color); + background: var(--ls-primary-background-color); + color: var(--ls-secondary-text-color); + font-size: 12px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ls-comments-target .page-reference { + display: inline; +} + +.ls-inline-comments { + margin-top: 4px; +} + .ls-comment-row { position: relative; display: flex; @@ -308,6 +349,7 @@ } .ls-comment-avatar { + @apply mt-1; display: flex; align-items: center; justify-content: center; @@ -315,10 +357,7 @@ width: 22px; height: 22px; border-radius: 50%; - background: var(--lx-gray-05, var(--rx-gray-05)); - color: var(--ls-primary-text-color); font-size: 10px; - font-weight: 600; } .ls-comment-main { diff --git a/src/main/frontend/components/block/comments.cljs b/src/main/frontend/components/block/comments.cljs index e53f83ba9d..b5e2c6a1e6 100644 --- a/src/main/frontend/components/block/comments.cljs +++ b/src/main/frontend/components/block/comments.cljs @@ -1,6 +1,7 @@ (ns frontend.components.block.comments (:require [clojure.string :as string] [dommy.core :as dom] + [frontend.components.avatar :as avatar] [frontend.components.block.comments-model :as comments-model] [frontend.components.icon :as icon-component] [frontend.config :as config] @@ -291,7 +292,7 @@ (rum/defc comment-row-view [config comment-block *hide-block-refs? *show-query? {:keys [block-content-or-editor block-reactions]}] (let [[editing? set-editing!] (hooks/use-state false) - {:keys [author avatar body created-at]} (comments-model/comment-row comment-block) + {:keys [author avatar-src author-uuid body created-at]} (comments-model/comment-row comment-block) current-user-uuid (user-handler/user-uuid) show-author? (comments-model/comment-author-visible? current-user-uuid) comment-uuid (:block/uuid comment-block) @@ -303,7 +304,12 @@ placeholder (t :block.comments/placeholder)] [:div.ls-comment-row (when show-author? - [:div.ls-comment-avatar avatar]) + (avatar/user-avatar + {:class "ls-comment-avatar" + :title author + :name author + :uuid author-uuid + :avatar-src avatar-src})) [:div.ls-comment-main [:div.ls-comment-meta (when show-author? @@ -371,13 +377,14 @@ (shui/tabler-icon "trash" {:size 14})))])])) (rum/defc add-comment-button - [config comments-block] + [config comments-block {:keys [focus-on-mount?]}] (let [placeholder (t :block.comments/placeholder)] [:div.ls-comment-add (comment-box {:config config :comments-block comments-block :placeholder placeholder + :focus-on-mount? focus-on-mount? :on-submit (fn [content] (comments-handler/insert-comment! comments-block content))})])) @@ -395,9 +402,25 @@ (dom/add-class! el "is-comment-thread-hovered") (dom/remove-class! el "is-comment-thread-hovered"))))) +(rum/defc comment-thread-targets-view + [comments-area] + (let [targets (comments-model/comment-thread-target-blocks comments-area)] + (when (> (count targets) 1) + [:div.ls-comments-targets + (for [target targets] + (let [uuid (:block/uuid target) + reference (state/get-component :block/reference)] + [:div.ls-comments-target + {:key (str uuid) + :data-block-id (str uuid)} + (if reference + (reference {} uuid) + (string/trim (or (:block/title target) "")))]))]))) + (rum/defc comments-area-view - [config block children collapsed? *hide-block-refs? *show-query? renderers] + [config block children collapsed? *hide-block-refs? *show-query? renderers {:keys [focus-editor? inline?]}] (let [*comments-list-ref (hooks/use-ref nil) + [targets-open? set-targets-open!] (hooks/use-state false) render-token (comments-model/comments-render-token children) summary (comments-model/comments-summary children) count (count children)] @@ -411,11 +434,13 @@ nil) [collapsed? render-token]) [:div.ls-comments-area - (when (comments-model/range-comments-area? block) - {:on-mouse-enter #(set-comment-thread-targets-hover! block true) - :on-mouse-leave #(set-comment-thread-targets-hover! block false) - :on-focus #(set-comment-thread-targets-hover! block true) - :on-blur #(set-comment-thread-targets-hover! block false)}) + (cond-> {} + inline? (assoc :class "ls-comments-area-inline") + (comments-model/range-comments-area? block) + (assoc :on-mouse-enter #(set-comment-thread-targets-hover! block true) + :on-mouse-leave #(set-comment-thread-targets-hover! block false) + :on-focus #(set-comment-thread-targets-hover! block true) + :on-blur #(set-comment-thread-targets-hover! block false))) (if collapsed? [:button.ls-comments-summary {:type "button" @@ -423,15 +448,21 @@ :on-click (fn [e] (util/stop e) (comments-handler/expand-comments-area! block))} - (if summary - (t :block.comments/collapsed-summary - (:count summary) - (:latest-author summary)) - (t :block.comments/count 0))] + (comments-model/collapsed-comments-label summary)] [:<> [:div.ls-comments-header [:span.ls-comments-label (t :block.comments/label)] - [:span.ls-comments-count count]] + [:span.ls-comments-count count] + (when (comments-model/comment-thread-targets-toggle-visible? block) + [:button.ls-comments-targets-toggle + {:type "button" + :on-pointer-down util/stop + :on-click (fn [e] + (util/stop e) + (set-targets-open! (not targets-open?)))} + (t :block.comments/on-those-blocks)])] + (when (comments-model/show-comment-thread-targets? block targets-open?) + (comment-thread-targets-view block)) (when (seq children) [:div.ls-comments-list {:ref *comments-list-ref} @@ -439,4 +470,4 @@ (rum/with-key (comment-row-view config comment-block *hide-block-refs? *show-query? renderers) (str (:block/uuid comment-block))))]) - (add-comment-button config block)])])) + (add-comment-button config block {:focus-on-mount? focus-editor?})])])) diff --git a/src/main/frontend/components/block/comments_model.cljs b/src/main/frontend/components/block/comments_model.cljs index 3615c5eeda..a5deaa353d 100644 --- a/src/main/frontend/components/block/comments_model.cljs +++ b/src/main/frontend/components/block/comments_model.cljs @@ -65,13 +65,59 @@ (remove :logseq.property/deleted-at) vec)) +(defn comment-thread-targets-toggle-visible? + [comments-area] + (> (count (comment-thread-target-blocks comments-area)) 1)) + +(defn show-comment-thread-targets? + [comments-area targets-open?] + (boolean + (and targets-open? + (comment-thread-targets-toggle-visible? comments-area)))) + +(defn comment-thread-click-action + [comments-area-visible?] + (if comments-area-visible? + :focus-comments-area + :show-inline-comments)) + +(defn next-inline-comment-thread + [current-inline-thread target-block-uuid comments-area-uuid] + (let [next-thread {:target-block-uuid (str target-block-uuid) + :comments-area-uuid (str comments-area-uuid)}] + (when-not (= current-inline-thread next-thread) + next-thread))) + +(defn inline-comment-container-id + [container-id] + (if (int? container-id) + container-id + :unknown-container)) + +(defn- child-comments-area? + [block comments-area] + (let [block-id (:db/id block) + parent-id (some-> comments-area :block/parent :db/id)] + (boolean + (and block-id + parent-id + (= block-id parent-id))))) + (defn comment-threads-for-block [block] (->> (get block :logseq.property.comments/_blocks) (filter comments-area?) + (remove #(child-comments-area? block %)) (remove :logseq.property/deleted-at) vec)) +(defn comment-thread-for-block + ([block] + (first (comment-threads-for-block block))) + ([rendered-block fresh-block] + (or (some-> fresh-block comment-thread-for-block) + (comment-thread-for-block rendered-block)))) + (defn- author-initials [author] (let [author (string/trim (str author))] @@ -90,6 +136,13 @@ string/trim not-empty)) +(defn- created-by-avatar-src + [block] + (some-> (:logseq.property/created-by-ref block) + :logseq.property.user/avatar + string/trim + not-empty)) + (defn- uuid-string [value] (cond @@ -106,10 +159,14 @@ (defn comment-row [block] (let [author (created-by-title block) + avatar-src (created-by-avatar-src block) + author-uuid (created-by-uuid block) created-at (:block/created-at block) updated-at (:block/updated-at block)] {:author author :avatar (author-initials author) + :avatar-src avatar-src + :author-uuid author-uuid :body (string/trim (or (:block/title block) "")) :created-at created-at :updated-at updated-at @@ -135,6 +192,14 @@ :latest-author (:author latest) :latest-created-at (:created-at latest)})))) +(defn collapsed-comments-label + [summary] + (if summary + (t :block.comments/collapsed-summary + (:count summary) + (:latest-author summary)) + (t :block.comments/label))) + (defn- same-local-day? [^js a ^js b] (and (= (.getFullYear a) (.getFullYear b)) diff --git a/src/main/frontend/components/header.cljs b/src/main/frontend/components/header.cljs index 6ce32db2ae..4f91dc89aa 100644 --- a/src/main/frontend/components/header.cljs +++ b/src/main/frontend/components/header.cljs @@ -6,6 +6,7 @@ [dommy.core :as d] [electron.ipc :as ipc] [frontend.common.missionary :as c.m] + [frontend.components.avatar :as avatar] [frontend.components.block :as component-block] [frontend.components.export :as export] [frontend.components.page-menu :as page-menu] @@ -36,7 +37,6 @@ [logseq.db :as ldb] [logseq.shui.hooks :as hooks] [logseq.shui.ui :as shui] - [logseq.shui.util :as shui-util] [missionary.core :as m] [promesa.core :as p] [reitit.frontend.easy :as rfe] @@ -103,17 +103,15 @@ (when (seq online-users) (for [{user-email :user/email user-name :user/name - user-uuid :user/uuid} online-users - :let [color (shui-util/uuid-color user-uuid)]] + user-uuid :user/uuid} online-users] (when user-name - (shui/avatar + (avatar/user-avatar {:class "w-5 h-5" :style {:app-region "no-drag"} - :title user-email} - (shui/avatar-fallback - {:style {:background-color (str color "50") - :font-size 11}} - (some-> (subs user-name 0 2) (string/upper-case)))))))]))) + :title user-email + :name user-name + :uuid user-uuid + :fallback-props {:style {:font-size 11}}}))))]))) (rum/defc left-menu-button < rum/reactive < {:key-fn #(identity "left-menu-toggle-button")} diff --git a/src/main/frontend/components/recycle.cljs b/src/main/frontend/components/recycle.cljs index 4e8f263c0b..af9dc7086f 100644 --- a/src/main/frontend/components/recycle.cljs +++ b/src/main/frontend/components/recycle.cljs @@ -1,7 +1,7 @@ (ns frontend.components.recycle "Recycle page UI" - (:require [clojure.string :as string] - [datascript.core :as d] + (:require [datascript.core :as d] + [frontend.components.avatar :as avatar] [frontend.components.block :as component-block] [frontend.context.i18n :as i18n :refer [t]] [frontend.db :as db] @@ -23,14 +23,6 @@ (vector? value) (d/entity db value) :else nil)) -(defn- user-initials - [user] - (let [name (or (:logseq.property.user/name user) - (:block/title user) - "U") - name (string/trim name)] - (subs name 0 (min 2 (count name))))) - (defn- deleted-roots [db] (->> (d/q '[:find [?e ...] @@ -67,12 +59,12 @@ (defn- deleted-by-avatar [user] - (let [avatar-src (:logseq.property.user/avatar user)] - (shui/avatar - {:class "w-4 h-4"} - (when (seq avatar-src) - (shui/avatar-image {:src avatar-src})) - (shui/avatar-fallback (user-initials user))))) + (avatar/user-avatar + {:class "w-4 h-4" + :name (or (:logseq.property.user/name user) + (:block/title user) + "U") + :avatar-src (:logseq.property.user/avatar user)})) (defn- deleted-root-header [db root] diff --git a/src/main/frontend/components/repo.cljs b/src/main/frontend/components/repo.cljs index 75a3c3b676..c2434b6d5f 100644 --- a/src/main/frontend/components/repo.cljs +++ b/src/main/frontend/components/repo.cljs @@ -119,10 +119,9 @@ dialog-config {:cancel-label (t :ui/cancel) :ok-label (t :ui/confirm)}] (-> (shui/dialog-confirm! - [:p.font-medium.-my-4 (t :graph/delete-local-confirm-desc graph-name) - [:span.my-2.flex.font-normal.opacity-75 - [:small (t :graph/delete-warning)]]] - dialog-config) + (assoc dialog-config + :title (t :graph/delete-local-confirm-desc graph-name) + :description (t :graph/delete-warning))) (p/then (fn [] (repo-handler/remove-repo! repo)))))) @@ -198,10 +197,9 @@ :on-click (fn [] (let [prompt-str (t :graph/delete-server-confirm-desc graph-name)] (-> (shui/dialog-confirm! - [:p.font-medium.-my-4 prompt-str - [:span.my-2.flex.font-normal.opacity-75 - [:small (t :graph/delete-warning)]]] - dialog-config) + (assoc dialog-config + :title prompt-str + :description (t :graph/delete-warning))) (p/then (fn [] (state/set-state! :rtc/loading-graphs? true) diff --git a/src/main/frontend/handler/comments.cljs b/src/main/frontend/handler/comments.cljs index c604d86fe1..acd52ae809 100644 --- a/src/main/frontend/handler/comments.cljs +++ b/src/main/frontend/handler/comments.cljs @@ -3,6 +3,7 @@ (:require [clojure.string :as string] [frontend.components.block.comments-model :as comments-model] [frontend.db :as db] + [frontend.db.async :as db-async] [frontend.handler.block :as block-handler] [frontend.handler.db-based.property :as db-property-handler] [frontend.handler.editor :as editor-handler] @@ -52,6 +53,90 @@ [block] [:block/uuid (:block/uuid block)]) +(def ^:private comment-thread-pull-selector + '[:db/id + :block/uuid + :block/title + :block/order + :block/created-at + :block/updated-at + :logseq.property/deleted-at + {:block/tags [:db/id :db/ident]} + {:block/parent [:db/id :block/uuid]} + {:logseq.property.comments/blocks [:db/id :block/uuid :block/title :logseq.property/deleted-at]}]) + +(defn > threads + (keep (fn [thread] + (db/entity [:block/uuid (:block/uuid thread)]))) + vec)))) + +(defn > block-uuids + (keep (fn [block-uuid] + (cond + (uuid? block-uuid) + block-uuid + + (and (string? block-uuid) (util/uuid-string? block-uuid)) + (uuid block-uuid) + + :else + nil))) + vec)] + (when (and repo (seq block-uuids)) + (p/let [result (db-async/") ; turn this block into a quote block + (and (not comment-editor?) (= value ">")) ; turn this block into a quote block (do (state/set-edit-content! (.-id input) "") (state/pub-event! [:editor/upsert-type-block {:block (assoc (state/get-edit-block) :block/title "") diff --git a/src/main/frontend/worker/db/migrate.cljs b/src/main/frontend/worker/db/migrate.cljs index 125e7961ec..40d8033038 100644 --- a/src/main/frontend/worker/db/migrate.cljs +++ b/src/main/frontend/worker/db/migrate.cljs @@ -80,6 +80,18 @@ (map (fn [comment-id] [:db/add comment-id :block/tags :logseq.class/Comment])))) +(defn- add-single-block-comment-targets + [db] + (->> (d/q '[:find ?comments-area-id ?parent-id + :where + [?comments-area-id :block/tags :logseq.class/Comments] + [?comments-area-id :block/parent ?parent-id]] + db) + (keep (fn [[comments-area-id parent-id]] + (let [comments-area (d/entity db comments-area-id)] + (when-not (seq (:logseq.property.comments/blocks comments-area)) + [:db/add comments-area-id :logseq.property.comments/blocks parent-id])))))) + (def schema-version->updates "A vec of tuples defining datascript migrations. Each tuple consists of the schema version integer and a migration map. A migration map can have keys of :properties, :classes @@ -113,7 +125,8 @@ ["65.27" {:classes [:logseq.class/Comments] :properties [:logseq.property.comments/blocks]}] ["65.28" {:classes [:logseq.class/Comment] - :fix tag-comment-blocks}]]) + :fix tag-comment-blocks}] + ["65.29" {:fix add-single-block-comment-targets}]]) (let [[major minor] (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first) schema-version->updates)))] diff --git a/src/resources/dicts/en.edn b/src/resources/dicts/en.edn index 9a4b09d485..0e32821db6 100644 --- a/src/resources/dicts/en.edn +++ b/src/resources/dicts/en.edn @@ -113,9 +113,9 @@ :block.comments/add-comment "Add comment" :block.comments/add-comment-command-desc "Add a comment to this block." :block.comments/collapsed-summary (fn [n author] (str n (if (= 1 n) " comment" " comments") " · latest from " author)) - :block.comments/count (fn [n] (str n (if (= 1 n) " comment" " comments"))) :block.comments/date-at-time "{1} at {2}" :block.comments/label "Comments" + :block.comments/on-those-blocks "On those blocks" :block.comments/placeholder "Reply..." :block.comments/yesterday-at "Yesterday at {1}" diff --git a/src/resources/dicts/zh-cn.edn b/src/resources/dicts/zh-cn.edn index 003143ecb3..8cfcab1071 100644 --- a/src/resources/dicts/zh-cn.edn +++ b/src/resources/dicts/zh-cn.edn @@ -110,9 +110,9 @@ :block.comments/add-comment "添加评论" :block.comments/add-comment-command-desc "为此块添加评论。" :block.comments/collapsed-summary "{1} 条评论 · 最新来自 {2}" - :block.comments/count "{1} 条评论" :block.comments/date-at-time "{1} {2}" :block.comments/label "评论" + :block.comments/on-those-blocks "在这些块上" :block.comments/placeholder "回复..." :block.comments/yesterday-at "昨天 {1}" diff --git a/src/test/frontend/components/block/comments_model_test.cljs b/src/test/frontend/components/block/comments_model_test.cljs deleted file mode 100644 index 9824df3fc6..0000000000 --- a/src/test/frontend/components/block/comments_model_test.cljs +++ /dev/null @@ -1,328 +0,0 @@ -(ns frontend.components.block.comments-model-test - (:require [cljs.test :refer [deftest is testing]] - [frontend.components.block.comments-model :as comments-model] - [frontend.context.i18n :refer [t]] - [goog.object :as gobj])) - -(deftest comments-area-detection - (testing "detects blocks tagged with the built-in Comments tag" - (is (true? (comments-model/comments-area? - {:block/tags [{:db/ident :logseq.class/Comments}]})))) - - (testing "does not treat ordinary blocks as comment areas" - (is (false? (comments-model/comments-area? - {:block/title "Comments"}))) - (is (false? (comments-model/comments-area? - {:block/tags [{:db/ident :logseq.class/Task}]}))))) - -(deftest comment-move-guards - (let [comments-area {:block/tags [{:db/ident :logseq.class/Comments}]} - comment-block {:block/title "comment" - :block/parent comments-area} - tagged-comment-block {:block/title "tagged comment" - :block/tags [{:db/ident :logseq.class/Comment}]} - ordinary-block {:block/title "ordinary"} - ordinary-target {:block/title "target"}] - (testing "comment area and its children are protected comment blocks" - (is (true? (comments-model/protected-comment-block? comments-area))) - (is (true? (comments-model/protected-comment-block? comment-block))) - (is (true? (comments-model/protected-comment-block? tagged-comment-block))) - (is (false? (comments-model/protected-comment-block? ordinary-block)))) - (testing "comment blocks cannot be moved" - (is (false? (comments-model/move-allowed? [comments-area] ordinary-target))) - (is (false? (comments-model/move-allowed? [comment-block] ordinary-target))) - (is (false? (comments-model/move-allowed? [tagged-comment-block] ordinary-target)))) - (testing "ordinary blocks cannot be moved to comment blocks" - (is (false? (comments-model/move-allowed? [ordinary-block] comments-area))) - (is (false? (comments-model/move-allowed? [ordinary-block] comment-block))) - (is (false? (comments-model/move-allowed? [ordinary-block] tagged-comment-block))) - (is (false? (comments-model/move-allowed? [ordinary-block] comments-area {:sibling? true}))) - (is (false? (comments-model/move-allowed? [ordinary-block] tagged-comment-block {:sibling? true})))) - (testing "ordinary moves outside comments stay allowed" - (is (true? (comments-model/move-allowed? [ordinary-block] ordinary-target)))))) - -(deftest comment-target-blocks - (let [comments-area {:block/title "Comments" - :block/tags [{:db/ident :logseq.class/Comments}]} - comment-block {:block/title "comment" - :block/parent comments-area} - first-block {:block/title "first"} - second-block {:block/title "second"}] - (is (= [first-block second-block] - (comments-model/comment-target-blocks - [first-block comments-area comment-block first-block second-block])) - "Selected-block comment actions should target normal blocks only once"))) - -(deftest range-comment-threads - (let [target-a {:block/title "a"} - target-b {:block/title "b"} - deleted-target {:block/title "deleted" - :logseq.property/deleted-at 1} - comments-area {:block/title "Comments" - :block/tags [{:db/ident :logseq.class/Comments}] - comments-model/comments-blocks-property [target-a deleted-target target-b]} - deleted-comments-area (assoc comments-area :logseq.property/deleted-at 1) - ordinary-block {:block/title "ordinary" - :logseq.property.comments/_blocks [comments-area deleted-comments-area]}] - (testing "range comment areas are comments blocks that point at target blocks" - (is (true? (comments-model/range-comments-area? comments-area))) - (is (false? (comments-model/range-comments-area? - {:block/tags [{:db/ident :logseq.class/Comments}]})))) - (testing "deleted targets do not participate in the rendered target set" - (is (= [target-a target-b] - (comments-model/comment-thread-target-blocks comments-area)))) - (testing "blocks expose live comment threads through the reverse property" - (is (= [comments-area] - (comments-model/comment-threads-for-block ordinary-block)))))) - -(deftest comment-row-derivation - (testing "uses the created-by ref as the comment author" - (is (= {:author "tienson" - :avatar "T" - :body "tienson: push PR" - :created-at 1710000000000 - :updated-at 1710000000000 - :edited? false} - (comments-model/comment-row - {:block/title "tienson: push PR" - :logseq.property/created-by-ref {:block/title "tienson"} - :block/created-at 1710000000000 - :block/updated-at 1710000000000})))) - - (testing "does not invent an author when created-by ref is absent" - (is (= {:author nil - :avatar "" - :body "review again" - :created-at nil - :updated-at nil - :edited? false} - (comments-model/comment-row {:block/title "review again"})))) - - (testing "marks comments edited only when updated later than created" - (is (true? (:edited? (comments-model/comment-row - {:block/title "me: updated" - :logseq.property/created-by-ref {:block/title "me"} - :block/created-at 10 - :block/updated-at 20})))))) - -(deftest comment-author-visibility - (let [visible? (resolve 'frontend.components.block.comments-model/comment-author-visible?)] - (is (fn? visible?)) - - (testing "hides comment avatar and username when there is no logged-in user" - (when (fn? visible?) - (is (false? (visible? nil))) - (is (false? (visible? ""))))) - - (testing "shows comment avatar and username when a user is logged in" - (when (fn? visible?) - (is (true? (visible? #uuid "6a073572-fefe-44c5-8b43-267ccc715077"))) - (is (true? (visible? "6a073572-fefe-44c5-8b43-267ccc715077"))))))) - -(deftest comments-summary - (testing "summarizes count and latest author by timestamp" - (is (= {:count 2 - :latest-author "tienson" - :latest-created-at 30} - (comments-model/comments-summary - [{:block/title "review again" - :logseq.property/created-by-ref {:block/title "zhiyuan"} - :block/created-at 10} - {:block/title "push PR" - :logseq.property/created-by-ref {:block/title "tienson"} - :block/created-at 30}])))) - - (testing "returns no summary for empty comment areas" - (is (nil? (comments-model/comments-summary []))))) - -(deftest comment-count-labels - (testing "uses singular English labels for one comment" - (is (= "1 comment" (t :block.comments/count 1))) - (is (= "1 comment · latest from tienson" - (t :block.comments/collapsed-summary 1 "tienson")))) - - (testing "uses plural English labels for other counts" - (is (= "0 comments" (t :block.comments/count 0))) - (is (= "2 comments" (t :block.comments/count 2))))) - -(deftest comment-time-label - (let [time-label (resolve 'frontend.components.block.comments-model/comment-time-label) - now (js/Date. 2026 4 18 9 0) - today (.getTime (js/Date. 2026 4 18 17 5)) - yesterday (.getTime (js/Date. 2026 4 17 17 5)) - older (.getTime (js/Date. 2026 3 5 17 5))] - (is (fn? time-label)) - (when (fn? time-label) - (is (= "5:05 PM" (time-label today now)) - "Today's comments should display only the time") - (is (= "Yesterday at 5:05 PM" (time-label yesterday now)) - "Yesterday's comments should include the relative day") - (is (= "Apr 5, 2026 at 5:05 PM" (time-label older now)) - "Older comments should include the date and time") - (is (nil? (time-label nil now)))))) - -(deftest comment-submit-content - (let [submit-content (resolve 'frontend.components.block.comments-model/submittable-comment-content)] - (testing "keeps comment drafts local until an explicit submit asks for content" - (is (fn? submit-content))) - - (testing "returns trimmed content for submitted create and edit drafts" - (when (fn? submit-content) - (is (= "ship comment box" (submit-content " ship comment box "))))) - - (testing "does not submit blank drafts" - (when (fn? submit-content) - (is (nil? (submit-content " \n\t "))) - (is (nil? (submit-content nil))))))) - -(deftest comment-ownership - (let [owned? (resolve 'frontend.components.block.comments-model/comment-owned-by?) - user-id #uuid "6a073572-fefe-44c5-8b43-267ccc715077" - other-id #uuid "fd94c4c7-bfb8-49d5-bbb1-46617e4f2154"] - (testing "detects comments created by the current user" - (is (fn? owned?)) - (when (fn? owned?) - (is (true? (owned? {:logseq.property/created-by-ref {:block/uuid user-id}} - (str user-id)))) - (is (true? (owned? {:logseq.property/created-by-ref {:block/uuid (str user-id)}} - user-id))))) - - (testing "does not allow ownership without matching created-by ref" - (when (fn? owned?) - (is (false? (owned? {:logseq.property/created-by-ref {:block/uuid other-id}} - (str user-id)))) - (is (false? (owned? {} (str user-id)))) - (is (true? (owned? {} nil))) - (is (false? (owned? {:logseq.property/created-by-ref {:block/uuid user-id}} - nil))))))) - -(deftest comment-actions - (let [actions (resolve 'frontend.components.block.comments-model/comment-actions) - user-id #uuid "6a073572-fefe-44c5-8b43-267ccc715077" - other-id #uuid "fd94c4c7-bfb8-49d5-bbb1-46617e4f2154"] - (testing "exposes reaction for comments created by other users" - (is (fn? actions)) - (when (fn? actions) - (is (= [:reaction] - (actions {:logseq.property/created-by-ref {:block/uuid other-id}} - (str user-id)))))) - - (testing "exposes edit and delete when logged out and created-by ref is absent" - (when (fn? actions) - (is (= [:reaction :edit :delete] - (actions {} nil))))) - - (testing "exposes edit and delete only for comments created by current user" - (when (fn? actions) - (is (= [:reaction :edit :delete] - (actions {:logseq.property/created-by-ref {:block/uuid user-id}} - (str user-id)))))))) - -(deftest comment-edit-cursor-position - (let [cursor-position (resolve 'frontend.components.block.comments-model/comment-edit-cursor-position)] - (testing "places the edit cursor at the end of the current comment" - (is (fn? cursor-position)) - (when (fn? cursor-position) - (is (= 11 (cursor-position "hello world"))) - (is (= 3 (cursor-position "a\nb"))) - (is (= 0 (cursor-position nil))))))) - -(deftest comment-submit-shortcut - (let [shortcut? (resolve 'frontend.components.block.comments-model/comment-submit-shortcut?)] - (testing "accepts enter without requiring a modifier" - (is (fn? shortcut?)) - (when (fn? shortcut?) - (is (true? (shortcut? #js {:key "Enter"}))) - (is (true? (shortcut? #js {:key "Enter" :metaKey true}))))) - - (testing "keeps shift-enter and non-enter keys available for editing" - (when (fn? shortcut?) - (is (false? (shortcut? #js {:key "Enter" :metaKey true :shiftKey true}))) - (is (false? (shortcut? #js {:key "a" :metaKey true}))))) - - (testing "does not submit while editor commands or autocomplete are active" - (when (fn? shortcut?) - (is (false? (shortcut? #js {:key "Enter"} :commands))) - (is (false? (shortcut? #js {:key "Enter"} :block-search))) - (is (false? (shortcut? #js {:key "Enter"} :page-search))))))) - -(deftest comments-render-token - (let [render-token (resolve 'frontend.components.block.comments-model/comments-render-token)] - (testing "tracks comment identity and content changes for scroll effects" - (is (fn? render-token)) - (when (fn? render-token) - (is (= [[#uuid "6a073572-fefe-44c5-8b43-267ccc715077" "first" 10] - [#uuid "fd94c4c7-bfb8-49d5-bbb1-46617e4f2154" "second" 20]] - (render-token - [{:block/uuid #uuid "6a073572-fefe-44c5-8b43-267ccc715077" - :block/title "first" - :block/updated-at 10} - {:block/uuid #uuid "fd94c4c7-bfb8-49d5-bbb1-46617e4f2154" - :block/title "second" - :block/updated-at 20}]))))))) - -(deftest comment-draft-block - (let [draft-block (resolve 'frontend.components.block.comments-model/comment-draft-block) - draft-id #uuid "a477a8fe-10fb-443b-9d59-45ee476931e8" - comments-id #uuid "6a073572-fefe-44c5-8b43-267ccc715077" - page-id #uuid "fd94c4c7-bfb8-49d5-bbb1-46617e4f2154" - comments-block {:block/uuid comments-id - :block/page {:block/uuid page-id}}] - (testing "builds a temporary block shaped for the normal editor" - (is (fn? draft-block)) - (when (fn? draft-block) - (is (= {:block/uuid draft-id - :block/title "draft" - :block/format :markdown - :block/page {:block/uuid page-id} - :block/parent comments-block} - (draft-block comments-block draft-id "draft"))))))) - -(deftest comment-draft-storage - (let [draft-key (resolve 'frontend.components.block.comments-model/comment-draft-storage-key) - load-draft (resolve 'frontend.components.block.comments-model/saved-comment-draft) - save-draft! (resolve 'frontend.components.block.comments-model/save-comment-draft!) - clear-draft! (resolve 'frontend.components.block.comments-model/clear-comment-draft!) - comments-id #uuid "6a073572-fefe-44c5-8b43-267ccc715077" - comments-block {:block/uuid comments-id}] - (is (fn? draft-key)) - (is (fn? load-draft)) - (is (fn? save-draft!)) - (is (fn? clear-draft!)) - (when (every? fn? [draft-key load-draft save-draft! clear-draft!]) - (is (= (str "comments-" comments-id "-draft") - (draft-key comments-block))) - (let [items (atom {}) - old-storage (gobj/get js/globalThis "localStorage") - storage (js-obj - "getItem" (fn [key] (get @items key)) - "setItem" (fn [key value] (swap! items assoc key value)) - "removeItem" (fn [key] (swap! items dissoc key)))] - (try - (gobj/set js/globalThis "localStorage" storage) - (save-draft! comments-block " draft body\n") - (is (= " draft body\n" (load-draft comments-block)) - "Non-blank drafts should be restored exactly") - (save-draft! comments-block " ") - (is (nil? (load-draft comments-block)) - "Blank drafts should not leave stale local storage entries") - (save-draft! comments-block "another draft") - (clear-draft! comments-block) - (is (nil? (load-draft comments-block)) - "Submitted comments should clear the stored draft") - (finally - (if old-storage - (gobj/set js/globalThis "localStorage" old-storage) - (gobj/remove js/globalThis "localStorage")))))))) - -(deftest comments-block-current-page - (let [current-page? (resolve 'frontend.components.block.comments-model/comments-block-current-page?) - comments-id #uuid "6a073572-fefe-44c5-8b43-267ccc715077"] - (is (fn? current-page?)) - (when (fn? current-page?) - (is (true? (current-page? {:block/uuid comments-id} - (str comments-id)))) - (is (false? (current-page? {:block/uuid comments-id} - "fd94c4c7-bfb8-49d5-bbb1-46617e4f2154"))) - (is (false? (current-page? {:block/uuid comments-id} nil))) - (is (false? (current-page? {} (str comments-id))))))) diff --git a/src/test/frontend/handler/comments_test.cljs b/src/test/frontend/handler/comments_test.cljs new file mode 100644 index 0000000000..79a7f65e99 --- /dev/null +++ b/src/test/frontend/handler/comments_test.cljs @@ -0,0 +1,98 @@ +(ns frontend.handler.comments-test + (:require [cljs.test :refer [async deftest is testing]] + [frontend.components.block.comments-model :as comments-model] + [frontend.db :as db] + [frontend.handler.comments :as comments-handler] + [frontend.handler.db-based.property :as db-property-handler] + [frontend.handler.editor :as editor-handler] + [promesa.core :as p])) + +(deftest ensure-comments-area-adds-target-property-for-single-block-comments + (async done + (let [target-uuid #uuid "11111111-1111-1111-1111-111111111111" + comments-uuid #uuid "22222222-2222-2222-2222-222222222222" + target {:db/id 1 + :block/uuid target-uuid + :block/title "target"} + comments-area {:db/id 2 + :block/uuid comments-uuid + :block/title "Comments" + :block/tags [{:db/ident comments-model/comments-tag-ident}]} + inserted (atom nil) + property-updates (atom [])] + (-> (p/with-redefs [db/entity (fn [lookup] + (case lookup + [:block/uuid target-uuid] target + nil)) + editor-handler/api-insert-new-block! + (fn [title opts] + (reset! inserted {:title title :opts opts}) + (p/resolved comments-area)) + db-property-handler/set-block-property! + (fn [block-id property value] + (swap! property-updates conj [block-id property value]) + (p/resolved nil)) + editor-handler/expand-block! (fn [_] (p/resolved nil))] + (comments-handler/ensure-comments-area! target-uuid)) + (p/then + (fn [result] + (is (= comments-area result)) + (is (contains? (get-in @inserted [:opts :other-attrs]) + comments-model/comments-blocks-property)) + (is (= #{[:block/uuid target-uuid]} + (get-in @inserted [:opts :other-attrs comments-model/comments-blocks-property]))) + (is (empty? @property-updates) + "New single-block comment areas should be created with the target property") + (done))))))) + +(deftest ensure-comments-area-backfills-existing-single-block-comments + (async done + (let [target-uuid #uuid "11111111-1111-1111-1111-111111111111" + comments-uuid #uuid "22222222-2222-2222-2222-222222222222" + comments-area {:db/id 2 + :block/uuid comments-uuid + :block/title "Comments" + :block/tags [{:db/ident comments-model/comments-tag-ident}]} + target {:db/id 1 + :block/uuid target-uuid + :block/title "target" + :block/_parent [comments-area]} + property-updates (atom [])] + (-> (p/with-redefs [db/entity (fn [lookup] + (case lookup + [:block/uuid target-uuid] target + nil)) + db/sort-by-order identity + db-property-handler/set-block-property! + (fn [block-id property value] + (swap! property-updates conj [block-id property value]) + (p/resolved nil)) + editor-handler/api-insert-new-block! + (fn [& _] + (throw (js/Error. "should not insert a duplicate comments area")))] + (comments-handler/ensure-comments-area! target-uuid)) + (p/then + (fn [result] + (is (= comments-area result)) + (is (= [[(:db/id comments-area) + comments-model/comments-blocks-property + #{[:block/uuid target-uuid]}]] + @property-updates)) + (done))))))) + +(deftest deleted-comment-thread-actions-are-no-ops + (testing "expand ignores stale comment thread data when the comments block is gone" + (let [expanded (atom [])] + (with-redefs [db/entity (constantly nil) + editor-handler/expand-block! (fn [block-id] (swap! expanded conj block-id))] + (comments-handler/expand-comments-area! + {:block/uuid #uuid "22222222-2222-2222-2222-222222222222"}) + (is (empty? @expanded))))) + + (testing "reveal ignores stale comment thread data when the comments block is gone" + (let [expanded (atom [])] + (with-redefs [db/entity (constantly nil) + editor-handler/expand-block! (fn [block-id] (swap! expanded conj block-id))] + (comments-handler/reveal-comments-area! + {:block/uuid #uuid "22222222-2222-2222-2222-222222222222"}) + (is (empty? @expanded)))))) diff --git a/src/test/frontend/handler/editor_async_test.cljs b/src/test/frontend/handler/editor_async_test.cljs index f884c37ea5..679bfb61d2 100644 --- a/src/test/frontend/handler/editor_async_test.cljs +++ b/src/test/frontend/handler/editor_async_test.cljs @@ -421,9 +421,10 @@ :opts {:block-uuid block-uuid :end? true :edit-block? false - :other-attrs {:block/tags #{comments-model/comments-tag-ident}}}}] + :other-attrs {:block/tags #{comments-model/comments-tag-ident} + comments-model/comments-blocks-property #{[:block/uuid block-uuid]}}}}] @inserts) - "Single-block comments area should be inserted as a child without range targets") + "Single-block comments area should be inserted as a child with the target property") (is (= [created-comments-area-uuid] @expanded) "The single-block comments area should be expanded inline")))))) diff --git a/src/test/frontend/handler/editor_test.cljs b/src/test/frontend/handler/editor_test.cljs index 1401bc40f4..4d8881a0d0 100644 --- a/src/test/frontend/handler/editor_test.cljs +++ b/src/test/frontend/handler/editor_test.cljs @@ -439,9 +439,29 @@ :cursor-pos (dec (count "`String#gsub and String#`"))}) (is (= nil (state/get-editor-action)) "No page search within backticks")) + + (testing "Comment editors do not open tag autocompletion" + (handle-last-input-handler {:value "#" + :cursor-pos 1 + :editor-config {:comment-editor? true}}) + (is (= nil (state/get-editor-action)) + "No tag search in comment editors")) ;; Reset state (state/set-editor-action! nil)) +(deftest comment-editor-quote-trigger-does-not-convert-draft-block + (let [input #js {:id "edit-block-test" + :value ">"} + events (atom [])] + (with-redefs [cursor/pos (constantly 1) + state/get-editor-args (constantly [nil nil {:comment-editor? true}]) + state/set-edit-content! (fn [& _]) + state/pub-event! (fn [event] (swap! events conj event)) + editor/default-case-for-keyup-handler (fn [& _])] + ((editor/keyup-handler nil input) #js {:key ">"} nil) + (is (empty? @events) + "Comment editor > should stay plain text instead of converting the draft to a quote block")))) + (deftest comment-editor-collapse-expand-shortcuts-do-not-touch-draft-blocks (let [draft-uuid #uuid "6a073572-fefe-44c5-8b43-267ccc715077" expanded (atom []) diff --git a/src/test/frontend/worker/migrate_test.cljs b/src/test/frontend/worker/migrate_test.cljs index 41ee384ff5..270fc71427 100644 --- a/src/test/frontend/worker/migrate_test.cljs +++ b/src/test/frontend/worker/migrate_test.cljs @@ -144,3 +144,45 @@ (is (= #{:logseq.class/Comments} (set (map :db/ident (:block/tags (d/entity @conn [:block/uuid comments-area-uuid])))))) (is (empty? (:block/tags (d/entity @conn [:block/uuid ordinary-child-uuid])))))) + +(deftest migrate-65-29-adds-single-block-comment-targets + (let [conn (d/create-conn db-schema/schema) + target-uuid #uuid "11111111-1111-1111-1111-111111111111" + comments-area-uuid #uuid "22222222-2222-2222-2222-222222222222" + range-comments-uuid #uuid "33333333-3333-3333-3333-333333333333" + range-target-uuid #uuid "44444444-4444-4444-4444-444444444444"] + (d/transact! conn + [{:db/ident :logseq.kv/schema-version + :kv/value {:major 65 :minor 28}} + {:db/ident :logseq.class/Comments + :block/title "Comments"} + {:block/uuid target-uuid + :block/title "target"} + {:block/uuid comments-area-uuid + :block/title "Comments" + :block/parent [:block/uuid target-uuid] + :block/tags #{:logseq.class/Comments}} + {:block/uuid range-target-uuid + :block/title "range target"} + {:block/uuid range-comments-uuid + :block/title "Comments" + :block/tags #{:logseq.class/Comments}}]) + (d/transact! conn + [[:db/add + (:db/id (d/entity @conn [:block/uuid range-comments-uuid])) + :logseq.property.comments/blocks + (:db/id (d/entity @conn [:block/uuid range-target-uuid]))]]) + + (db-migrate/migrate conn :target-version {:major 65 :minor 29}) + + (is (= {:major 65 :minor 29} + (:kv/value (d/entity @conn :logseq.kv/schema-version)))) + (is (= #{(:db/id (d/entity @conn [:block/uuid target-uuid]))} + (set (map :db/id + (:logseq.property.comments/blocks + (d/entity @conn [:block/uuid comments-area-uuid])))))) + (is (= #{(:db/id (d/entity @conn [:block/uuid range-target-uuid]))} + (set (map :db/id + (:logseq.property.comments/blocks + (d/entity @conn [:block/uuid range-comments-uuid]))))) + "Existing range comment targets should be preserved")))