♻️ Refactor presence and realtime cursors handling.

This commit is contained in:
Andrey Antukh 2020-04-27 08:54:43 +02:00 committed by Alonso Torres
parent 1c3664921d
commit 285735e35f
15 changed files with 519 additions and 318 deletions

View file

@ -40,6 +40,7 @@
[uxbox.util.time :as dt]
[uxbox.util.transit :as t]
[uxbox.util.webapi :as wapi]
[uxbox.util.avatars :as avatars]
[uxbox.main.data.workspace.common :refer [IBatchedChange IUpdateGroup] :as common]
[uxbox.main.data.workspace.transforms :as transforms]))
@ -63,7 +64,7 @@
;; --- Declarations
(declare fetch-project)
(declare handle-who)
(declare handle-presence)
(declare handle-pointer-update)
(declare handle-pointer-send)
(declare handle-page-change)
@ -122,11 +123,19 @@
(rx/map (constantly ::index-initialized)))))]
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(assoc state :workspace-presence {}))
ptk/WatchEvent
(watch [_ state stream]
(rx/merge
(rx/of (fetch-bundle project-id file-id)
(initialize-ws file-id))
(rx/of (fetch-bundle project-id file-id))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
(rx/mapcat (fn [_] (rx/of (initialize-ws file-id))))
(rx/first))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
@ -198,7 +207,7 @@
[ids]
(ptk/reify ::adjust-group-shapes
IBatchedChange
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
@ -249,7 +258,8 @@
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(let [url (ws/url (str "/sub/" file-id))]
(let [sid (:session-id state)
url (ws/url (str "/notifications/" file-id "/" sid))]
(assoc-in state [:ws file-id] (ws/open url))))
ptk/WatchEvent
@ -263,7 +273,7 @@
(rx/filter #(s/valid? ::message %))
(rx/map (fn [{:keys [type] :as msg}]
(case type
:who (handle-who msg)
:presence (handle-presence msg)
:pointer-update (handle-pointer-update msg)
:page-change (handle-page-change msg)
::unknown))))
@ -287,37 +297,37 @@
;; --- Handle: Who
;; TODO: assign color
(defn- assign-user-color
[state user-id]
(let [user (get-in state [:workspace-users :by-id user-id])
color "#000000" #_(js/randomcolor)
user (if (string? (:color user))
user
(assoc user :color color))]
(assoc-in state [:workspace-users :by-id user-id] user)))
(defn handle-who
[{:keys [users] :as msg}]
(us/verify set? users)
(ptk/reify ::handle-who
(defn handle-presence
[{:keys [sessions] :as msg}]
(ptk/reify ::handle-presence
ptk/UpdateEvent
(update [_ state]
(as-> state $$
(assoc-in $$ [:workspace-users :active] users)
(reduce assign-user-color $$ users)))))
(let [users (:workspace-users state)]
(update state :workspace-presence
(fn [prev-sessions]
(reduce (fn [acc [sid pid]]
(if-let [prev (get prev-sessions sid)]
(assoc acc sid prev)
(let [profile (get users pid)
session {:id sid
:fullname (:fullname profile)
:photo-uri (:photo-uri profile)}]
(assoc acc sid (avatars/assign session)))))
{}
sessions)))))))
(defn handle-pointer-update
[{:keys [user-id page-id x y] :as msg}]
[{:keys [page-id profile-id session-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}))))
(let [profile (get-in state [:workspace-users profile-id])]
(update-in state [:workspace-presence session-id]
(fn [session]
(assoc session
:point (gpt/point x y)
:updated-at (dt/now)
:page-id page-id)))))))
(defn handle-pointer-send
[file-id point]
@ -325,6 +335,7 @@
ptk/EffectEvent
(effect [_ state stream]
(let [ws (get-in state [:ws file-id])
sid (:session-id state)
pid (get-in state [:workspace-page :id])
msg {:type :pointer-update
:page-id pid
@ -341,8 +352,6 @@
(when (= page-id page-id')
(rx/of (shapes-changes-commited msg)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Persistence
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -443,22 +452,24 @@
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ state stream]
(->> (rx/zip (rp/query :file-with-users {:id file-id})
(->> (rx/zip (rp/query :file {:id file-id})
(rp/query :file-users {:id file-id})
(rp/query :project-by-id {:project-id project-id})
(rp/query :pages {:file-id file-id}))
(rx/first)
(rx/map (fn [[file project pages]]
(bundle-fetched file project pages)))
(rx/map (fn [[file users project pages]]
(bundle-fetched file users project pages)))
(rx/catch (fn [{:keys [type] :as error}]
(when (= :not-found type)
(rx/of (rt/nav :not-found)))))))))
(defn- bundle-fetched
[file project pages]
[file users project pages]
(ptk/reify ::bundle-fetched
IDeref
(-deref [_]
{:file file
:users users
:project project
:pages pages})
@ -468,6 +479,7 @@
(as-> state $$
(assoc $$
:workspace-file file
:workspace-users (d/index-by :id users)
:workspace-pages {}
:workspace-project project)
(reduce assoc-page $$ pages))))))

View file

@ -49,6 +49,9 @@
(def workspace-users
(l/derived :workspace-users st/state))
(def workspace-presence
(l/derived :workspace-presence st/state))
(def workspace-data
(-> #(let [page-id (get-in % [:workspace-page :id])]
(get-in % [:workspace-data page-id]))

View file

@ -20,6 +20,7 @@
[uxbox.main.ui.modal :as modal]
[uxbox.main.ui.workspace.images :refer [import-image-modal]]
[uxbox.main.ui.components.dropdown :refer [dropdown]]
[uxbox.main.ui.workspace.presence :as presence]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.data :refer [classnames]]
[uxbox.util.math :as mth]
@ -60,34 +61,8 @@
[:li {:on-click on-zoom-to-200}
"Zoom to 200%" [:span "Shift + 2"]]]]]))
;; --- Header Users
(mf/defc user-widget
[{:keys [user self?] :as props}]
(let [photo (or (:photo-uri user)
(if self?
"/images/avatar.jpg"
"/images/avatar-red.jpg"))]
[:li.tooltip.tooltip-bottom
{:alt (:fullname user)
:on-click (when self?
#(st/emit! (rt/navigate :settings/profile)))}
[:img {:style {:border-color (:color user)}
:src photo}]]))
(mf/defc active-users
[props]
(let [profile (mf/deref refs/profile)
users (mf/deref refs/workspace-users)]
[:ul.active-users
[:& user-widget {:user profile :self? true}]
(for [id (->> (:active users)
(remove #(= % (:id profile))))]
[:& user-widget {:user (get-in users [:by-id id])
:key id}])]))
(mf/defc menu
[{:keys [layout project file] :as props}]
(let [show-menu? (mf/use-state false)
@ -161,7 +136,7 @@
:file file}]
[:div.users-section
[:& active-users]]
[:& presence/active-sessions]]
[:div.options-section
[:& zoom-widget

View file

@ -0,0 +1,81 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.workspace.presence
(:require
[rumext.alpha :as mf]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.util.router :as rt]))
(def ^:const pointer-icon-path
(str "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"))
(mf/defc session-cursor
[{:keys [session] :as props}]
(let [point (:point session)
color (:color session "#000000")
transform (str "translate(" (:x point) "," (:y point) ") scale(4)")]
[:g.multiuser-cursor {:transform transform}
[:path {:fill color
:d pointer-icon-path
: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
: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"}
(:fullname session)]]]))
(mf/defc active-cursors
{::mf/wrap [mf/memo]}
[{:keys [page] :as props}]
(let [sessions (mf/deref refs/workspace-presence)
sessions (->> (vals sessions)
(filter #(= (:id page) (:page-id %))))]
(for [session sessions]
[:& session-cursor {:session session :key (:id session)}])))
(mf/defc session-widget
[{:keys [session self?] :as props}]
(let [photo (:photo-uri session "/images/avatar.jpg")]
[:li.tooltip.tooltip-bottom
{:alt (:fullname session)
:on-click (when self?
#(st/emit! (rt/navigate :settings/profile)))}
[:img {:style {:border-color (:color session)}
:src photo}]]))
(mf/defc active-sessions
{::mf/wrap [mf/memo]}
[]
(let [profile (mf/deref refs/profile)
sessions (mf/deref refs/workspace-presence)]
[:ul.active-users
(for [session (vals sessions)]
[:& session-widget {:session session :key (:id session)}])]))

View file

@ -2,8 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2019 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.workspace.viewport
(:require
@ -26,6 +28,7 @@
[uxbox.main.ui.workspace.grid :refer [grid]]
[uxbox.main.ui.workspace.ruler :refer [ruler]]
[uxbox.main.ui.workspace.selection :refer [selection-handlers]]
[uxbox.main.ui.workspace.presence :as presence]
[uxbox.util.dom :as dom]
[uxbox.util.geom.point :as gpt]
[uxbox.util.perf :as perf]
@ -315,50 +318,5 @@
(when (contains? flags :ruler)
[:& ruler {:zoom zoom :ruler (:ruler local)}])
[:& remote-user-cursors {:page page}]
[:& presence/active-cursors {:page page}]
[:& selection-rect {:data (:selrect local)}]]]))
(mf/defc remote-user-cursor
[{: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

@ -0,0 +1,41 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.util.avatars
(:require
[cuerdas.core :as str]
[uxbox.util.object :as obj]
["randomcolor" :as rdcolor]))
(defn- impl-generate-image
[{:keys [name color size]
:or {color "#303236" size 128}}]
(let [parts (str/words (str/upper name))
letters (if (= 1 (count parts))
(ffirst parts)
(str (ffirst parts) (first (second parts))))
canvas (.createElement js/document "canvas")
context (.getContext canvas "2d")]
(set! (.-width canvas) size)
(set! (.-height canvas) size)
(set! (.-fillStyle context) "#303236")
(.fillRect context 0 0 size size)
(set! (.-font context) (str (/ size 2) "px Arial"))
(set! (.-textAlign context) "center")
(set! (.-fillStyle context) "#FFFFFF")
(.fillText context letters (/ size 2) (/ size 1.5))
(.toDataURL canvas)))
(defn assign
[{:keys [id photo-uri fullname color] :as profile}]
(cond-> profile
(not photo-uri) (assoc :photo-uri (impl-generate-image {:name fullname}))
(not color) (assoc :color (rdcolor))))