mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
enhance(ux): editing user avatar presence
This commit is contained in:
@@ -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
|
||||
|
||||
19
deps/db-sync/src/logseq/db_sync/worker.cljs
vendored
19
deps/db-sync/src/logseq/db_sync/worker.cljs
vendored
@@ -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)]
|
||||
|
||||
@@ -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>}`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
Reference in New Issue
Block a user