浏览代码

refactor: move left sidebar out from container

Tienson Qin 4 月之前
父节点
当前提交
0fcd0116ed

+ 8 - 545
src/main/frontend/components/container.cljs

@@ -2,54 +2,38 @@
   (:require [cljs-drag-n-drop.core :as dnd]
   (:require [cljs-drag-n-drop.core :as dnd]
             [clojure.string :as string]
             [clojure.string :as string]
             [dommy.core :as d]
             [dommy.core :as d]
-            [electron.ipc :as ipc]
-            [frontend.components.block :as block]
             [frontend.components.content :as cp-content]
             [frontend.components.content :as cp-content]
-            [frontend.components.dnd :as dnd-component]
             [frontend.components.find-in-page :as find-in-page]
             [frontend.components.find-in-page :as find-in-page]
             [frontend.components.handbooks :as handbooks]
             [frontend.components.handbooks :as handbooks]
             [frontend.components.header :as header]
             [frontend.components.header :as header]
-            [frontend.components.icon :as icon]
             [frontend.components.journal :as journal]
             [frontend.components.journal :as journal]
+            [frontend.components.left-sidebar :as app-left-sidebar]
             [frontend.components.plugins :as plugins]
             [frontend.components.plugins :as plugins]
-            [frontend.components.repo :as repo]
             [frontend.components.right-sidebar :as right-sidebar]
             [frontend.components.right-sidebar :as right-sidebar]
             [frontend.components.theme :as theme]
             [frontend.components.theme :as theme]
             [frontend.components.window-controls :as window-controls]
             [frontend.components.window-controls :as window-controls]
             [frontend.config :as config]
             [frontend.config :as config]
-            [frontend.context.i18n :refer [t tt]]
+            [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.async :as db-async]
             [frontend.db.async :as db-async]
             [frontend.db.model :as db-model]
             [frontend.db.model :as db-model]
-            [frontend.extensions.fsrs :as fsrs]
-            [frontend.extensions.pdf.utils :as pdf-utils]
-            [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.page :as page-handler]
-            [frontend.handler.recent :as recent-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.handler.whiteboard :as whiteboard-handler]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
             [frontend.mobile.footer :as footer]
             [frontend.mobile.footer :as footer]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.shortcut.data-helper :as shortcut-dh]
             [frontend.modules.shortcut.data-helper :as shortcut-dh]
-            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.state :as state]
             [frontend.state :as state]
-            [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
             [frontend.util.cursor :as cursor]
-            [frontend.util.page :as page-util]
             [frontend.version :refer [version]]
             [frontend.version :refer [version]]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.object :as gobj]
-            [logseq.common.config :as common-config]
             [logseq.common.path :as path]
             [logseq.common.path :as path]
-            [logseq.common.util.namespace :as ns-util]
-            [logseq.db :as ldb]
             [logseq.shui.dialog.core :as shui-dialog]
             [logseq.shui.dialog.core :as shui-dialog]
             [logseq.shui.hooks :as hooks]
             [logseq.shui.hooks :as hooks]
             [logseq.shui.popup.core :as shui-popup]
             [logseq.shui.popup.core :as shui-popup]
@@ -61,528 +45,6 @@
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
             [rum.core :as rum]))
 
 
