enhance(ux): editing user avatar presence

This commit is contained in:
Tienson Qin
2026-01-21 19:29:34 +08:00
parent 8c74eb2736
commit 36c5afeece
13 changed files with 133 additions and 3 deletions

View File

@@ -13,6 +13,10 @@
[:map
[:type [:= "hello"]]
[:client :string]]]
["presence"
[:map
[:type [:= "presence"]]
[:editing-block-uuid {:optional true} [:maybe :string]]]]
["pull"
[:map
[:type [:= "pull"]]
@@ -41,7 +45,8 @@
[:user-id :string]
[:email {:optional true} [:maybe :string]]
[:username {:optional true} [:maybe :string]]
[:name {:optional true} [:maybe :string]]])
[:name {:optional true} [:maybe :string]]
[:editing-block-uuid {:optional true} [:maybe :string]]])
(def online-users-schema
[:map

View File

@@ -162,6 +162,20 @@
[^js self ^js ws user]
(swap! (presence* self) assoc ws user))
(defn- update-presence!
[^js self ^js ws {:keys [editing-block-uuid] :as updates}]
(swap! (presence* self)
(fn [presence]
(if-let [user (get presence ws)]
(let [user' (if (contains? updates :editing-block-uuid)
(if (and (string? editing-block-uuid)
(not (string/blank? editing-block-uuid)))
(assoc user :editing-block-uuid editing-block-uuid)
(dissoc user :editing-block-uuid))
user)]
(assoc presence ws user'))
presence))))
(defn- remove-presence!
[^js self ^js ws]
(swap! (presence* self) dissoc ws))
@@ -420,6 +434,11 @@
"ping"
(send! ws {:type "pong"})
"presence"
(let [editing-block-uuid (:editing-block-uuid message)]
(update-presence! self ws {:editing-block-uuid editing-block-uuid})
(broadcast-online-users! self))
"pull"
(let [raw-since (:since message)
since (if (some? raw-since) (parse-int raw-since) 0)]

View File

@@ -9,6 +9,8 @@
## Client -> Server
- `{"type":"hello","client":"<repo-id>"}`
- Initial handshake from client.
- `{"type":"presence","editing-block-uuid":"<uuid|null>"}`
- Update current editing block for presence (omit or null to clear).
- `{"type":"pull","since":<t>}`
- Request txs after `since` (defaults to 0).
- `{"type":"tx/batch","t-before":<t>,"txs":["<tx-transit>", ...]}`
@@ -21,6 +23,7 @@
- Server hello with current t.
- `{"type":"online-users","online-users":[{"user-id":"...","email":"...","username":"...","name":"..."}...]}`
- Presence update with currently online users (fields may be omitted).
- Optional `editing-block-uuid` indicates the block the user is editing.
- `{"type":"pull/ok","t":<t>,"txs":[{"t":<t>,"tx":"<tx-transit>"}...]}`
- Pull response with txs.
- `{"type":"tx/batch/ok","t":<t>}`

View File

@@ -51,6 +51,7 @@
[frontend.handler.route :as route-handler]
[frontend.handler.search :as search-handler]
[frontend.handler.ui :as ui-handler]
[frontend.handler.user :as user-handler]
[frontend.mixins :as mixins]
[frontend.mobile.haptics :as haptics]
[frontend.mobile.intent :as mobile-intent]
@@ -81,6 +82,7 @@
[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]
[promesa.core :as p]
[rum.core :as rum]))
@@ -1737,12 +1739,48 @@
[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- editing-user-for-block
[block-uuid online-users current-user-uuid]
(when (and block-uuid (seq online-users))
(some (fn [{:user/keys [editing-block-uuid uuid] :as user}]
(when (and (string? editing-block-uuid)
(= editing-block-uuid (str block-uuid))
(not= uuid current-user-uuid))
user))
online-users)))
(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
[:span.block-editing-avatar-wrap
(shui/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))])))
(rum/defcs ^:large-vars/cleanup-todo block-control < rum/reactive
(rum/local false ::dragging?)
[state config block {:keys [uuid block-id collapsed? *control-show? edit? selected? top? bottom?]}]
(let [*bullet-dragging? (::dragging? state)
doc-mode? (state/sub :document/mode?)
control-show? (util/react *control-show?)
rtc-state (state/sub :rtc/state)
online-users (:online-users rtc-state)
current-user-uuid (user-handler/user-uuid)
editing-user (editing-user-for-block uuid online-users current-user-uuid)
ref? (:ref? config)
empty-content? (block-content-empty? block)
fold-button-right? (state/enable-fold-button-right?)
@@ -1767,6 +1805,8 @@
:is-with-icon with-icon?
:bullet-closed collapsed?
:bullet-hidden (:hide-bullet? config)}])}
(when (and (not page-title?) editing-user)
(editing-user-avatar editing-user))
(when (and (or (not fold-button-right?) collapsable? collapsed?)
(not (:table? config)))
[:a.block-control

View File

@@ -195,6 +195,7 @@
.block-control-wrap, .ls-page-title .property-value .block-control-wrap {
@apply h-[24px];
@apply relative;
&.is-order-list {
@apply mr-0 pr-0;
@@ -228,6 +229,12 @@
}
}
.block-editing-avatar-wrap {
@apply absolute top-1/2 -translate-y-1/2 pointer-events-none;
left: 2px;
z-index: 2;
}
.ls-page-title .block-control-wrap {
height: initial;
}

View File

@@ -102,6 +102,10 @@
(log/info :db-sync/stop true)
(state/<invoke-db-worker :thread-api/db-sync-stop))
(defn <rtc-update-presence!
[editing-block-uuid]
(state/<invoke-db-worker :thread-api/db-sync-update-presence editing-block-uuid))
(defn <rtc-get-users-info
[]
(when-let [graph-uuid (ldb/get-graph-rtc-uuid (db/get-db))]

View File

@@ -60,6 +60,10 @@
[]
(state/<invoke-db-worker :thread-api/rtc-stop))
(defn <rtc-update-presence!
[_editing-block-uuid]
(p/resolved nil))
(defn <rtc-branch-graph!
[repo]
(p/let [_ (js/Promise. user-handler/task--ensure-id&access-token)

View File

@@ -31,6 +31,12 @@
(db-sync-handler/<rtc-stop!)
(rtc-handler/<rtc-stop!)))
(defn <rtc-update-presence!
[editing-block-uuid]
(if (db-sync-enabled?)
(db-sync-handler/<rtc-update-presence! editing-block-uuid)
(rtc-handler/<rtc-update-presence! editing-block-uuid)))
(defn <rtc-branch-graph! [repo]
(rtc-handler/<rtc-branch-graph! repo))

View File

@@ -322,6 +322,9 @@
(defmethod handle :rtc/sync-state [[_ state]]
(state/update-state! :rtc/state (fn [old] (merge old state))))
(defmethod handle :rtc/presence-update [[_ {:keys [editing-block-uuid]}]]
(rtc-handler/<rtc-update-presence! editing-block-uuid))
(defmethod handle :rtc/log [[_ data]]
(state/set-state! :rtc/log data))

View File

@@ -1250,6 +1250,8 @@ Similar to re-frame subscriptions"
(when clear-editing-block?
(set-state! :editor/editing? nil)
(set-state! :editor/block nil))
(when clear-editing-block?
(pub-event! [:rtc/presence-update {:editing-block-uuid nil}]))
(set-state! :editor/start-pos nil)
(clear-editor-last-pos!)
(clear-cursor-range!)
@@ -1806,6 +1808,8 @@ Similar to re-frame subscriptions"
(set-state! :editor/last-key-code nil)
(set-state! :editor/set-timestamp-block nil)
(set-state! :editor/cursor-range cursor-range)
(when-let [block-uuid (:block/uuid block)]
(pub-event! [:rtc/presence-update {:editing-block-uuid (str block-uuid)}]))
(when (= :code (:logseq.property.node/display-type (d/entity db (:db/id block))))
(pub-event! [:editor/focus-code-editor block block-element]))
(when-let [input (gdom/getElement edit-input-id)]

View File

@@ -51,12 +51,15 @@
(defn- normalize-online-users
[users]
(->> users
(keep (fn [{:keys [user-id email username name]}]
(keep (fn [{:keys [user-id email username name editing-block-uuid]}]
(when (string? user-id)
(let [display-name (or username name user-id)]
(cond-> {:user/uuid user-id
:user/name display-name}
(string? email) (assoc :user/email email))))))
(string? email) (assoc :user/email email)
(and (string? editing-block-uuid)
(not (string/blank? editing-block-uuid)))
(assoc :user/editing-block-uuid editing-block-uuid))))))
(vec)))
(defn- broadcast-rtc-state!
@@ -245,6 +248,13 @@
(.send ws (js/JSON.stringify (clj->js coerced)))
(log/error :db-sync/ws-request-invalid {:message message}))))
(defn update-presence!
[editing-block-uuid]
(when-let [client @worker-state/*db-sync-client]
(when-let [ws (:ws client)]
(send! ws {:type "presence"
:editing-block-uuid editing-block-uuid}))))
(defn- remove-ignored-attrs
[tx-data]
(remove (fn [d] (contains? #{:logseq.property.embedding/hnsw-label-updated-at

View File

@@ -425,6 +425,10 @@
[]
(db-sync/stop!))
(def-thread-api :thread-api/db-sync-update-presence
[editing-block-uuid]
(db-sync/update-presence! editing-block-uuid))
(def-thread-api :thread-api/db-sync-upload-graph
[repo]
(db-sync/upload-graph! repo))

View File

@@ -229,3 +229,24 @@
nil
[[:db/add (:db/id child1) :block/title "same"]])
(is (= 1 (count (#'db-sync/pending-txs test-repo))))))))))
(deftest normalize-online-users-include-editing-block-test
(testing "online user normalization preserves editing block info"
(let [result (#'db-sync/normalize-online-users
[{:user-id "user-1"
:name "Jane"
:editing-block-uuid "block-1"}])]
(is (= [{:user/uuid "user-1"
:user/name "Jane"
:user/editing-block-uuid "block-1"}]
result)))))
(deftest normalize-online-users-omit-empty-editing-block-test
(testing "online user normalization drops empty editing block info"
(let [result (#'db-sync/normalize-online-users
[{:user-id "user-1"
:name "Jane"
:editing-block-uuid nil}])]
(is (= [{:user/uuid "user-1"
:user/name "Jane"}]
result)))))