🚧 More work on collaborative edition (in real time).

This commit is contained in:
Andrey Antukh 2019-12-19 13:13:08 +01:00
parent 34291fc4b4
commit 758c70f7c3
8 changed files with 282 additions and 110 deletions

View file

@ -30,7 +30,9 @@
[uxbox.util.spec :as us]
[uxbox.util.transit :as t]
[uxbox.util.time :as dt]
[uxbox.util.uuid :as uuid]))
[uxbox.util.uuid :as uuid]
[vendor.randomcolor]))
;; TODO: temporal workaround
(def clear-ruler nil)
@ -113,6 +115,128 @@
(defn interrupt? [e] (= e :interrupt))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Websockets Events
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Initialize WebSocket
(declare fetch-users)
(declare handle-who)
(declare handle-pointer-update)
(declare handle-page-snapshot)
(declare shapes-changes-commited)
(s/def ::type keyword?)
(s/def ::message
(s/keys :req-un [::type]))
(defn initialize-ws
[file-id]
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(let [uri (str "ws://localhost:6060/sub/" file-id)]
(assoc-in state [:ws file-id] (ws/open uri))))
ptk/WatchEvent
(watch [_ state stream]
(let [wsession (get-in state [:ws file-id])]
(->> (rx/merge
(rx/of (fetch-users file-id))
(->> (ws/-stream wsession)
(rx/filter #(= :message (:type %)))
(rx/map (comp t/decode :payload))
(rx/filter #(s/valid? ::message %))
(rx/map (fn [{:keys [type] :as msg}]
(case type
:who (handle-who msg)
:pointer-update (handle-pointer-update msg)
:page-snapshot (handle-page-snapshot msg)
::unknown)))))
(rx/take-until
(rx/filter #(= ::finalize %) stream)))))))
;; --- Finalize Websocket
(defn finalize-ws
[file-id]
(ptk/reify ::finalize
ptk/WatchEvent
(watch [_ state stream]
(ws/-close (get-in state [:ws file-id]))
(rx/of ::finalize))))
;; --- Fetch Workspace Users
(declare users-fetched)
(defn fetch-users
[file-id]
(ptk/reify ::fetch-users
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :project-file-users {:file-id file-id})
(rx/map users-fetched)))))
(defn users-fetched
[users]
(ptk/reify ::users-fetched
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state user]
(update-in state [:workspace-users :by-id (:id user)] merge user))
state
users))))
;; --- Handle: Who
;; TODO: assign color
(defn- assign-user-color
[state user-id]
(let [user (get-in state [:workspace-users :by-id user-id])
color (js/randomcolor)
user (if (string? (:color user))
user
(assoc user :color color))]
(prn "assign-user-color" user-id)
(assoc-in state [:workspace-users :by-id user-id] user)))
(defn handle-who
[{:keys [users] :as msg}]
(s/assert set? users)
(ptk/reify ::handle-who
ptk/UpdateEvent
(update [_ state]
(prn "handle-who" users)
(as-> state $$
(assoc-in $$ [:workspace-users :active] users)
(reduce assign-user-color $$ users)))))
(defn handle-pointer-update
[{:keys [user-id page-id x y] :as msg}]
(ptk/reify ::handle-pointer-update
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-users :pointer user-id]
{:page-id page-id
:user-id user-id
:x x
:y y}))))
(defn handle-page-snapshot
[{:keys [user-id page-id version operations] :as msg}]
(ptk/reify ::handle-page-snapshot
ptk/WatchEvent
(watch [_ state stream]
(let [local (:workspace-local state)]
(when (= (:page-id local) page-id)
(prn "handle-page-snapshot" msg)
(rx/of (shapes-changes-commited msg)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; General workspace events
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -132,7 +256,6 @@
:tooltip nil})
(declare initialized)
;; (declare watch-events)
(defn initialize
"Initialize the workspace state."
@ -147,9 +270,6 @@
:page-id page-id)]
(-> state
(assoc :workspace-layout default-layout)
;; (update :workspace-layout
;; (fn [data]
;; (if (nil? data) default-layout data)))
(assoc :workspace-local local))))
ptk/WatchEvent
@ -172,16 +292,18 @@
(rx/mapcat #(rx/of (initialized file-id page-id)
#_(initialize-alignment page-id))))
;; When workspace is initialized, run the event watchers.
(->> (rx/filter (ptk/type? ::initialized) stream)
(rx/take 1)
(rx/ignore))))
(->> stream
(rx/filter uxbox.main.ui.workspace.streams/pointer-event?)
(rx/sample 150)
(rx/tap (fn [{:keys [pt] :as event}]
(let [msg {:type :pointer-update
:page-id page-id
:x (:x pt)
:y (:y pt)}]
(ws/-send (get-in state [:ws file-id]) (t/encode msg)))))
(rx/ignore)
(rx/take-until (rx/filter #(= ::stop-watcher %) stream)))))))
ptk/EffectEvent
(effect [_ state stream]
;; Optimistic prefetch of projects if them are not already fetched
#_(when-not (seq (:projects state))
(st/emit! (dp/fetch-projects))))))
(defn- initialized
[file-id page-id]
@ -212,7 +334,6 @@
(disj flags flag)
(conj flags flag)))))))
;; --- Workspace Flags
(defn activate-flag
@ -235,7 +356,6 @@
(update [_ state]
(update-in state [:workspace-local :flags] disj flag))))
(defn toggle-flag
[flag]
(s/assert keyword? flag)
@ -851,8 +971,6 @@
(rx/of (commit-shapes-changes changes)
#(dissoc state ::tmp-changes)))))))
(declare shapes-changes-commited)
(defn commit-shapes-changes
[operations]
(s/assert ::cp/operations operations)
@ -871,21 +989,23 @@
:version (:version page)
:operations operations}]
(->> (rp/mutation :update-project-page params)
(rx/tap #(prn "KAKAKAKA" %))
(rx/map shapes-changes-commited))))))
(s/def ::shapes-changes-commited
(s/keys :req-un [::id ::version ::cp/operations]))
(s/keys :req-un [::page-id ::version ::cp/operations]))
(defn shapes-changes-commited
[{:keys [id version operations] :as params}]
[{:keys [page-id version operations] :as params}]
(prn "shapes-changes-commited" params)
(s/assert ::shapes-changes-commited params)
(ptk/reify ::shapes-changes-commited
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-page :version] version)
(assoc-in [:pages id :version] version)
(update-in [:pages-data id] cp/process-ops operations)
(assoc-in [:pages page-id :version] version)
(update-in [:pages-data page-id] cp/process-ops operations)
(update :workspace-data cp/process-ops operations)))))
;; --- Start shape "edition mode"

