diff --git a/backend/src/app/notifications.clj b/backend/src/app/notifications.clj index 94667c698..cd145e955 100644 --- a/backend/src/app/notifications.clj +++ b/backend/src/app/notifications.clj @@ -194,6 +194,7 @@ ;; --- CONNECTION INIT +(declare send-presence) (declare handle-message) (declare start-loop!) @@ -211,7 +212,7 @@ (.disconnect session))))))) (defn- start-loop! - [{:keys [rcv-ch out-ch sub-ch session-id] :as cfg}] + [{:keys [rcv-ch out-ch sub-ch session-id profile-id] :as cfg}] (aa/go-try (loop [] (let [timeout (a/timeout 30000) @@ -224,11 +225,17 @@ (aa/! out-ch val)) (recur)) @@ -243,51 +250,14 @@ :else nil))))) -;; --- PRESENCE HANDLING API - -(def ^:private - sql:retrieve-presence - "select * from presence - where file_id=? - and (clock_timestamp() - updated_at) < '5 min'::interval") - -(def ^:private - sql:update-presence - "insert into presence (file_id, session_id, profile_id, updated_at) - values (?, ?, ?, clock_timestamp()) - on conflict (file_id, session_id, profile_id) - do update set updated_at=clock_timestamp()") - -(defn- retrieve-presence - [{:keys [pool file-id] :as cfg}] - (let [rows (db/exec! pool [sql:retrieve-presence file-id])] - (mapv (juxt :session-id :profile-id) rows))) - -(defn- retrieve-presence* - [{:keys [executor] :as cfg}] - (aa/with-thread executor - (retrieve-presence cfg))) - -(defn- update-presence - [{:keys [pool file-id session-id profile-id] :as cfg}] - (let [sql [sql:update-presence file-id session-id profile-id]] - (db/exec-one! pool sql))) - -(defn- update-presence* - [{:keys [executor] :as cfg}] - (aa/with-thread executor - (update-presence cfg))) - -(defn- delete-presence - [{:keys [pool file-id session-id profile-id] :as cfg}] - (db/delete! pool :presence {:file-id file-id - :profile-id profile-id - :session-id session-id})) - -(defn- delete-presence* - [{:keys [executor] :as cfg}] - (aa/with-thread executor - (delete-presence cfg))) +(defn send-presence + ([cfg] (send-presence cfg :presence)) + ([{:keys [msgbus session-id profile-id file-id]} type] + (a/go + (a/ state + (assoc :profile-id id) (assoc :profile data) ;; Safeguard if the profile is loaded after teams (assoc-in [:profile :teams] (get-in state [:profile :teams])))) diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index effd20f97..437b7d67d 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -70,6 +70,10 @@ (rx/filter #(s/valid? ::message %)) (rx/map process-message)) + (rx/of (handle-presence {:type :connect + :session-id (:session-id state) + :profile-id (:profile-id state)})) + ;; Send back to backend all pointer messages. (->> stream (rx/filter ms/pointer-event?) @@ -80,9 +84,11 @@ (defn- process-message [{:keys [type] :as msg}] (case type - :presence (handle-presence msg) + :connect (handle-presence msg) + :presence (handle-presence msg) + :disconnect (handle-presence msg) :pointer-update (handle-pointer-update msg) - :file-change (handle-file-change msg) + :file-change (handle-file-change msg) :library-change (handle-library-change msg) ::unknown)) @@ -136,41 +142,41 @@ }) (defn handle-presence - [{:keys [sessions] :as message}] - (letfn [(assign-color [sessions session] - (if (string? (:color session)) - session - (let [used (into #{} - (comp (map second) - (map :color) - (remove nil?)) - sessions) - avail (set/difference presence-palette used) - color (or (first avail) "#000000")] - (assoc session :color color)))) + [{:keys [type session-id profile-id] :as message}] + (letfn [(get-next-color [presence] + (let [xfm (comp (map second) + (map :color) + (remove nil?)) + used (into #{} xfm presence) + avail (set/difference presence-palette used)] + (or (first avail) "#000000"))) - (assign-session [sessions {:keys [id profile]}] - (let [session {:id id - :fullname (:fullname profile) - :updated-at (dt/now) - :photo-uri (cfg/resolve-profile-photo-url profile)} - session (assign-color sessions session)] - (assoc sessions id session))) + (update-color [color presence] + (if (some? color) + color + (get-next-color presence))) - (update-sessions [previous profiles] - (let [previous (select-keys previous (map first sessions)) ; Initial clearing - pending (->> sessions - (filter #(not (contains? previous (first %)))) - (map (fn [[session-id profile-id]] - {:id session-id - :profile (get profiles profile-id)})))] - (reduce assign-session previous pending)))] + (update-sesion [session presence] + (-> session + (assoc :id session-id) + (assoc :profile-id profile-id) + (assoc :updated-at (dt/now)) + (update :color update-color presence))) + + (update-presence [presence] + (-> presence + (update session-id update-sesion presence) + (d/without-nils))) + + ] (ptk/reify ::handle-presence ptk/UpdateEvent (update [_ state] - (let [profiles (:users state)] - (update state :workspace-presence update-sessions profiles)))))) + ;; (let [profiles (:users state)] + (if (= :disconnect type) + (update state :workspace-presence dissoc session-id) + (update state :workspace-presence update-presence)))))) (defn handle-pointer-update [{:keys [page-id profile-id session-id x y] :as msg}] diff --git a/frontend/src/app/main/ui/workspace/presence.cljs b/frontend/src/app/main/ui/workspace/presence.cljs index 6e7f8ded3..722c2def4 100644 --- a/frontend/src/app/main/ui/workspace/presence.cljs +++ b/frontend/src/app/main/ui/workspace/presence.cljs @@ -5,18 +5,19 @@ ;; 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 +;; Copyright (c) UXBOX Labs SL (ns app.main.ui.workspace.presence (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [beicon.core :as rx] + [app.config :as cfg] [app.main.refs :as refs] [app.main.store :as st] + [app.util.router :as rt] [app.util.time :as dt] - [app.util.timers :as tm] - [app.util.router :as rt])) + [app.util.timers :as ts] + [beicon.core :as rx] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (def pointer-icon-path (str "M5.292 4.027L1.524.26l-.05-.01L0 0l.258 1.524 3.769 3.768zm-.45 " @@ -24,7 +25,7 @@ "3.39 3.39zM1.192.526l-.668.667L.431.646.64.43l.552.094z")) (mf/defc session-cursor - [{:keys [session] :as props}] + [{:keys [session profile] :as props}] (let [zoom (mf/deref refs/selected-zoom) point (:point session) color (:color session "#000000") @@ -32,7 +33,7 @@ [:g.multiuser-cursor {:transform transform} [:path {:fill color :d pointer-icon-path - :font-family "sans-serif"}] + }] [:g {:transform "translate(0 -291.708)"} [:rect {:width 25 :height 5 @@ -50,53 +51,58 @@ :overflow "hidden" :fill "#fff" :stroke-width 1 - :font-family "Open Sans" + :font-family "Works Sans" :font-size 3 :font-weight 400 :letter-spacing 0 :style { :line-height 1.25 } :word-spacing 0} - (str (str/slice (:fullname session) 0 14) - (when (> (count (:fullname session)) 14) "..."))]]])) + (str (str/slice (:fullname profile) 0 14) + (when (> (count (:fullname profile)) 14) "..."))]]])) (mf/defc active-cursors {::mf/wrap [mf/memo]} [{:keys [page-id] :as props}] (let [counter (mf/use-state 0) + users (mf/deref refs/users) sessions (mf/deref refs/workspace-presence) sessions (->> (vals sessions) (filter #(= page-id (:page-id %))) - (filter #(>= 3000 (- (inst-ms (dt/now)) (inst-ms (:updated-at %))))))] + (filter #(>= 5000 (- (inst-ms (dt/now)) (inst-ms (:updated-at %))))))] (mf/use-effect nil (fn [] - (let [sem (tm/schedule 1000 #(swap! counter inc))] + (let [sem (ts/schedule 1000 #(swap! counter inc))] (fn [] (rx/dispose! sem))))) (for [session sessions] (when (:point session) - [:& session-cursor {:session session :key (:id session)}])))) + [:& session-cursor {:session session + :profile (get users (:profile-id session)) + :key (:id session)}])))) + +;; --- SESSION WIDGET (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}]])) + [{:keys [session self? profile] :as props}] + [:li.tooltip.tooltip-bottom + {:alt (:fullname profile) + :on-click (when self? (st/emitf (rt/navigate :settings/profile)))} + [:img {:style {:border-color (:color session)} + :src (cfg/resolve-profile-photo-url profile)}]]) (mf/defc active-sessions {::mf/wrap [mf/memo]} [] (let [profile (mf/deref refs/profile) - sessions (mf/deref refs/workspace-presence)] + users (mf/deref refs/users) + presence (mf/deref refs/workspace-presence)] [:ul.active-users - (for [session (vals sessions)] + (for [session (vals presence)] [:& session-widget {:session session - :self? (= (:id session) (:id profile)) + :profile (get users (:profile-id session)) + :self? (= (:profile-id session) (:id profile)) :key (:id session)}])]))