-(rum/defc sidebar-content-group < rum/reactive
-  [name {:keys [class count more header-props enter-show-more? collapsable?]} child]
-  (let [collapsed? (state/sub [:ui/navigation-item-collapsed? class])]
-    [:div.sidebar-content-group
-     {:class (util/classnames [class {:is-expand (not collapsed?)
-                                      :has-children (and (number? count) (> count 0))}])}
-     [:div.sidebar-content-group-inner
-      [:div.hd.items-center
-       (cond-> (merge header-props
-                      {:class (util/classnames [(:class header-props)
-                                                {:non-collapsable (false? collapsable?)
-                                                 :enter-show-more (true? enter-show-more?)}])})
-
-         (not (false? collapsable?))
-         (assoc :on-click (fn [^js/MouseEvent _e]
-                            (state/toggle-navigation-item-collapsed! class))))
-       [:span.a name]
-       [:span.b (or more (ui/icon "chevron-right" {:class "more" :size 15}))]]
-      (when child [:div.bd child])]]))
-
-(rum/defc ^:large-vars/cleanup-todo page-name < rum/reactive db-mixins/query
-  [page recent?]
-  (when-let [id (:db/id page)]
-    (let [page (db/sub-block id)
-          repo (state/get-current-repo)
-          db-based? (config/db-based-graph? repo)
-          icon (icon/get-node-icon-cp page {:size 16})
-          title (:block/title page)
-          untitled? (db-model/untitled-page? title)
-          name (:block/name page)
-          file-rpath (when (util/electron?) (page-util/get-page-file-rpath name))
-          ctx-icon #(shui/tabler-icon %1 {:class "scale-90 pr-1 opacity-80"})
-          open-in-sidebar #(state/sidebar-add-block!
-                            (state/get-current-repo)
-                            (:db/id page)
-                            :page)
-          x-menu-content (fn []
-                           (let [x-menu-item shui/dropdown-menu-item
-                                 x-menu-shortcut shui/dropdown-menu-shortcut]
-                             [:<>
-                              (when-not recent?
-                                (x-menu-item
-                                 {:key "unfavorite"
-                                  :on-click #(page-handler/<unfavorite-page! (if db-based? (str (:block/uuid page)) title))}
-                                 (ctx-icon "star-off")
-                                 (t :page/unfavorite)
-                                 (x-menu-shortcut (when-let [binding (shortcut-dh/shortcut-binding :command/toggle-favorite)]
-                                                    (some-> binding
-                                                            (first)
-                                                            (shortcut-utils/decorate-binding))))))
-                              (when-let [page-fpath (and (util/electron?) file-rpath
-                                                         (config/get-repo-fpath (state/get-current-repo) file-rpath))]
-                                [:<>
-                                 (x-menu-item
-                                  {:key "open-in-folder"
-                                   :on-click #(ipc/ipc :openFileInFolder page-fpath)}
-                                  (ctx-icon "folder")
-                                  (t :page/open-in-finder))
-
-                                 (x-menu-item
-                                  {:key "open with default app"
-                                   :on-click #(js/window.apis.openPath page-fpath)}
-                                  (ctx-icon "file")
-                                  (t :page/open-with-default-app))])
-                              (x-menu-item
-                               {:key "open in sidebar"
-                                :on-click open-in-sidebar}
-                               (ctx-icon "layout-sidebar-right")
-                               (t :content/open-in-sidebar)
-                               (x-menu-shortcut (shortcut-utils/decorate-binding "shift+click")))]))]
-
-    ;; TODO: move to standalone component
-      [:a.link-item.group
-       (if (util/mobile?)
-         {:on-pointer-down util/stop-propagation
-          :on-pointer-up (fn [_e]
-                           (route-handler/redirect-to-page! (:block/uuid page) {:click-from-recent? recent?}))}
-         (cond->
-          {:on-click
-           (fn [e]
-             (if (gobj/get e "shiftKey")
-               (open-in-sidebar)
-               (route-handler/redirect-to-page! (:block/uuid page) {:click-from-recent? recent?})))
-           :on-context-menu (fn [^js e]
-                              (shui/popup-show! e (x-menu-content)
-                                                {:as-dropdown? true
-                                                 :content-props {:on-click (fn [] (shui/popup-hide!))
-                                                                 :class "w-60"}})
-                              (util/stop e))}
-           (ldb/object? page)
-           (assoc :title (block-handler/block-unique-title page))))
-       [:span.page-icon {:key "page-icon"} icon]
-       [:span.page-title {:key "title"
-                          :class (when untitled? "opacity-50")
-                          :style {:display "ruby"}}
-        (cond
-          (not (db/page? page))
-          (block/inline-text :markdown (:block/title page))
-          untitled? (t :untitled)
-          :else (let [title' (pdf-utils/fix-local-asset-pagename title)
-                      parent (:block/parent page)]
-                  (if (and parent
-                           (not (or (ldb/class? page)
-                                    (and (:logseq.property/built-in? parent)
-                                         (= (:block/title parent)
-                                            common-config/library-page-name)))))
-                    (str (:block/title parent) ns-util/parent-char title')
-                    title')))]
-
-     ;; dots trigger
-       (shui/button
-        {:key "more actions"
-         :size :sm
-         :variant :ghost
-         :class "absolute !bg-transparent right-0 top-0 px-1.5 scale-75 opacity-40 hidden group-hover:block hover:opacity-80 active:opacity-100"
-         :on-click #(do
-                      (shui/popup-show! (.-target %) (x-menu-content)
-                                        {:as-dropdown? true
-                                         :content-props {:on-click (fn [] (shui/popup-hide!))
-                                                         :class "w-60"}})
-                      (util/stop %))}
-        [:i.relative {:style {:top "4px"}} (shui/tabler-icon "dots")])])))
-
-(defn sidebar-item
-  [{:keys [on-click-handler class title icon icon-extension? active href shortcut more]}]
-  [:div
-   {:key class
-    :class (util/classnames [class {:active active}])}
-   [:a.item.group.flex.items-center.text-sm.rounded-md.font-medium
-    {:on-click on-click-handler
-     :class (when active "active")
-     :href href}
-    (ui/icon (str icon) {:extension? icon-extension? :size 16})
-    [:span.flex-1 title]
-    (when shortcut
-      [:span.ml-1
-       (ui/render-keyboard-shortcut
-        (ui/keyboard-shortcut-from-config shortcut {:pick-first? true}))])
-    more]])
-
-(rum/defc sidebar-graphs
-  []
-  [:div.sidebar-graphs
-   (repo/graphs-selector)])
-
-(rum/defc sidebar-navigations-edit-content
-  [{:keys [_id navs checked-navs set-checked-navs!]}]
-  (let [[local-navs set-local-navs!] (rum/use-state checked-navs)]
-
-    (hooks/use-effect!
-     (fn []
-       (set-checked-navs! local-navs))
-     [local-navs])
-
-    (for [nav navs
-          :let [name' (name nav)]]
-      (shui/dropdown-menu-checkbox-item
-       {:checked (contains? (set local-navs) nav)
-        :onCheckedChange (fn [v] (set-local-navs!
-                                  (fn []
-                                    (if v
-                                      (conj local-navs nav)
-                                      (filterv #(not= nav %) local-navs)))))}
-       (tt (keyword "left-side-bar" name')
-           (keyword "right-side-bar" name'))))))
-
-(rum/defc ^:large-vars/cleanup-todo sidebar-navigations
-  [{:keys [default-home route-match route-name srs-open? db-based? enable-whiteboards?]}]
-  (let [navs (cond-> [:flashcards :graph-view :all-pages]
-               db-based?
-               (concat [:tag/tasks :tag/assets])
-               (not db-based?)
-               (#(cons :whiteboards %)))
-        [checked-navs set-checked-navs!] (rum/use-state (or (storage/get :ls-sidebar-navigations)
-                                                            [:whiteboards :flashcards :graph-view :all-pages]))]
-
-    (hooks/use-effect!
-     (fn []
-       (when (vector? checked-navs)
-         (storage/set :ls-sidebar-navigations checked-navs)))
-     [checked-navs])
-
-    (sidebar-content-group
-     [:a.wrap-th [:strong.flex-1 "Navigations"]]
-     {:collapsable? false
-      :enter-show-more? true
-      :header-props {:on-click (fn [^js e] (when-let [^js _el (some-> (.-target e) (.closest ".as-edit"))]
-                                             (shui/popup-show! _el
-                                                               #(sidebar-navigations-edit-content
-                                                                 {:id (:id %) :navs navs
-                                                                  :checked-navs checked-navs
-                                                                  :set-checked-navs! set-checked-navs!})
-                                                               {:as-dropdown? false})))}
-      :more [:a.as-edit {:class "!opacity-60 hover:!opacity-80 relative -top-0.5 -right-0.5"}
-             (shui/tabler-icon "filter-edit" {:size 14})]}
-     [:div.sidebar-navigations.flex.flex-col.mt-1
-       ;; required custom home page
-      (let [page (:page default-home)
-            enable-journals? (state/enable-journals? (state/get-current-repo))]
-        (if (and page (not enable-journals?))
-          (sidebar-item
-           {:class "home-nav"
-            :title page
-            :on-click-handler route-handler/redirect-to-home!
-            :active (and (not srs-open?)
-                         (= route-name :page)
-                         (= page (get-in route-match [:path-params :name])))
-            :icon "home"
-            :shortcut :go/home})
-
-          (when enable-journals?
-            (sidebar-item
-             {:class "journals-nav"
-              :active (and (not srs-open?)
-                           (or (= route-name :all-journals) (= route-name :home)))
-              :title (t :left-side-bar/journals)
-              :on-click-handler (fn [e]
-                                  (if (gobj/get e "shiftKey")
-                                    (route-handler/sidebar-journals!)
-                                    (route-handler/go-to-journals!)))
-              :icon "calendar"
-              :shortcut :go/journals}))))
-
-      (for [nav checked-navs]
-        (cond
-          (= nav :whiteboards)
-          (when enable-whiteboards?
-            (when (not db-based?)
-              (sidebar-item
-               {:class "whiteboard"
-                :title (t :right-side-bar/whiteboards)
-                :href (rfe/href :whiteboards)
-                :on-click-handler (fn [_e] (whiteboard-handler/onboarding-show))
-                :active (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
-                :icon "writing"
-                :shortcut :go/whiteboards})))
-
-          (= nav :flashcards)
-          (when (state/enable-flashcards? (state/get-current-repo))
-            (let [num (state/sub :srs/cards-due-count)]
-              (sidebar-item
-               {:class "flashcards-nav"
-                :title (t :right-side-bar/flashcards)
-                :icon "infinity"
-                :shortcut :go/flashcards
-                :active srs-open?
-                :on-click-handler #(do (fsrs/update-due-cards-count)
-                                       (state/pub-event! [:modal/show-cards]))
-                :more (when (and num (not (zero? num)))
-                        [:span.ml-1.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])})))
-
-          (= nav :graph-view)
-          (sidebar-item
-           {:class "graph-view-nav"
-            :title (t :right-side-bar/graph-view)
-            :href (rfe/href :graph)
-            :active (and (not srs-open?) (= route-name :graph))
-            :icon "hierarchy"
-            :shortcut :go/graph-view})
-
-          (= nav :all-pages)
-          (sidebar-item
-           {:class "all-pages-nav"
-            :title (t :right-side-bar/all-pages)
-            :href (rfe/href :all-pages)
-            :active (and (not srs-open?) (= route-name :all-pages))
-            :icon "files"})
-
-          (= (namespace nav) "tag")
-          (when db-based?
-            (let [name'' (name nav)
-                  class-ident (get {"assets" :logseq.class/Asset  "tasks" :logseq.class/Task} name'')]
-              (when-let [tag-uuid (and class-ident (:block/uuid (db/entity class-ident)))]
-                (sidebar-item
-                 {:class (str "tag-view-nav " name'')
-                  :title (tt (keyword "left-side-bar" name'')
-                             (keyword "right-side-bar" name''))
-                  :href (rfe/href :page {:name tag-uuid})
-                  :active (= (str tag-uuid) (get-in route-match [:path-params :name]))
-                  :icon "hash"}))))))])))
-
-(rum/defc sidebar-favorites < rum/reactive
-  []
-  (let [_favorites-updated? (state/sub :favorites/updated?)
-        favorite-entities (page-handler/get-favorites)]
-    (sidebar-content-group
-     [:a.wrap-th
-      [:strong.flex-1 (t :left-side-bar/nav-favorites)]]
-
-     {:class "favorites"
-      :count (count favorite-entities)
-      :edit-fn
-      (fn [e]
-        (rfe/push-state :page {:name "Favorites"})
-        (util/stop e))}
-     (when (seq favorite-entities)
-       (let [favorite-items (map
-                             (fn [e]
-                               {:id (str (:db/id e))
-                                :value (:block/uuid e)
-                                :content [:li.favorite-item.font-medium (page-name e false)]})
-                             favorite-entities)]
-         (dnd-component/items favorite-items
-                              {:on-drag-end (fn [favorites']
-                                              (page-handler/<reorder-favorites! favorites'))
-                               :parent-node :ul.favorites.text-sm}))))))
-
-(rum/defc sidebar-recent-pages < rum/reactive db-mixins/query
-  []
-  (let [pages (recent-handler/get-recent-pages)]
-    (sidebar-content-group
-     [:a.wrap-th [:strong.flex-1 (t :left-side-bar/nav-recent-pages)]]
-
-     {:class "recent"
-      :count (count pages)}
-
-     [:ul.text-sm
-      (for [page pages]
-        [:li.recent-item.select-none.font-medium
-         {:key (str "recent-" (:db/id page))
-          :title (block-handler/block-unique-title page)}
-         (page-name page true)])])))
-
-(defn get-default-home-if-valid
-  []
-  (when-let [default-home (state/get-default-home)]
-    (let [page (:page default-home)
-          page (when (and (string? page)
-                          (not (string/blank? page)))
-                 (db/get-page page))]
-      (if page
-        default-home
-        (dissoc default-home :page)))))
-
-(rum/defc ^:large-vars/cleanup-todo sidebar-container
-  [route-match close-modal-fn left-sidebar-open? enable-whiteboards? srs-open?
-   *closing? close-signal touching-x-offset]
-  (let [[local-closing? set-local-closing?] (rum/use-state false)
-        [el-rect set-el-rect!] (rum/use-state nil)
-        ref-el (rum/use-ref nil)
-        ref-open? (rum/use-ref left-sidebar-open?)
-        db-based? (config/db-based-graph? (state/get-current-repo))
-        default-home (get-default-home-if-valid)
-        route-name (get-in route-match [:data :name])
-        on-contents-scroll #(when-let [^js el (.-target %)]
-                              (let [top (.-scrollTop el)
-                                    cls (.-classList el)
-                                    cls' "is-scrolled"]
-                                (if (> top 2)
-                                  (.add cls cls')
-                                  (.remove cls cls'))))
-        close-fn #(set-local-closing? true)
-        touching-x-offset (when (number? touching-x-offset)
-                            (if-not left-sidebar-open?
-                              (when (> touching-x-offset 0)
-                                (min touching-x-offset (:width el-rect)))
-                              (when (< touching-x-offset 0)
-                                (max touching-x-offset (- 0 (:width el-rect))))))
-        offset-ratio (and (number? touching-x-offset)
-                          (some->> (:width el-rect)
-                                   (/ touching-x-offset)))]
-
-    (hooks/use-effect!
-     #(js/setTimeout
-       (fn [] (some-> (rum/deref ref-el)
-                      (.getBoundingClientRect)
-                      (.toJSON)
-                      (js->clj :keywordize-keys true)
-                      (set-el-rect!)))
-       16)
-     [])
-
-    (hooks/use-layout-effect!
-     (fn []
-       (when (and (rum/deref ref-open?) local-closing?)
-         (reset! *closing? true))
-       (rum/set-ref! ref-open? left-sidebar-open?)
-       #())
-     [local-closing? left-sidebar-open?])
-
-    (hooks/use-effect!
-     (fn []
-       (when-not (neg? close-signal)
-         (close-fn)))
-     [close-signal])
-
-    [:<>
-     [:div.left-sidebar-inner.as-container.flex-1.flex.flex-col.min-h-0
-      {:key "left-sidebar"
-       :ref ref-el
-       :style (cond-> {}
-                (and (number? offset-ratio)
-                     (> touching-x-offset 0))
-                (assoc :transform (str "translate3d(calc(" touching-x-offset "px - 100%), 0, 0)"))
-
-                (and (number? offset-ratio)
-                     (< touching-x-offset 0))
-                (assoc :transform (str "translate3d(" (* offset-ratio 100) "%, 0, 0)")))
-       :on-transition-end (fn []
-                            (when local-closing?
-                              (reset! *closing? false)
-                              (set-local-closing? false)
-                              (close-modal-fn)))
-       :on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
-                    (when (some (fn [sel] (boolean (.closest target sel)))
-                                [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
-                      (close-fn)))}
-
-      [:div.wrap
-       [:div.sidebar-header-container
-        ;; sidebar graphs
-        (when (not config/publishing?)
-          (sidebar-graphs))
-
-        ;; sidebar sticky navigations
-        (sidebar-navigations
-         {:default-home default-home
-          :route-match route-match
-          :db-based? db-based?
-          :enable-whiteboards? enable-whiteboards?
-          :route-name route-name
-          :srs-open? srs-open?})]
-
-       [:div.sidebar-contents-container
-        {:on-scroll on-contents-scroll}
-        (sidebar-favorites)
-
-        (when (not config/publishing?)
-          (sidebar-recent-pages))]]]
-
-     [:span.shade-mask
-      (cond-> {:on-click close-fn
-               :key "shade-mask"}
-        (number? offset-ratio)
-        (assoc :style {:opacity (cond-> offset-ratio
-                                  (neg? offset-ratio)
-                                  (+ 1))}))]]))
-
-(rum/defc sidebar-resizer
-  []
-  (let [*el-ref (rum/use-ref nil)
-        ^js el-doc js/document.documentElement
-        adjust-size! (fn [width]
-                       (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width)
-                       (storage/set :ls-left-sidebar-width width))]
-
-    ;; restore size
-    (hooks/use-layout-effect!
-     (fn []
-       (when-let [width (storage/get :ls-left-sidebar-width)]
-         (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width))))
-
-    ;; draggable handler
-    (hooks/use-effect!
-     (fn []
-       (when-let [el (and (fn? js/window.interact) (rum/deref *el-ref))]
-         (let [^js sidebar-el (.querySelector el-doc "#left-sidebar")]
-           (-> (js/interact el)
-               (.draggable
-                #js {:listeners
-                     #js {:move (fn [^js/MouseEvent e]
-                                  (when-let [offset (.-left (.-rect e))]
-                                    (let [width (.toFixed (max (min offset 460) 240) 2)]
-                                      (adjust-size! (str width "px")))))}})
-               (.styleCursor false)
-               (.on "dragstart" (fn []
-                                  (.. sidebar-el -classList (add "is-resizing"))
-                                  (.. el-doc -classList (add "is-resizing-buf"))))
-               (.on "dragend" (fn []
-                                (.. sidebar-el -classList (remove "is-resizing"))
-                                (.. el-doc -classList (remove "is-resizing-buf"))))))
-         #()))
-     [])
-    [:span.left-sidebar-resizer {:ref *el-ref}]))
-
-(rum/defcs left-sidebar < rum/reactive
-  (rum/local false ::closing?)
-  (rum/local -1 ::close-signal)
-  (rum/local nil ::touch-state)
-  [s {:keys [left-sidebar-open? route-match]}]
-  (let [close-fn #(state/set-left-sidebar-open! false)
-        *closing? (::closing? s)
-        *touch-state (::touch-state s)
-        *close-signal (::close-signal s)
-        enable-whiteboards? (state/enable-whiteboards?)
-        touch-point-fn (fn [^js e] (some-> (gobj/get e "touches") (aget 0) (#(hash-map :x (.-clientX %) :y (.-clientY %)))))
-        srs-open? (= :srs (state/sub :modal/id))
-        touching-x-offset (and (some-> @*touch-state :after)
-                               (some->> @*touch-state
-                                        ((juxt :after :before))
-                                        (map :x) (apply -)))
-        touch-pending? (> (abs touching-x-offset) 20)]
-
-    [:div#left-sidebar.cp__sidebar-left-layout
-     {:class (util/classnames [{:is-open left-sidebar-open?
-                                :is-closing @*closing?
-                                :is-touching touch-pending?}])
-      :on-touch-start
-      (fn [^js e]
-        (reset! *touch-state {:before (touch-point-fn e)}))
-      :on-touch-move
-      (fn [^js e]
-        (when @*touch-state
-          (some-> *touch-state (swap! assoc :after (touch-point-fn e)))))
-      :on-touch-end
-      (fn []
-        (when touch-pending?
-          (cond
-            (and (not left-sidebar-open?) (> touching-x-offset 40))
-            (state/set-left-sidebar-open! true)
-
-            (and left-sidebar-open? (< touching-x-offset -30))
-            (reset! *close-signal (inc @*close-signal))))
-        (reset! *touch-state nil))}
-
-     ;; sidebar contents
-     (sidebar-container route-match close-fn left-sidebar-open? enable-whiteboards? srs-open? *closing?
-                        @*close-signal (and touch-pending? touching-x-offset))
-
-     ;; resizer
-     (sidebar-resizer)]))
-
 (rum/defc recording-bar
 (rum/defc recording-bar
   []
   []
   [:> react-draggable
   [:> react-draggable
@@ -627,8 +89,9 @@
      {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
      {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
 
 
      ;; desktop left sidebar layout
      ;; desktop left sidebar layout
-     (left-sidebar {:left-sidebar-open? left-sidebar-open?
-                    :route-match route-match})
+     (app-left-sidebar/left-sidebar
+      {:left-sidebar-open? left-sidebar-open?
+       :route-match route-match})
 
 
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none.relative
      [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none.relative
 
 
@@ -696,7 +159,7 @@
   {:init (fn [state]
   {:init (fn [state]
            (when-not @sidebar-inited?
            (when-not @sidebar-inited?
              (let [current-repo (state/sub :git/current-repo)
              (let [current-repo (state/sub :git/current-repo)
-                   default-home (get-default-home-if-valid)
+                   default-home (app-left-sidebar/get-default-home-if-valid)
                    sidebar (:sidebar default-home)
                    sidebar (:sidebar default-home)
                    sidebar (if (string? sidebar) [sidebar] sidebar)]
                    sidebar (if (string? sidebar) [sidebar] sidebar)]
                (when-let [pages (->> (seq sidebar)
                (when-let [pages (->> (seq sidebar)
@@ -710,7 +173,7 @@
                  (reset! sidebar-inited? true))))
                  (reset! sidebar-inited? true))))
            state)}
            state)}
   []
   []
-  (let [default-home (get-default-home-if-valid)
+  (let [default-home (app-left-sidebar/get-default-home-if-valid)
         current-repo (state/sub :git/current-repo)
         current-repo (state/sub :git/current-repo)
         loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
         loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
         graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
         graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
@@ -986,7 +449,7 @@
         native-titlebar? (state/sub [:electron/user-cfgs :window/native-titlebar?])
         native-titlebar? (state/sub [:electron/user-cfgs :window/native-titlebar?])
         window-controls? (and (util/electron?) (not util/mac?) (not native-titlebar?))
         window-controls? (and (util/electron?) (not util/mac?) (not native-titlebar?))
         edit? (state/editing?)
         edit? (state/editing?)
-        default-home (get-default-home-if-valid)
+        default-home (app-left-sidebar/get-default-home-if-valid)
         logged? (user-handler/logged-in?)
         logged? (user-handler/logged-in?)
         fold-button-on-right? (state/enable-fold-button-right?)
         fold-button-on-right? (state/enable-fold-button-right?)
         show-action-bar? (state/sub :mobile/show-action-bar?)
         show-action-bar? (state/sub :mobile/show-action-bar?)

+ 558 - 0
src/main/frontend/components/left_sidebar.cljs

@@ -0,0 +1,558 @@
+(ns frontend.components.left-sidebar
+  "App left sidebar"
+  (:require [clojure.string :as string]
+            [electron.ipc :as ipc]
+            [frontend.components.block :as block]
+            [frontend.components.dnd :as dnd-component]
+            [frontend.components.icon :as icon]
+            [frontend.components.repo :as repo]
+            [frontend.config :as config]
+            [frontend.context.i18n :refer [t tt]]
+            [frontend.db :as db]
+            [frontend.db-mixins :as db-mixins]
+            [frontend.db.model :as db-model]
+            [frontend.extensions.fsrs :as fsrs]
+            [frontend.extensions.pdf.utils :as pdf-utils]
+            [frontend.handler.block :as block-handler]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.recent :as recent-handler]
+            [frontend.handler.route :as route-handler]
+            [frontend.handler.whiteboard :as whiteboard-handler]
+            [frontend.modules.shortcut.data-helper :as shortcut-dh]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
+            [frontend.state :as state]
+            [frontend.storage :as storage]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [frontend.util.page :as page-util]
+            [goog.object :as gobj]
+            [logseq.common.config :as common-config]
+            [logseq.common.util.namespace :as ns-util]
+            [logseq.db :as ldb]
+            [logseq.shui.hooks :as hooks]
+            [logseq.shui.ui :as shui]
+            [react-draggable]
+            [reitit.frontend.easy :as rfe]
+            [rum.core :as rum]))
+
+(defn get-default-home-if-valid
+  []
+  (when-let [default-home (state/get-default-home)]
+    (let [page (:page default-home)
+          page (when (and (string? page)
+                          (not (string/blank? page)))
+                 (db/get-page page))]
+      (if page
+        default-home
+        (dissoc default-home :page)))))
+
+(rum/defc ^:large-vars/cleanup-todo page-name < rum/reactive db-mixins/query
+  [page recent?]
+  (when-let [id (:db/id page)]
+    (let [page (db/sub-block id)
+          repo (state/get-current-repo)
+          db-based? (config/db-based-graph? repo)
+          icon (icon/get-node-icon-cp page {:size 16})
+          title (:block/title page)
+          untitled? (db-model/untitled-page? title)
+          name (:block/name page)
+          file-rpath (when (util/electron?) (page-util/get-page-file-rpath name))
+          ctx-icon #(shui/tabler-icon %1 {:class "scale-90 pr-1 opacity-80"})
+          open-in-sidebar #(state/sidebar-add-block!
+                            (state/get-current-repo)
+                            (:db/id page)
+                            :page)
+          x-menu-content (fn []
+                           (let [x-menu-item shui/dropdown-menu-item
+                                 x-menu-shortcut shui/dropdown-menu-shortcut]
+                             [:<>
+                              (when-not recent?
+                                (x-menu-item
+                                 {:key "unfavorite"
+                                  :on-click #(page-handler/<unfavorite-page! (if db-based? (str (:block/uuid page)) title))}
+                                 (ctx-icon "star-off")
+                                 (t :page/unfavorite)
+                                 (x-menu-shortcut (when-let [binding (shortcut-dh/shortcut-binding :command/toggle-favorite)]
+                                                    (some-> binding
+                                                            (first)
+                                                            (shortcut-utils/decorate-binding))))))
+                              (when-let [page-fpath (and (util/electron?) file-rpath
+                                                         (config/get-repo-fpath (state/get-current-repo) file-rpath))]
+                                [:<>
+                                 (x-menu-item
+                                  {:key "open-in-folder"
+                                   :on-click #(ipc/ipc :openFileInFolder page-fpath)}
+                                  (ctx-icon "folder")
+                                  (t :page/open-in-finder))
+
+                                 (x-menu-item
+                                  {:key "open with default app"
+                                   :on-click #(js/window.apis.openPath page-fpath)}
+                                  (ctx-icon "file")
+                                  (t :page/open-with-default-app))])
+                              (x-menu-item
+                               {:key "open in sidebar"
+                                :on-click open-in-sidebar}
+                               (ctx-icon "layout-sidebar-right")
+                               (t :content/open-in-sidebar)
+                               (x-menu-shortcut (shortcut-utils/decorate-binding "shift+click")))]))]
+
+    ;; TODO: move to standalone component
+      [:a.link-item.group
+       (if (util/mobile?)
+         {:on-pointer-down util/stop-propagation
+          :on-pointer-up (fn [_e]
+                           (route-handler/redirect-to-page! (:block/uuid page) {:click-from-recent? recent?}))}
+         (cond->
+          {:on-click
+           (fn [e]
+             (if (gobj/get e "shiftKey")
+               (open-in-sidebar)
+               (route-handler/redirect-to-page! (:block/uuid page) {:click-from-recent? recent?})))
+           :on-context-menu (fn [^js e]
+                              (shui/popup-show! e (x-menu-content)
+                                                {:as-dropdown? true
+                                                 :content-props {:on-click (fn [] (shui/popup-hide!))
+                                                                 :class "w-60"}})
+                              (util/stop e))}
+           (ldb/object? page)
+           (assoc :title (block-handler/block-unique-title page))))
+       [:span.page-icon {:key "page-icon"} icon]
+       [:span.page-title {:key "title"
+                          :class (when untitled? "opacity-50")
+                          :style {:display "ruby"}}
+        (cond
+          (not (db/page? page))
+          (block/inline-text :markdown (:block/title page))
+          untitled? (t :untitled)
+          :else (let [title' (pdf-utils/fix-local-asset-pagename title)
+                      parent (:block/parent page)]
+                  (if (and parent
+                           (not (or (ldb/class? page)
+                                    (and (:logseq.property/built-in? parent)
+                                         (= (:block/title parent)
+                                            common-config/library-page-name)))))
+                    (str (:block/title parent) ns-util/parent-char title')
+                    title')))]
+
+     ;; dots trigger
+       (shui/button
+        {:key "more actions"
+         :size :sm
+         :variant :ghost
+         :class "absolute !bg-transparent right-0 top-0 px-1.5 scale-75 opacity-40 hidden group-hover:block hover:opacity-80 active:opacity-100"
+         :on-click #(do
+                      (shui/popup-show! (.-target %) (x-menu-content)
+                                        {:as-dropdown? true
+                                         :content-props {:on-click (fn [] (shui/popup-hide!))
+                                                         :class "w-60"}})
+                      (util/stop %))}
+        [:i.relative {:style {:top "4px"}} (shui/tabler-icon "dots")])])))
+
+(defn sidebar-item
+  [{:keys [on-click-handler class title icon icon-extension? active href shortcut more]}]
+  [:div
+   {:key class
+    :class (util/classnames [class {:active active}])}
+   [:a.item.group.flex.items-center.text-sm.rounded-md.font-medium
+    {:on-click on-click-handler
+     :class (when active "active")
+     :href href}
+    (ui/icon (str icon) {:extension? icon-extension? :size 16})
+    [:span.flex-1 title]
+    (when shortcut
+      [:span.ml-1
+       (ui/render-keyboard-shortcut
+        (ui/keyboard-shortcut-from-config shortcut {:pick-first? true}))])
+    more]])
+
+(rum/defc sidebar-graphs
+  []
+  [:div.sidebar-graphs
+   (repo/graphs-selector)])
+
+(rum/defc sidebar-navigations-edit-content
+  [{:keys [_id navs checked-navs set-checked-navs!]}]
+  (let [[local-navs set-local-navs!] (rum/use-state checked-navs)]
+
+    (hooks/use-effect!
+     (fn []
+       (set-checked-navs! local-navs))
+     [local-navs])
+
+    (for [nav navs
+          :let [name' (name nav)]]
+      (shui/dropdown-menu-checkbox-item
+       {:checked (contains? (set local-navs) nav)
+        :onCheckedChange (fn [v] (set-local-navs!
+                                  (fn []
+                                    (if v
+                                      (conj local-navs nav)
+                                      (filterv #(not= nav %) local-navs)))))}
+       (tt (keyword "left-side-bar" name')
+           (keyword "right-side-bar" name'))))))
+
+(rum/defc sidebar-content-group < rum/reactive
+  [name {:keys [class count more header-props enter-show-more? collapsable?]} child]
+  (let [collapsed? (state/sub [:ui/navigation-item-collapsed? class])]
+    [:div.sidebar-content-group
+     {:class (util/classnames [class {:is-expand (not collapsed?)
+                                      :has-children (and (number? count) (> count 0))}])}
+     [:div.sidebar-content-group-inner
+      [:div.hd.items-center
+       (cond-> (merge header-props
+                      {:class (util/classnames [(:class header-props)
+                                                {:non-collapsable (false? collapsable?)
+                                                 :enter-show-more (true? enter-show-more?)}])})
+
+         (not (false? collapsable?))
+         (assoc :on-click (fn [^js/MouseEvent _e]
+                            (state/toggle-navigation-item-collapsed! class))))
+       [:span.a name]
+       [:span.b (or more (ui/icon "chevron-right" {:class "more" :size 15}))]]
+      (when child [:div.bd child])]]))
+
+(rum/defc ^:large-vars/cleanup-todo sidebar-navigations
+  [{:keys [default-home route-match route-name srs-open? db-based? enable-whiteboards?]}]
+  (let [navs (cond-> [:flashcards :graph-view :all-pages]
+               db-based?
+               (concat [:tag/tasks :tag/assets])
+               (not db-based?)
+               (#(cons :whiteboards %)))
+        [checked-navs set-checked-navs!] (rum/use-state (or (storage/get :ls-sidebar-navigations)
+                                                            [:whiteboards :flashcards :graph-view :all-pages]))]
+
+    (hooks/use-effect!
+     (fn []
+       (when (vector? checked-navs)
+         (storage/set :ls-sidebar-navigations checked-navs)))
+     [checked-navs])
+
+    (sidebar-content-group
+     [:a.wrap-th [:strong.flex-1 "Navigations"]]
+     {:collapsable? false
+      :enter-show-more? true
+      :header-props {:on-click (fn [^js e] (when-let [^js _el (some-> (.-target e) (.closest ".as-edit"))]
+                                             (shui/popup-show! _el
+                                                               #(sidebar-navigations-edit-content
+                                                                 {:id (:id %) :navs navs
+                                                                  :checked-navs checked-navs
+                                                                  :set-checked-navs! set-checked-navs!})
+                                                               {:as-dropdown? false})))}
+      :more [:a.as-edit {:class "!opacity-60 hover:!opacity-80 relative -top-0.5 -right-0.5"}
+             (shui/tabler-icon "filter-edit" {:size 14})]}
+     [:div.sidebar-navigations.flex.flex-col.mt-1
+       ;; required custom home page
+      (let [page (:page default-home)
+            enable-journals? (state/enable-journals? (state/get-current-repo))]
+        (if (and page (not enable-journals?))
+          (sidebar-item
+           {:class "home-nav"
+            :title page
+            :on-click-handler route-handler/redirect-to-home!
+            :active (and (not srs-open?)
+                         (= route-name :page)
+                         (= page (get-in route-match [:path-params :name])))
+            :icon "home"
+            :shortcut :go/home})
+
+          (when enable-journals?
+            (sidebar-item
+             {:class "journals-nav"
+              :active (and (not srs-open?)
+                           (or (= route-name :all-journals) (= route-name :home)))
+              :title (t :left-side-bar/journals)
+              :on-click-handler (fn [e]
+                                  (if (gobj/get e "shiftKey")
+                                    (route-handler/sidebar-journals!)
+                                    (route-handler/go-to-journals!)))
+              :icon "calendar"
+              :shortcut :go/journals}))))
+
+      (for [nav checked-navs]
+        (cond
+          (= nav :whiteboards)
+          (when enable-whiteboards?
+            (when (not db-based?)
+              (sidebar-item
+               {:class "whiteboard"
+                :title (t :right-side-bar/whiteboards)
+                :href (rfe/href :whiteboards)
+                :on-click-handler (fn [_e] (whiteboard-handler/onboarding-show))
+                :active (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
+                :icon "writing"
+                :shortcut :go/whiteboards})))
+
+          (= nav :flashcards)
+          (when (state/enable-flashcards? (state/get-current-repo))
+            (let [num (state/sub :srs/cards-due-count)]
+              (sidebar-item
+               {:class "flashcards-nav"
+                :title (t :right-side-bar/flashcards)
+                :icon "infinity"
+                :shortcut :go/flashcards
+                :active srs-open?
+                :on-click-handler #(do (fsrs/update-due-cards-count)
+                                       (state/pub-event! [:modal/show-cards]))
+                :more (when (and num (not (zero? num)))
+                        [:span.ml-1.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])})))
+
+          (= nav :graph-view)
+          (sidebar-item
+           {:class "graph-view-nav"
+            :title (t :right-side-bar/graph-view)
+            :href (rfe/href :graph)
+            :active (and (not srs-open?) (= route-name :graph))
+            :icon "hierarchy"
+            :shortcut :go/graph-view})
+
+          (= nav :all-pages)
+          (sidebar-item
+           {:class "all-pages-nav"
+            :title (t :right-side-bar/all-pages)
+            :href (rfe/href :all-pages)
+            :active (and (not srs-open?) (= route-name :all-pages))
+            :icon "files"})
+
+          (= (namespace nav) "tag")
+          (when db-based?
+            (let [name'' (name nav)
+                  class-ident (get {"assets" :logseq.class/Asset  "tasks" :logseq.class/Task} name'')]
+              (when-let [tag-uuid (and class-ident (:block/uuid (db/entity class-ident)))]
+                (sidebar-item
+                 {:class (str "tag-view-nav " name'')
+                  :title (tt (keyword "left-side-bar" name'')
+                             (keyword "right-side-bar" name''))
+                  :href (rfe/href :page {:name tag-uuid})
+                  :active (= (str tag-uuid) (get-in route-match [:path-params :name]))
+                  :icon "hash"}))))))])))
+
+(rum/defc sidebar-favorites < rum/reactive
+  []
+  (let [_favorites-updated? (state/sub :favorites/updated?)
+        favorite-entities (page-handler/get-favorites)]
+    (sidebar-content-group
+     [:a.wrap-th
+      [:strong.flex-1 (t :left-side-bar/nav-favorites)]]
+
+     {:class "favorites"
+      :count (count favorite-entities)
+      :edit-fn
+      (fn [e]
+        (rfe/push-state :page {:name "Favorites"})
+        (util/stop e))}
+     (when (seq favorite-entities)
+       (let [favorite-items (map
+                             (fn [e]
+                               {:id (str (:db/id e))
+                                :value (:block/uuid e)
+                                :content [:li.favorite-item.font-medium (page-name e false)]})
+                             favorite-entities)]
+         (dnd-component/items favorite-items
+                              {:on-drag-end (fn [favorites']
+                                              (page-handler/<reorder-favorites! favorites'))
+                               :parent-node :ul.favorites.text-sm}))))))
+
+(rum/defc sidebar-recent-pages < rum/reactive db-mixins/query
+  []
+  (let [pages (recent-handler/get-recent-pages)]
+    (sidebar-content-group
+     [:a.wrap-th [:strong.flex-1 (t :left-side-bar/nav-recent-pages)]]
+
+     {:class "recent"
+      :count (count pages)}
+
+     [:ul.text-sm
+      (for [page pages]
+        [:li.recent-item.select-none.font-medium
+         {:key (str "recent-" (:db/id page))
+          :title (block-handler/block-unique-title page)}
+         (page-name page true)])])))
+
+(rum/defc ^:large-vars/cleanup-todo sidebar-container
+  [route-match close-modal-fn left-sidebar-open? enable-whiteboards? srs-open?
+   *closing? close-signal touching-x-offset]
+  (let [[local-closing? set-local-closing?] (rum/use-state false)
+        [el-rect set-el-rect!] (rum/use-state nil)
+        ref-el (rum/use-ref nil)
+        ref-open? (rum/use-ref left-sidebar-open?)
+        db-based? (config/db-based-graph? (state/get-current-repo))
+        default-home (get-default-home-if-valid)
+        route-name (get-in route-match [:data :name])
+        on-contents-scroll #(when-let [^js el (.-target %)]
+                              (let [top (.-scrollTop el)
+                                    cls (.-classList el)
+                                    cls' "is-scrolled"]
+                                (if (> top 2)
+                                  (.add cls cls')
+                                  (.remove cls cls'))))
+        close-fn #(set-local-closing? true)
+        touching-x-offset (when (number? touching-x-offset)
+                            (if-not left-sidebar-open?
+                              (when (> touching-x-offset 0)
+                                (min touching-x-offset (:width el-rect)))
+                              (when (< touching-x-offset 0)
+                                (max touching-x-offset (- 0 (:width el-rect))))))
+        offset-ratio (and (number? touching-x-offset)
+                          (some->> (:width el-rect)
+                                   (/ touching-x-offset)))]
+
+    (hooks/use-effect!
+     #(js/setTimeout
+       (fn [] (some-> (rum/deref ref-el)
+                      (.getBoundingClientRect)
+                      (.toJSON)
+                      (js->clj :keywordize-keys true)
+                      (set-el-rect!)))
+       16)
+     [])
+
+    (hooks/use-layout-effect!
+     (fn []
+       (when (and (rum/deref ref-open?) local-closing?)
+         (reset! *closing? true))
+       (rum/set-ref! ref-open? left-sidebar-open?)
+       #())
+     [local-closing? left-sidebar-open?])
+
+    (hooks/use-effect!
+     (fn []
+       (when-not (neg? close-signal)
+         (close-fn)))
+     [close-signal])
+
+    [:<>
+     [:div.left-sidebar-inner.as-container.flex-1.flex.flex-col.min-h-0
+      {:key "left-sidebar"
+       :ref ref-el
+       :style (cond-> {}
+                (and (number? offset-ratio)
+                     (> touching-x-offset 0))
+                (assoc :transform (str "translate3d(calc(" touching-x-offset "px - 100%), 0, 0)"))
+
+                (and (number? offset-ratio)
+                     (< touching-x-offset 0))
+                (assoc :transform (str "translate3d(" (* offset-ratio 100) "%, 0, 0)")))
+       :on-transition-end (fn []
+                            (when local-closing?
+                              (reset! *closing? false)
+                              (set-local-closing? false)
+                              (close-modal-fn)))
+       :on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
+                    (when (some (fn [sel] (boolean (.closest target sel)))
+                                [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
+                      (close-fn)))}
+
+      [:div.wrap
+       [:div.sidebar-header-container
+        ;; sidebar graphs
+        (when (not config/publishing?)
+          (sidebar-graphs))
+
+        ;; sidebar sticky navigations
+        (sidebar-navigations
+         {:default-home default-home
+          :route-match route-match
+          :db-based? db-based?
+          :enable-whiteboards? enable-whiteboards?
+          :route-name route-name
+          :srs-open? srs-open?})]
+
+       [:div.sidebar-contents-container
+        {:on-scroll on-contents-scroll}
+        (sidebar-favorites)
+
+        (when (not config/publishing?)
+          (sidebar-recent-pages))]]]
+
+     [:span.shade-mask
+      (cond-> {:on-click close-fn
+               :key "shade-mask"}
+        (number? offset-ratio)
+        (assoc :style {:opacity (cond-> offset-ratio
+                                  (neg? offset-ratio)
+                                  (+ 1))}))]]))
+
+(rum/defc sidebar-resizer
+  []
+  (let [*el-ref (rum/use-ref nil)
+        ^js el-doc js/document.documentElement
+        adjust-size! (fn [width]
+                       (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width)
+                       (storage/set :ls-left-sidebar-width width))]
+
+    ;; restore size
+    (hooks/use-layout-effect!
+     (fn []
+       (when-let [width (storage/get :ls-left-sidebar-width)]
+         (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width))))
+
+    ;; draggable handler
+    (hooks/use-effect!
+     (fn []
+       (when-let [el (and (fn? js/window.interact) (rum/deref *el-ref))]
+         (let [^js sidebar-el (.querySelector el-doc "#left-sidebar")]
+           (-> (js/interact el)
+               (.draggable
+                #js {:listeners
+                     #js {:move (fn [^js/MouseEvent e]
+                                  (when-let [offset (.-left (.-rect e))]
+                                    (let [width (.toFixed (max (min offset 460) 240) 2)]
+                                      (adjust-size! (str width "px")))))}})
+               (.styleCursor false)
+               (.on "dragstart" (fn []
+                                  (.. sidebar-el -classList (add "is-resizing"))
+                                  (.. el-doc -classList (add "is-resizing-buf"))))
+               (.on "dragend" (fn []
+                                (.. sidebar-el -classList (remove "is-resizing"))
+                                (.. el-doc -classList (remove "is-resizing-buf"))))))
+         #()))
+     [])
+    [:span.left-sidebar-resizer {:ref *el-ref}]))
+
+(rum/defcs left-sidebar < rum/reactive
+  (rum/local false ::closing?)
+  (rum/local -1 ::close-signal)
+  (rum/local nil ::touch-state)
+  [s {:keys [left-sidebar-open? route-match]}]
+  (let [close-fn #(state/set-left-sidebar-open! false)
+        *closing? (::closing? s)
+        *touch-state (::touch-state s)
+        *close-signal (::close-signal s)
+        enable-whiteboards? (state/enable-whiteboards?)
+        touch-point-fn (fn [^js e] (some-> (gobj/get e "touches") (aget 0) (#(hash-map :x (.-clientX %) :y (.-clientY %)))))
+        srs-open? (= :srs (state/sub :modal/id))
+        touching-x-offset (and (some-> @*touch-state :after)
+                               (some->> @*touch-state
+                                        ((juxt :after :before))
+                                        (map :x) (apply -)))
+        touch-pending? (> (abs touching-x-offset) 20)]
+
+    [:div#left-sidebar.cp__sidebar-left-layout
+     {:class (util/classnames [{:is-open left-sidebar-open?
+                                :is-closing @*closing?
+                                :is-touching touch-pending?}])
+      :on-touch-start
+      (fn [^js e]
+        (reset! *touch-state {:before (touch-point-fn e)}))
+      :on-touch-move
+      (fn [^js e]
+        (when @*touch-state
+          (some-> *touch-state (swap! assoc :after (touch-point-fn e)))))
+      :on-touch-end
+      (fn []
+        (when touch-pending?
+          (cond
+            (and (not left-sidebar-open?) (> touching-x-offset 40))
+            (state/set-left-sidebar-open! true)
+
+            (and left-sidebar-open? (< touching-x-offset -30))
+            (reset! *close-signal (inc @*close-signal))))
+        (reset! *touch-state nil))}
+
+     ;; sidebar contents
+     (sidebar-container route-match close-fn left-sidebar-open? enable-whiteboards? srs-open? *closing?
+                        @*close-signal (and touch-pending? touching-x-offset))
+
+     ;; resizer
+     (sidebar-resizer)]))

+ 3 - 3
src/main/mobile/components/left_sidebar.cljs

@@ -2,7 +2,7 @@
   "Mobile left sidebar"
   "Mobile left sidebar"
   (:require [cljs-bean.core :as bean]
   (:require [cljs-bean.core :as bean]
             [dommy.core :as dom]
             [dommy.core :as dom]
-            [frontend.components.container :as container]
+            [frontend.components.left-sidebar :as app-left-sidebar]
             [frontend.rum :as r]
             [frontend.rum :as r]
             [logseq.shui.hooks :as hooks]
             [logseq.shui.hooks :as hooks]
             [logseq.shui.silkhq :as silkhq]
             [logseq.shui.silkhq :as silkhq]
@@ -15,8 +15,8 @@
    [:div.left-sidebar-inner
    [:div.left-sidebar-inner
     [:div.sidebar-contents-container
     [:div.sidebar-contents-container
      {:class "!gap-4"}
      {:class "!gap-4"}
-     (container/sidebar-favorites)
-     (container/sidebar-recent-pages)]]])
+     (app-left-sidebar/sidebar-favorites)
+     (app-left-sidebar/sidebar-recent-pages)]]])
 
 
 (rum/defc left-sidebar-inner
 (rum/defc left-sidebar-inner
   []
   []