fix: comment issues

This commit is contained in:
Tienson Qin
2026-05-20 02:15:51 +08:00
parent 29854b9708
commit 8e48079bfb
20 changed files with 690 additions and 425 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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)))))

View File

@@ -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-comment-thread-block-uuids repo block-uuids)
commented-block-uuids (into #{} commented-block-uuids)]
(doseq [[cache-key listeners] repo-requests
:let [present? (contains? commented-block-uuids (second cache-key))]]
(cache-comment-thread-presence! cache-key present?)
(doseq [*listener listeners]
(reset! *listener present?)))))))))
(defn- hydrate-comment-thread!
[state block]
(when-let [uuid (:block/uuid block)]
(let [*hydrated-comment-thread (get state ::hydrated-comment-thread)
cache-key (comment-thread-presence-key block)]
(p/let [threads (comments-handler/<get-comment-threads-for-block uuid)
thread (or (ui-comment-thread-for-block block)
(comments-model/comment-thread-for-block
(assoc block :logseq.property.comments/_blocks threads)))]
(when cache-key
(cache-comment-thread-presence! cache-key thread))
(reset! *hydrated-comment-thread (when thread
{:block-uuid uuid
:thread thread}))
(some-> (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

View File

@@ -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 {

View File

@@ -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?})])]))

View File

@@ -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))

View File

@@ -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")}

View File

@@ -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]

View File

@@ -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)

View File

@@ -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 <get-comment-threads-for-block
[block-uuid]
(when-let [repo (and block-uuid (state/get-current-repo))]
(p/let [threads (db-async/<q repo
{:transact-db? true}
'[:find [(pull ?comments-area ?selector) ...]
:in $ ?block-uuid ?selector
:where
[?block :block/uuid ?block-uuid]
[?comments-area :logseq.property.comments/blocks ?block]
[?comments-area :block/tags :logseq.class/Comments]
[(missing? $ ?comments-area :logseq.property/deleted-at)]]
block-uuid
comment-thread-pull-selector)
_ (p/all
(mapv (fn [thread]
(db-async/<get-block repo
(:block/uuid thread)
{:children? true
:include-collapsed-children? true
:skip-refresh? true}))
threads))]
(->> threads
(keep (fn [thread]
(db/entity [:block/uuid (:block/uuid thread)])))
vec))))
(defn <get-comment-thread-block-uuids
([block-uuids]
(<get-comment-thread-block-uuids (state/get-current-repo) block-uuids))
([repo block-uuids]
(let [block-uuids (->> 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/<q repo
{:transact-db? false}
'[:find [?block-uuid ...]
:in $ [?block-uuid ...]
:where
[?block :block/uuid ?block-uuid]
[?comments-area :logseq.property.comments/blocks ?block]
[?comments-area :block/tags :logseq.class/Comments]
[(missing? $ ?comments-area :logseq.property/deleted-at)]]
block-uuids)]
(mapv str result))))))
(defn- single-comment-targets
[block]
#{(block-lookup-ref block)})
(defn- comments-area-entity
[comments-area]
(when-let [uuid (:block/uuid comments-area)]
(db/entity [:block/uuid uuid])))
(defn- ensure-single-comment-target-property!
[comments-area block]
(when (and comments-area block (not (seq (get comments-area comments-model/comments-blocks-property))))
(db-property-handler/set-block-property! (:db/id comments-area)
comments-model/comments-blocks-property
(single-comment-targets block))))
(defn- comments-area-title
[block]
(if (ldb/page? block)
@@ -68,12 +153,14 @@
[block-id]
(when-let [block (db/entity [:block/uuid block-id])]
(if-let [comments-area (comments-area-child block)]
(p/resolved comments-area)
(p/let [_ (ensure-single-comment-target-property! comments-area block)]
comments-area)
(editor-handler/api-insert-new-block!
(comments-area-title block)
(merge {:block-uuid block-id
: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 (single-comment-targets block)}}
(comments-area-insert-position block))))))
(defn- same-comment-targets?
@@ -131,7 +218,7 @@
([comments-area]
(reveal-comments-area! comments-area nil))
([comments-area {:keys [focus-editor?]}]
(when-let [uuid (:block/uuid comments-area)]
(when-let [uuid (:block/uuid (comments-area-entity comments-area))]
(p/do!
(editor-handler/expand-block! uuid)
(js/requestAnimationFrame
@@ -147,7 +234,7 @@
(defn expand-comments-area!
[comments-area]
(when-let [uuid (:block/uuid comments-area)]
(when-let [uuid (:block/uuid (comments-area-entity comments-area))]
(editor-handler/expand-block! uuid)))
(defn- selected-block-entities

View File

@@ -1911,7 +1911,8 @@
(state/clear-editor-action!)
;; Open "Search page or New page" auto-complete
(and (= last-input-char commands/hashtag)
(and (not (:comment-editor? config))
(= last-input-char commands/hashtag)
;; Only trigger at beginning of a line, before whitespace or after a reference
(or (re-find #"(?m)^#" (str (.-value input)))
(start-of-new-word? input pos)
@@ -3024,13 +3025,14 @@
:else
(str "Key" (string/upper-case c)))
false]
[key-code
[key-code
(gobj/get e "key")
(if (mobile-util/native-android?)
(gobj/get e "key")
(if (mobile-util/native-android?)
(gobj/get e "key")
(gobj/getValueByKeys e "event_" "code"))
(gobj/getValueByKeys e "event_" "code"))
;; #3440
(util/goog-event-is-composing? e true)])]
(util/goog-event-is-composing? e true)])
comment-editor? (:comment-editor? (last (state/get-editor-args)))]
(cond
(= value "``````") ; turn this block into a code block
(do
@@ -3039,7 +3041,7 @@
:type :code
:update-current-block? true}]))
(= value ">") ; 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 "")

View File

@@ -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)))]

View File

@@ -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}"

View File

@@ -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}"

View File

@@ -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)))))))

View File

@@ -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))))))

View File

@@ -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"))))))

View File

@@ -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 [])

View File

@@ -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")))