View file

@ -24,6 +24,8 @@
(declare fetch-users)
(declare handle-who)
(declare handle-pointer-update)
(declare handle-page-snapshot)
(s/def ::type keyword?)
(s/def ::message
@ -35,29 +37,37 @@
ptk/UpdateEvent
(update [_ state]
(let [uri (str "ws://localhost:6060/sub/" file-id)]
(assoc-in state [::ws file-id] (ws/open uri))))
(assoc-in state [:ws file-id] (ws/open uri))))
ptk/WatchEvent
(watch [_ state stream]
(rx/merge
(rx/of (fetch-users file-id))
(->> (ws/-stream (get-in state [::ws file-id]))
(rx/filter #(= :message (:type %)))
(rx/map (comp t/decode :payload))
(rx/filter #(s/valid? ::message %))
(rx/map (fn [{:keys [type] :as msg}]
(case type
:who (handle-who msg)
::unknown))))))))
(let [wsession (get-in state [:ws file-id])]
(->> (rx/merge
(rx/of (fetch-users file-id))
(->> (ws/-stream wsession)
(rx/filter #(= :message (:type %)))
(rx/map (comp t/decode :payload))
(rx/filter #(s/valid? ::message %))
(rx/map (fn [{:keys [type] :as msg}]
(case type
:who (handle-who msg)
:pointer-update (handle-pointer-update msg)
:page-snapshot (handle-page-snapshot msg)
::unknown)))))
(rx/take-until
(rx/filter #(= ::finalize %) stream)))))))
;; --- Finalize Websocket
(defn finalize
[file-id]
(ptk/reify ::finalize
ptk/EffectEvent
(effect [_ state stream]
(ws/-close (get-in state [::ws file-id])))))
ptk/WatchEvent
(watch [_ state stream]
(ws/-close (get-in state [:ws file-id]))
(rx/of ::finalize))))
;; --- Fetch Workspace Users
@ -93,3 +103,25 @@
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-users :active] users))))
(defn handle-pointer-update
[{:keys [user-id page-id x y] :as msg}]
(ptk/reify ::handle-pointer-update
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-users :pointer user-id]
{:page-id page-id
:user-id user-id
:x x
:y y}))))
(defn handle-page-snapshot
[{:keys [user-id page-id version operations :as msg]}]
(ptk/reify ::handle-page-snapshot
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-page :version] version)
(assoc-in [:pages page-id :version] version)
(update-in [:pages-data page-id] cp/process-ops operations)
(update :workspace-data cp/process-ops operations)))))

View file

