diff --git a/backend/src/uxbox/http/ws.clj b/backend/src/uxbox/http/ws.clj index 4e8e4b55b8..734c56e764 100644 --- a/backend/src/uxbox/http/ws.clj +++ b/backend/src/uxbox/http/ws.clj @@ -56,8 +56,10 @@ (defmethod handle-message :disconnect [{:keys [user-id] :as ws} {:keys [file-id] :as message}] - (swap! state update file-id dissoc user-id) - nil) + (let [local (swap! state update file-id dissoc user-id) + sessions (get local file-id) + message {:type :who :users (set (keys sessions))}] + (run! #(send! % message) (vals sessions)))) (defmethod handle-message :who [{:keys [file-id] :as ws} message] diff --git a/backend/src/uxbox/services/queries/project_files.clj b/backend/src/uxbox/services/queries/project_files.clj index 06fa82135f..920a684cb7 100644 --- a/backend/src/uxbox/services/queries/project_files.clj +++ b/backend/src/uxbox/services/queries/project_files.clj @@ -22,6 +22,7 @@ (s/def ::id ::us/uuid) (s/def ::name ::us/string) (s/def ::project-id ::us/uuid) +(s/def ::file-id ::us/uuid) (s/def ::user ::us/uuid) (su/defstr sql:generic-project-files @@ -75,7 +76,6 @@ (-> (db/query conn [sql:recent-files user 20]) (p/then' (partial mapv decode-row)))) - ;; --- Query: Project File (By ID) (su/defstr sql:project-file @@ -90,6 +90,40 @@ (-> (db/query-one db/pool [sql:project-file user id]) (p/then' decode-row))) +;; --- Query: Users of the File + +(su/defstr sql:file-users + "select u.id, u.fullname, u.photo + from users as u + join project_file_users as pfu on (pfu.user_id = u.id) + where pfu.file_id = $1 + union all + select u.id, u.fullname, u.photo + from users as u + join project_users as pu on (pu.user_id = u.id) + where pu.project_id = $2") + +(declare retrieve-minimal-file) + +(su/defstr sql:minimal-file + "with files as (~{sql:generic-project-files}) + select id, project_id from files where id = $2") + +(s/def ::project-file-users + (s/keys :req-un [::user ::file-id])) + +(sq/defquery ::project-file-users + [{:keys [user file-id] :as params}] + (db/with-atomic [conn db/pool] + (-> (retrieve-minimal-file conn user file-id) + (p/then (fn [{:keys [id project-id]}] + (prn ::project-file-users id project-id) + (db/query conn [sql:file-users id project-id])))))) + +(defn- retrieve-minimal-file + [conn user-id file-id] + (-> (db/query-one conn [sql:minimal-file user-id file-id]) + (p/then' su/raise-not-found-if-nil))) ;; --- Helpers diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index 854e406516..3785731d07 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -8,6 +8,7 @@ (:require [cljs.spec.alpha :as s] [beicon.core :as rx] + [cuerdas.core :as str] [potok.core :as ptk] [uxbox.main.repo.core :as rp] [uxbox.util.i18n :as i18n :refer [tr]] @@ -31,7 +32,7 @@ ;; --- Profile Fetched -(s/def ::profile-fetched-params +(s/def ::profile-fetched (s/keys :req-un [::id ::username ::fullname @@ -41,8 +42,8 @@ (defn profile-fetched [data] - (s/assert ::profile-fetched-params data) - (reify + (s/assert ::profile-fetched data) + (ptk/reify ::profile-fetched ptk/UpdateEvent (update [_ state] (assoc state :profile data)) diff --git a/frontend/src/uxbox/main/data/workspace_websocket.cljs b/frontend/src/uxbox/main/data/workspace_websocket.cljs new file mode 100644 index 0000000000..c115e33237 --- /dev/null +++ b/frontend/src/uxbox/main/data/workspace_websocket.cljs @@ -0,0 +1,98 @@ +;; 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/. +;; +;; Copyright (c) 2019 Andrey Antukh + +(ns uxbox.main.data.workspace-websocket + (:require + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [potok.core :as ptk] + [uxbox.config :as cfg] + [uxbox.common.data :as d] + [uxbox.common.pages :as cp] + [uxbox.main.websockets :as ws] + [uxbox.main.data.icons :as udi] + [uxbox.main.data.projects :as dp] + [uxbox.main.repo.core :as rp] + [uxbox.main.store :as st] + [uxbox.util.transit :as t] + [vendor.randomcolor])) + +;; --- Initialize WebSocket + +(declare fetch-users) +(declare handle-who) + +(s/def ::type keyword?) +(s/def ::message + (s/keys :req-un [::type])) + +(defn initialize + [file-id] + (ptk/reify ::initialize + ptk/UpdateEvent + (update [_ state] + ;; (prn "initialize-websocket$update" file-id) + (let [uri (str "ws://localhost:6060/sub/" file-id)] + (assoc-in state [::ws file-id] (ws/open uri)))) + + ptk/WatchEvent + (watch [_ state stream] + ;; (prn "initialize-websocket$watch" file-id) + (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/tap #(js/console.log "ws-message" file-id %)) + (rx/map (fn [{:keys [type] :as msg}] + (case type + :who (handle-who msg) + ::unknown)))))))) + +;; --- Finalize Websocket + +(defn finalize + [file-id] + (ptk/reify ::finalize + ptk/EffectEvent + (effect [_ state stream] + (ws/-close (get-in state [::ws file-id]))))) + +;; --- 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] + (assoc-in state [:workspace-users :by-id (:id user)] user)) + state + users)))) + + +;; --- Handle: Who + +;; TODO: assign color + +(defn handle-who + [{:keys [users] :as msg}] + (s/assert set? users) + (ptk/reify ::handle-who + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-users :active] users)))) diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index 76666c9628..bab162d0e0 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -13,7 +13,8 @@ [uxbox.main.constants :as c] [uxbox.main.data.history :as udh] [uxbox.main.data.undo :as udu] - [uxbox.main.data.workspace :as udw] + [uxbox.main.data.workspace :as dw] + [uxbox.main.data.workspace-websocket :as dws] [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.ui.confirm] @@ -54,8 +55,8 @@ (dom/prevent-default event) (dom/stop-propagation event) (if (pos? (.-deltaY event)) - (st/emit! (udw/decrease-zoom)) - (st/emit! (udw/increase-zoom))) + (st/emit! (dw/decrease-zoom)) + (st/emit! (dw/increase-zoom))) (scroll/scroll-to-point dom mouse-point scroll-position)))) (mf/defc workspace-content @@ -96,17 +97,18 @@ [{:keys [file-id page-id] :as props}] (mf/use-effect - {:deps #js [file-id page-id] + {:deps #js [(str file-id) + (str page-id)] :fn (fn [] (let [sub (shortcuts/init)] - (st/emit! (udw/initialize file-id page-id)) + (st/emit! (dw/initialize file-id page-id)) #(rx/cancel! sub)))}) (mf/use-effect {:deps #js [(str file-id)] :fn (fn [] - (st/emit! (udw/initialize-websocket file-id)) - #(st/emit! (udw/finalize-websocket file-id)))}) + (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) diff --git a/frontend/src/uxbox/main/ui/workspace/header.cljs b/frontend/src/uxbox/main/ui/workspace/header.cljs index f2bff40bb0..798882408f 100644 --- a/frontend/src/uxbox/main/ui/workspace/header.cljs +++ b/frontend/src/uxbox/main/ui/workspace/header.cljs @@ -8,6 +8,7 @@ (ns uxbox.main.ui.workspace.header (:require [rumext.alpha :as mf] + [lentes.core :as l] [uxbox.builtins.icons :as i] [uxbox.config :as cfg] [uxbox.main.data.history :as udh] @@ -40,6 +41,50 @@ ;; --- Header Component +;; (mf/defc user +;; [props] +;; (let [open (mf/use-state false) +;; profile (mf/deref profile-ref) +;; photo (if (str/empty? (:photo profile "")) +;; "/images/avatar.jpg" +;; (:photo profile))] +;; [:div.user-zone {:on-click #(st/emit! (rt/navigate :settings/profile)) +;; :on-mouse-enter #(reset! open true) +;; :on-mouse-leave #(reset! open false)} +;; [:span (:fullname profile)] +;; [:img {:src photo}] +;; (when @open +;; [:& user-menu])])) + + +(def profile-ref + (-> (l/key :profile) + (l/derive st/state))) + +(def users-ref + (-> (l/key :workspace-users) + (l/derive st/state))) + +(mf/defc user-item + [{: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"}]]) + +(mf/defc users + [props] + (let [profile (mf/deref profile-ref) + users (mf/deref users-ref)] + [:ul.user-multi + [:& user-item {:user profile :self? true}] + (for [id (->> (:active users) + (remove #(= % (:id profile))))] + [:& user-item {:user (get-in users [:by-id id]) + :key id}])])) + + (mf/defc header [{:keys [page layout flags] :as props}] (let [toggle #(st/emit! (dw/toggle-flag %)) @@ -60,10 +105,7 @@ [:span {} "Project name / File name";(:name page) ]] - [:ul.user-multi - [:li.tooltip.tooltip-bottom - {:alt "USER_NAME"} - [:img {:src "images/avatar.jpg"}]]] + [:& users] [:div.workspace-options [:ul.options-btn diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap-v2.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap-v2.cljs deleted file mode 100644 index 499f09e22d..0000000000 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap-v2.cljs +++ /dev/null @@ -1,111 +0,0 @@ -;; 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/. -;; -;; Copyright (c) 2015-2019 Andrey Antukh -;; Copyright (c) 2015-2016 Juan de la Cruz - -(ns uxbox.main.ui.workspace.sidebar.sitemap - (:require - [cuerdas.core :as str] - [lentes.core :as l] - [rumext.alpha :as mf] - [uxbox.builtins.icons :as i] - [uxbox.main.data.pages :as udp] - [uxbox.main.data.projects :as dp] - [uxbox.main.data.workspace :as dw] - [uxbox.main.store :as st] - [uxbox.main.refs :as refs] - [uxbox.main.ui.confirm :refer [confirm-dialog]] - [uxbox.main.ui.modal :as modal] - [uxbox.main.ui.workspace.sidebar.sitemap-forms :refer [page-form-dialog]] - [uxbox.main.ui.workspace.sortable :refer [use-sortable]] - [uxbox.util.data :refer [classnames enumerate]] - [uxbox.util.dom :as dom] - [uxbox.util.i18n :refer [tr]] - [uxbox.util.router :as rt])) - -;; --- Page Item - -(mf/defc page-item - [{:keys [page index deletable? selected?] :as props}] - (let [on-edit #(modal/show! page-form-dialog {:page page}) - delete-fn #(st/emit! (udp/delete-page (:id page))) - on-delete #(do - (dom/prevent-default %) - (dom/stop-propagation %) - (modal/show! confirm-dialog {:on-accept delete-fn})) - on-drop #(do (prn "TODO")) - on-hover #(st/emit! (dw/change-page-order {:id (:id page) - :index index})) - - navigate-fn #(st/emit! (dw/go-to-page (:id page))) - [dprops ref] (use-sortable {:type "page-item" - :data {:id (:id page) - :index index} - :on-hover on-hover - :on-drop on-drop})] - [:li {:ref ref :class (classnames :selected selected?)} - [:div.element-list-body - {:class (classnames :selected selected? - :dragging (:dragging? dprops)) - :on-click navigate-fn - :on-double-click #(dom/stop-propagation %) - :draggable true} - - [:div.page-icon i/page] - [:span (:name page)] - [:div.page-actions {} - [:a {:on-click on-edit} i/pencil] - (when deletable? - [:a {:on-click on-delete} i/trash])]]])) - - -;; --- Page Item Wrapper - -(defn- make-page-ref - [page-id] - (-> (l/in [:pages page-id]) - (l/derive st/state))) - -(mf/defc page-item-wrapper - [{:keys [page-id index deletable? selected?] :as props}] - (let [page-ref (mf/use-memo {:deps #js [page-id] - :fn #(make-page-ref page-id)}) - page (mf/deref page-ref)] - [:& page-item {:page page - :index index - :deletable? deletable? - :selected? selected?}])) - -;; --- Pages List - -(mf/defc pages-list - [{:keys [file current-page] :as props}] - (let [pages (enumerate (:pages file)) - deletable? (> (count pages) 1)] - [:ul.element-list - (for [[index page-id] pages] - [:& page-item-wrapper - {:page-id page-id - :index index - :deletable? deletable? - :selected? (= page-id (:id current-page)) - :key page-id}])])) - -;; --- Sitemap Toolbox - -(mf/defc sitemap-toolbox - [{:keys [file page] :as props}] - (let [create-fn #(modal/show! page-form-dialog {:page {:file-id (:file-id page)}}) - close-fn #(st/emit! (dw/toggle-layout-flag :sitemap))] - [:div.sitemap.tool-window - [:div.tool-window-bar - [:div.tool-window-icon i/project-tree] - [:span (tr "ds.settings.sitemap")] - [:div.tool-window-close {:on-click close-fn} i/close]] - [:div.tool-window-content - [:div.project-title - #_[:span (:name project)] - [:div.add-page {:on-click create-fn} i/close]] - [:& pages-list {:file file :current-page page}]]])) diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 65f8f6ed28..9c4b032798 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -73,14 +73,29 @@ ;; --- Selection Rect +(defn- selection->rect + [data] + (let [start (:start data) + stop (:stop data) + start-x (min (:x start) (:x stop)) + start-y (min (:y start) (:y stop)) + end-x (max (:x start) (:x stop)) + end-y (max (:y start) (:y stop))] + (assoc data + :x1 start-x + :y1 start-y + :x2 end-x + :y2 end-y + :type :rect))) + (def ^:private handle-selrect (letfn [(update-state [state position] (let [selrect (get-in state [:workspace-local :selrect])] (if selrect (assoc-in state [:workspace-local :selrect] - (dw/selection->rect (assoc selrect :stop position))) + (selection->rect (assoc selrect :stop position))) (assoc-in state [:workspace-local :selrect] - (dw/selection->rect {:start position :stop position}))))) + (selection->rect {:start position :stop position}))))) (clear-state [state] (update state :workspace-local dissoc :selrect))]