@ -96,6 +96,12 @@
(mf/defc workspace
[{:keys [file-id page-id] :as props}]
(mf/use-effect
{:deps #js [(str file-id)]
:fn (fn []
(st/emit! (dw/initialize-ws file-id))
#(st/emit! (dw/finalize-ws file-id)))})
(mf/use-effect
{:deps #js [(str file-id)
(str page-id)]
@ -104,12 +110,6 @@
(st/emit! (dw/initialize file-id page-id))
#(rx/cancel! sub)))})
(mf/use-effect
{:deps #js [(str file-id)]
:fn (fn []
(st/emit! (dws/initialize file-id))
#(st/emit! (dws/finalize file-id)))})
(let [layout (mf/deref refs/workspace-layout)
file (mf/deref refs/workspace-file)
page (mf/deref refs/workspace-page)

View file

@ -41,24 +41,25 @@
;; --- Header Users
(mf/defc user-item
(mf/defc user-widget
[{:keys [user self?] :as props}]
[:li.tooltip.tooltip-bottom
{:alt (:fullname user)
:on-click (when self?
#(st/emit! (rt/navigate :settings/profile)))}
[:img {:src "/images/avatar.jpg"}]])
[:img {:style {:border-color (:color user)}
:src "/images/avatar.jpg"}]])
(mf/defc users-list
(mf/defc active-users
[props]
(let [profile (mf/deref refs/profile)
users (mf/deref refs/workspace-users)]
[:ul.user-multi
[:& user-item {:user profile :self? true}]
[:& user-widget {:user profile :self? true}]
(for [id (->> (:active users)
(remove #(= % (:id profile))))]
[:& user-item {:user (get-in users [:by-id id])
:key id}])]))
[:& user-widget {:user (get-in users [:by-id id])
:key id}])]))
;; --- Header Component
@ -80,7 +81,7 @@
:on-click #(st/emit! (dw/toggle-layout-flag :sitemap))}
[:span (:project-name file) " / " (:name file)]]
[:& users-list]
[:& active-users]
[:div.workspace-options
[:ul.options-btn

View file

@ -143,7 +143,7 @@
;; --- Viewport
(declare remote-user-cursor)
(declare remote-user-cursors)
(mf/defc canvas-and-shapes
{:wrap [mf/wrap-memo]}
@ -295,42 +295,51 @@
;; -- METER CURSOR MULTIUSUARIO
;;[:& remote-user-cursor]
[:& remote-user-cursors {:page page}]
[:& selrect {:data (:selrect local)}]]])))
(mf/defc remote-user-cursor
[props]
[:g.multiuser-cursor #_{:transform "translate(100, 100) scale(2)"}
[:svg {:x "100"
:y "100"
:style {:fill "#000"}
:width "106.824"
:height "20.176"
:viewBox "0 0 28.264 5.338"}
[:path {:d "M5.292 4.027L1.524.26l-.05-.01L0 0l.258 1.524 3.769 3.768zm-.45 0l-.313.314L1.139.95l.314-.314zm-.5.5l-.315.316-3.39-3.39.315-.315 3.39 3.39zM1.192.526l-.668.667L.431.646.64.43l.552.094z"
:font-family "sans-serif"}]
[:g {:transform "translate(0 -291.708)"}
[:rect {:width "21.415"
:height "5.292"
:x "6.849"
:y "291.755"
:fill-opacity ".893"
:paint-order "stroke fill markers"
:rx ".794"
:ry ".794"}]
[:text {:x "9.811"
:y "295.216"
:fill "#fff"
:stroke-width ".265"
:font-family "Open Sans"
:font-size"2.91"
:font-weight "400"
:letter-spacing"0"
:line-height "1.25"
:word-spacing "0"
;; :style="line-height:1
}
"User 2"]]]])
[{:keys [pointer user] :as props}]
[:g.multiuser-cursor {:key (:user-id pointer)
:transform (str "translate(" (:x pointer) "," (:y pointer) ") scale(4)")}
[:path {:fill (:color user)
:d "M5.292 4.027L1.524.26l-.05-.01L0 0l.258 1.524 3.769 3.768zm-.45 0l-.313.314L1.139.95l.314-.314zm-.5.5l-.315.316-3.39-3.39.315-.315 3.39 3.39zM1.192.526l-.668.667L.431.646.64.43l.552.094z"
:font-family "sans-serif"}]
[:g {:transform "translate(0 -291.708)"}
[:rect {:width "21.415"
:height "5.292"
:x "6.849"
:y "291.755"
:fill (:color user)
:fill-opacity ".893"
:paint-order "stroke fill markers"
:rx ".794"
:ry ".794"}]
[:text {:x "9.811"
:y "295.216"
:fill "#fff"
:stroke-width ".265"
:font-family "Open Sans"
:font-size"2.91"
:font-weight "400"
:letter-spacing"0"
:style {:line-height "1.25"}
:word-spacing "0"
;; :style="line-height:1
}
(:fullname user)]]])
(mf/defc remote-user-cursors
[{:keys [page] :as props}]
(let [users (mf/deref refs/workspace-users)
pointers (->> (vals (:pointer users))
(remove #(not= (:id page) (:page-id %)))
(filter #((:active users) (:user-id %))))]
(for [pointer pointers]
(let [user (get-in users [:by-id (:user-id pointer)])]
[:& remote-user-cursor {:pointer pointer
:user user
:key (:user-id pointer)}]))))

View file

@ -48,4 +48,3 @@
(ev/unlistenByKey lk1)
(ev/unlistenByKey lk2)
(ev/unlistenByKey lk3)))))