Browse Source

Merge branch 'feat/db' into feat/capacitor-new

charlie 9 months ago
parent
commit
2bd775715a
39 changed files with 1151 additions and 1135 deletions
  1. 1 1
      deps.edn
  2. 7 3
      deps/db/src/logseq/db/common/view.cljs
  3. 4 2
      deps/db/src/logseq/db/sqlite/build.cljs
  4. 43 12
      deps/db/src/logseq/db/sqlite/export.cljs
  5. 13 8
      deps/outliner/src/logseq/outliner/core.cljs
  6. 1 0
      deps/shui/src/logseq/shui/ui.cljs
  7. 0 1
      package.json
  8. 44 21
      src/main/frontend/common/missionary.cljs
  9. 6 0
      src/main/frontend/components/block.css
  10. 14 13
      src/main/frontend/components/container.cljs
  11. 1 7
      src/main/frontend/components/editor.cljs
  12. 17 20
      src/main/frontend/components/file_based/block.cljs
  13. 11 16
      src/main/frontend/components/file_based/query.cljs
  14. 53 42
      src/main/frontend/components/page.cljs
  15. 9 11
      src/main/frontend/components/plugins.cljs
  16. 2 5
      src/main/frontend/components/property.cljs
  17. 5 7
      src/main/frontend/components/property/config.cljs
  18. 72 44
      src/main/frontend/components/reference_filters.cljs
  19. 26 41
      src/main/frontend/components/settings.cljs
  20. 52 51
      src/main/frontend/components/views.cljs
  21. 32 38
      src/main/frontend/extensions/srs.cljs
  22. 5 2
      src/main/frontend/handler.cljs
  23. 3 3
      src/main/frontend/handler/editor.cljs
  24. 2 599
      src/main/frontend/handler/events.cljs
  25. 354 0
      src/main/frontend/handler/events/ui.cljs
  26. 302 26
      src/main/frontend/handler/file_based/events.cljs
  27. 0 79
      src/main/frontend/tippy-tooltip.css
  28. 12 54
      src/main/frontend/ui.cljs
  29. 8 1
      src/main/frontend/worker/db_worker.cljs
  30. 2 1
      src/main/frontend/worker/export.cljs
  31. 13 0
      src/main/frontend/worker/flows.cljs
  32. 5 1
      src/main/frontend/worker/rtc/client.cljs
  33. 3 1
      src/main/frontend/worker/rtc/ws.cljs
  34. 6 1
      src/main/frontend/worker/state.cljs
  35. 12 0
      src/main/frontend/worker/thread_atom.cljs
  36. 4 4
      src/rtc_e2e_test/client_steps.cljs
  37. 7 7
      src/rtc_e2e_test/helper.cljs
  38. 0 1
      tailwind.all.css
  39. 0 12
      yarn.lock

+ 1 - 1
deps.edn

@@ -40,7 +40,7 @@
   logseq/shui                           {:local/root "deps/shui"}
   metosin/malli                         {:mvn/version "0.16.1"}
   com.cognitect/transit-cljs            {:mvn/version "0.8.280"}
-  missionary/missionary                 {:mvn/version "b.39"}
+  missionary/missionary                 {:mvn/version "b.44"}
   meander/epsilon                       {:mvn/version "0.0.650"}
 
   io.github.open-spaced-repetition/cljc-fsrs {:git/sha "0e70e96a73cf63c85dcc2df4d022edf12806b239"

+ 7 - 3
deps/db/src/logseq/db/common/view.cljs

@@ -311,6 +311,7 @@
                                           (= (:db/id block) id)
                                           (= id (:db/id (:block/page block)))
                                           (ldb/hidden? (:block/page block))
+                                          (ldb/hidden? block)
                                           (contains? (set (map :db/id (:block/tags block))) (:db/id entity))
                                           (some? (get block (:db/ident entity))))
                                          (or
@@ -331,7 +332,8 @@
                                         distinct))
                                      full-ref-blocks)
                              (remove nil?)
-                             (frequencies))]
+                             (frequencies)
+                             (sort-by second #(> %1 %2)))]
     {:ref-pages-count ref-pages-count
      :ref-blocks ref-blocks}))
 
@@ -564,9 +566,11 @@
                                         [(:block/uuid (first blocks))
                                          (map (fn [b]
                                                 {:db/id (:db/id b)
-                                                 :block/parent (:block/uuid (:block/parent b))}) blocks)])
+                                                 :block/parent (:block/uuid (:block/parent b))})
+                                              (ldb/sort-by-order blocks))])
                                       parent-groups))
-                                   (map :db/id entities))]
+                                   (->> (sort-entities db sorting entities)
+                                        (map :db/id)))]
                        [by-value' group]))
                    result)
                   (map :db/id result))]

+ 4 - 2
deps/db/src/logseq/db/sqlite/build.cljs

@@ -659,8 +659,10 @@
                                (if (:logseq.property/classes m)
                                  (update m :logseq.property/classes
                                          (fn [cs]
-                                           (mapv #(or (some->> (:db/ident %) class-ident->id (hash-map :db/id))
-                                                      (throw (ex-info (str "No :db/id found for :db/ident " (pr-str (:db/ident %))) {})))
+                                           (mapv #(if (db-class/logseq-class? (:db/ident %))
+                                                    %
+                                                    (or (some->> (:db/ident %) class-ident->id (hash-map :db/id))
+                                                        (throw (ex-info (str "No :db/id found for :db/ident " (pr-str %)) {}))))
                                                  cs)))
                                  m))
                              properties-tx)

+ 43 - 12
deps/db/src/logseq/db/sqlite/export.cljs

@@ -9,11 +9,13 @@
             [logseq.db :as ldb]
             [logseq.db.frontend.class :as db-class]
             [logseq.db.frontend.content :as db-content]
+            [logseq.db.frontend.db :as db-db]
             [logseq.db.frontend.entity-plus :as entity-plus]
             [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.sqlite.build :as sqlite-build]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [logseq.db.frontend.property.type :as db-property-type]))
 
 ;; Export fns
 ;; ==========
@@ -40,7 +42,7 @@
 
 (defn- build-pvalue-entity-for-build-page
   [pvalue]
-  (cond (ldb/internal-page? pvalue)
+  (cond (entity-util/internal-page? pvalue)
         ;; Should page properties be pulled here?
         [:build/page (cond-> (shallow-copy-page pvalue)
                        (seq (:block/tags pvalue))
@@ -48,7 +50,10 @@
         (entity-util/journal? pvalue)
         [:build/page {:build/journal (:block/journal-day pvalue)}]))
 
-(defn- build-pvalue-entity-default [ent-properties pvalue options]
+(defn- build-pvalue-entity-default [db ent-properties pvalue
+                                    {:keys [include-uuid-fn]
+                                     :or {include-uuid-fn (constantly false)}
+                                     :as options}]
   (if (or (seq ent-properties) (seq (:block/tags pvalue)))
     (cond-> {:build/property-value :block
              :block/title (or (block-title pvalue)
@@ -57,7 +62,17 @@
       (assoc :build/tags (->build-tags (:block/tags pvalue)))
 
       (seq ent-properties)
-      (assoc :build/properties ent-properties)
+      (assoc :build/properties
+             ;; TODO: Add support for ref properties here and in sqlite.build
+             (->> ent-properties
+                  (keep (fn [[k v]]
+                          (let [prop-type (:logseq.property/type (d/entity db k))]
+                            (when-not (contains? db-property-type/all-ref-property-types prop-type)
+                              [k v]))))
+                  (into {})))
+
+      (include-uuid-fn (:block/uuid pvalue))
+      (assoc :block/uuid (:block/uuid pvalue) :build/keep-uuid? true)
 
       (:include-timestamps? options)
       (merge (select-keys pvalue [:block/created-at :block/updated-at])))
@@ -87,7 +102,7 @@
                                                (medley/filter-keys db-property/internal-property?))
                           ent-properties (when (and (not (:block/closed-value-property pvalue)) (seq ent-properties*))
                                            (buildable-properties db' ent-properties* properties-config' options'))]
-                      (build-pvalue-entity-default ent-properties pvalue options'))))))]
+                      (build-pvalue-entity-default db ent-properties pvalue options'))))))]
     (->> ent-properties
          (map (fn [[k v]]
                 [k
@@ -259,7 +274,8 @@
        (mapcat (fn [val-or-vals]
                  (keep #(when (and (vector? %)
                                    (= :block/uuid (first %))
-                                   (::existing-property-value? (meta %))) (second %))
+                                   (::existing-property-value? (meta %)))
+                          (second %))
                        (if (set? val-or-vals) val-or-vals [val-or-vals]))))
        set))
 
@@ -293,7 +309,7 @@
         (when-let [prop-ids (seq (map :db/ident (filter entity-util/property? ents)))]
           (build-export-properties db prop-ids export-opts))
         classes
-        (when-let [class-ents (seq (filter ldb/class? ents))]
+        (when-let [class-ents (seq (filter entity-util/class? ents))]
           (->> class-ents
                (map #(vector (:db/ident %) (build-export-class % export-opts)))
                (into {})))]
@@ -328,7 +344,7 @@
   (let [class-parent-ents (->> classes-config
                                (filter #(:build/class-parent (val %)))
                                (map #(d/entity db (key %)))
-                               ldb/get-classes-parents)
+                               db-db/get-classes-parents)
         classes
         (->> class-parent-ents
              (remove #(db-class/logseq-class? (:db/ident %)))
@@ -670,8 +686,16 @@
         (update :pages-and-blocks
                 (fn [pages-and-blocks]
                   (mapv (fn [{:keys [page blocks]}]
-                          {:page (remove-uuid-if-not-ref page)
-                           :blocks (sqlite-build/update-each-block blocks remove-uuid-if-not-ref)})
+                          (let [page-map {:page (remove-uuid-if-not-ref page)
+                                          :blocks (sqlite-build/update-each-block blocks remove-uuid-if-not-ref)}
+                                ;; TODO: Walk data structure via :build/properties instead of slower walk
+                                page-map'
+                                (walk/postwalk (fn [f]
+                                                 (if (and (map? f) (:build/property-value f))
+                                                   (remove-uuid-if-not-ref f)
+                                                   f))
+                                               page-map)]
+                            page-map'))
                         pages-and-blocks))))))
 
 (defn- add-ontology-for-include-namespaces
@@ -750,12 +774,19 @@
     undefined))
 
 (defn- find-undefined-uuids [{:keys [classes properties pages-and-blocks]}]
-  (let [known-uuids
+  (let [pvalue-known-uuids (atom #{})
+        _ (walk/postwalk (fn [f]
+                           (if (and (map? f) (:build/property-value f) (:block/uuid f))
+                             (swap! pvalue-known-uuids conj (:block/uuid f))
+                             f))
+                         pages-and-blocks)
+        known-uuids
         (->> (concat (keep :block/uuid (vals classes))
                      (keep :block/uuid (vals properties))
                      (keep #(get-in % [:page :block/uuid]) pages-and-blocks)
                      (mapcat #(sqlite-build/extract-from-blocks (:blocks %) (fn [m] (some-> m :block/uuid vector)))
-                             pages-and-blocks))
+                             pages-and-blocks)
+                     @pvalue-known-uuids)
              set)
         ;; Only looks one-level deep in properties e.g. not inside :build/page
         ;; Doesn't find :block/link refs

+ 13 - 8
deps/outliner/src/logseq/outliner/core.cljs

@@ -221,7 +221,7 @@
 
 (extend-type Entity
   otree/INode
-  (-save [this *txs-state db repo _date-formatter {:keys [retract-attributes? retract-attributes]
+  (-save [this *txs-state db repo _date-formatter {:keys [retract-attributes? retract-attributes outliner-op]
                                                    :or {retract-attributes? true}}]
     (assert (ds/outliner-txs-state? *txs-state)
             "db should be satisfied outliner-tx-state?")
@@ -233,12 +233,16 @@
                    data)
                   db-based?
                   (dissoc :block/properties))
-          m* (-> data'
-                 (dissoc :block/children :block/meta :block/unordered
-                         :block.temp/ast-title :block.temp/ast-body :block/level :block.temp/fully-loaded?)
-                 common-util/remove-nils
-                 block-with-updated-at
-                 (fix-tag-ids db {:db-graph? db-based?}))
+          collapse-or-expand? (= outliner-op :collapse-expand-blocks)
+          m* (cond->
+              (-> data'
+                  (dissoc :block/children :block/meta :block/unordered
+                          :block.temp/ast-title :block.temp/ast-body :block/level :block.temp/fully-loaded?)
+                  common-util/remove-nils
+
+                  (fix-tag-ids db {:db-graph? db-based?}))
+               (not collapse-or-expand?)
+               block-with-updated-at)
           db-id (:db/id this)
           block-uuid (:block/uuid this)
           eid (or db-id (when block-uuid [:block/uuid block-uuid]))
@@ -292,7 +296,8 @@
                                               retract-attributes)))))))
 
         ;; Update block's page attributes
-        (update-page-when-save-block *txs-state block-entity m)
+        (when-not collapse-or-expand?
+          (update-page-when-save-block *txs-state block-entity m))
         ;; Remove orphaned refs from block
         (when (and (:block/title m) (not= (:block/title m) (:block/title block-entity)))
           (remove-orphaned-refs-when-save db *txs-state block-entity m {:db-graph? db-based?})))

+ 1 - 0
deps/shui/src/logseq/shui/ui.cljs

@@ -53,6 +53,7 @@
 (def tooltip-portal (util/lsui-wrap "TooltipPortal"))
 (def tooltip-content (util/lsui-wrap "TooltipContent"))
 (def tooltip-provider (util/lsui-wrap "TooltipProvider"))
+(def tooltip-arrow (util/lsui-wrap "TooltipArrow"))
 
 (def card (util/lsui-wrap "Card"))
 (def card-header (util/lsui-wrap "CardHeader"))

+ 0 - 1
package.json

@@ -166,7 +166,6 @@
         "react-intersection-observer": "^9.3.5",
         "react-resize-context": "3.0.0",
         "react-textarea-autosize": "8.3.3",
-        "react-tippy": "1.4.0",
         "react-transition-group": "4.3.0",
         "react-virtuoso": "4.12.5",
         "remove-accents": "0.4.2",

+ 44 - 21
src/main/frontend/common/missionary.cljs

@@ -21,32 +21,55 @@
         (m/reductions {} init-value)
         (m/latest identity))))
 
-(def delays (reductions * 1000 (repeat 2)))
-
-(def ^:private retry-sentinel (js-obj))
-(defn backoff
-  "Retry task when it throw exception `(get ex-data :missionary/retry)`"
-  [delays-seq task]
-  (m/sp
-    (loop [[delay & rest-delays] (seq delays-seq)]
-      (let [r (try
-                (m/? task)
-                (catch :default e
-                  (if (and (some-> e ex-data :missionary/retry)
-                           (pos-int? delay))
-                    (do (m/? (m/sleep delay))
-                        (println :missionary/retry "after" delay "ms (" (ex-message e) ")")
-                        retry-sentinel)
-                    (throw e))))]
-        (if (identical? r retry-sentinel)
-          (recur rest-delays)
-          r)))))
-
 (defn mix
   "Return a flow which is mixed by `flows`"
   [& flows]
   (m/ap (m/?> (m/?> (count flows) (m/seed flows)))))
 
+(def never-flow (m/ap (m/? m/never)))
+
+(def delays (reductions * 1000 (repeat 2)))
+
+(def ^:private retry-sentinel (js-obj))
+(defn backoff
+  "Retry task when it throw exception `(get ex-data :missionary/retry)`
+  :delay-seq - retry delay-msecs
+  :reset-flow - retry immediately when getting value from flow and reset delays to init state"
+  [{:keys [delay-seq reset-flow]
+    :or {delay-seq (take 4 delays)
+         reset-flow never-flow}}
+   task]
+  (let [reset-flow* (mix reset-flow never-flow)]
+    (m/sp
+      (loop [[delay & rest-delays] (seq delay-seq)]
+        (let [r (try
+                  (m/? task)
+                  (catch :default e
+                    (if (and (some-> e ex-data :missionary/retry)
+                             (pos-int? delay))
+                      (let [delay-or-reset
+                            (m/? (m/race (m/sleep delay :delay)
+                                         (m/reduce (fn [_ r] (when r (reduced :reset))) nil
+                                                   (->> (continue-flow reset-flow*)
+                                                        (m/eduction (drop 1) (take 1))))))
+                            rest-delays*
+                            (case delay-or-reset
+                              :delay
+                              (do (println :missionary/retry "after" delay "ms (" (ex-message e) ")")
+                                  rest-delays)
+                              :reset
+                              (do (println :missionary/retry  "retry now (" (ex-message e) ")")
+                                  delay-seq))]
+                        [retry-sentinel rest-delays*])
+                      (throw e))))]
+          (if (and (vector? r)
+                   (first r) ;; if delete this `(first r)`,
+                       ;; the code continues to the next line even if r=0...
+                       ;; I suspect it's a bug in missionary.
+                   (identical? retry-sentinel (first r)))
+            (recur (second r))
+            r))))))
+
 (defn clock
   "Return a flow that emits `value` every `interval-ms`."
   ([interval-ms]

+ 6 - 0
src/main/frontend/components/block.css

@@ -1082,3 +1082,9 @@ html.is-mac {
   aspect-ratio: 16 / 9;
   height: auto;
 }
+
+.ls-filters {
+    div[data-testid='virtuoso-item-list'] button {
+        @apply mb-2;
+    }
+}

+ 14 - 13
src/main/frontend/components/container.cljs

@@ -782,19 +782,20 @@
 (rum/defc new-block-mode < rum/reactive
   []
   (when (state/sub [:document/mode?])
-    (ui/tippy {:html [:div.p-2
-                      [:p.mb-2 [:b "Document mode"]]
-                      [:ul
-                       [:li
-                        [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line))]
-                        [:p.inline-block "to create new block"]]
-                       [:li
-                        [:p.inline-block.mr-1 "Click `D` or type"]
-                        [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode))]
-                        [:p.inline-block "to toggle document mode"]]]]}
-              [:a.block.px-1.text-sm.font-medium.bg-base-2.rounded-md.mx-2
-               {:on-click state/toggle-document-mode!}
-               "D"])))
+    (ui/tooltip
+      [:a.block.px-1.text-sm.font-medium.bg-base-2.rounded-md.mx-2
+       {:on-click state/toggle-document-mode!}
+       "D"]
+      [:div.p-2
+       [:p.mb-2 [:b "Document mode"]]
+       [:ul
+        [:li
+         [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line))]
+         [:p.inline-block "to create new block"]]
+        [:li
+         [:p.inline-block.mr-1 "Click `D` or type"]
+         [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode))]
+         [:p.inline-block "to toggle document mode"]]]])))
 
 (def help-menu-items
   [{:title "Handbook" :icon "book-2" :on-click #(handbooks/toggle-handbooks)}

+ 1 - 7
src/main/frontend/components/editor.cljs

@@ -81,13 +81,7 @@
             [:div.has-help
              {:title plugin-id}
              command-name
-             (when doc (ui/tippy
-                        {:html doc
-                         :interactive true
-                         :fixed-position? true
-                         :position "right"}
-
-                        [:small (svg/help-circle)]))]
+             (when doc (ui/tooltip [:small (svg/help-circle)] doc))]
 
             (string? doc)
             [:div {:title doc}

+ 17 - 20
src/main/frontend/components/file_based/block.cljs

@@ -81,10 +81,9 @@
 (defn priority-cp
   [{:block/keys [pre-block? priority] :as block}]
   (when (and (not pre-block?) priority)
-    (ui/tippy
-     {:interactive true
-      :html (set-priority block priority)}
-     (priority-text priority))))
+    (ui/tooltip
+      (priority-text priority)
+      (set-priority block priority))))
 
 (defn clock-summary-cp
   [block body]
@@ -96,22 +95,20 @@
                  (not= summary "0m")
                  (not (string/blank? summary)))
         [:div {:style {:max-width 100}}
-         (ui/tippy {:html        (fn []
-                                   (when-let [logbook (drawer/get-logbook body)]
-                                     (let [clocks (->> (last logbook)
-                                                       (filter #(string/starts-with? % "CLOCK:"))
-                                                       (remove string/blank?))]
-                                       [:div.p-4
-                                        [:div.font-bold.mb-2 "LOGBOOK:"]
-                                        [:ul
-                                         (for [clock (take 10 (reverse clocks))]
-                                           [:li clock])]])))
-                    :interactive true
-                    :in-editor?  true
-                    :delay       [1000, 100]}
-                   [:div.text-sm.time-spent.ml-1 {:style {:padding-top 3}}
-                    [:a.fade-link
-                     summary]])]))))
+         (ui/tooltip
+           [:div.text-sm.time-spent.ml-1 {:style {:padding-top 3}}
+            [:a.fade-link
+             summary]]
+
+           (when-let [logbook (drawer/get-logbook body)]
+             (let [clocks (->> (last logbook)
+                            (filter #(string/starts-with? % "CLOCK:"))
+                            (remove string/blank?))]
+               [:div.p-4
+                [:div.font-bold.mb-2 "LOGBOOK:"]
+                [:ul
+                 (for [clock (take 10 (reverse clocks))]
+                   [:li clock])]])))]))))
 
 (rum/defc timestamp-editor
   [ast *show-datapicker?]

+ 11 - 16
src/main/frontend/components/file_based/query.cljs

@@ -10,22 +10,17 @@
 
 (rum/defc query-refresh-button
   [query-time {:keys [on-pointer-down full-text-search?]}]
-  (ui/tippy
-   {:html  [:div
-            [:p
-             (if full-text-search?
-               [:span "Full-text search results will not be refreshed automatically."]
-               [:span (str "This query takes " (int query-time) "ms to finish, it's a bit slow so that auto refresh is disabled.")])]
-            [:p
-             "Click the refresh button instead if you want to see the latest result."]]
-    :interactive     true
-    :popperOptions   {:modifiers {:preventOverflow
-                                  {:enabled           true
-                                   :boundariesElement "viewport"}}}
-    :arrow true}
-   [:a.fade-link.flex
-    {:on-pointer-down on-pointer-down}
-    (ui/icon "refresh" {:style {:font-size 20}})]))
+  (ui/tooltip
+    [:a.fade-link.flex
+     {:on-pointer-down on-pointer-down}
+     (ui/icon "refresh" {:style {:font-size 20}})]
+    [:div
+     [:p
+      (if full-text-search?
+        [:span "Full-text search results will not be refreshed automatically."]
+        [:span (str "This query takes " (int query-time) "ms to finish, it's a bit slow so that auto refresh is disabled.")])]
+     [:p
+      "Click the refresh button instead if you want to see the latest result."]]))
 
 ;; Custom query header only used by file graphs
 (rum/defc custom-query-header

+ 53 - 42
src/main/frontend/components/page.cljs

@@ -908,30 +908,35 @@
                                (reset! *excluded-pages? value)
                                (set-setting! :excluded-pages? value)))
                            true)]]
+
               (when (config/db-based-graph? (state/get-current-repo))
                 [:div.flex.flex-col.mb-2
                  [:p "Created before"]
                  (when created-at-filter
                    [:div (.toDateString (js/Date. (+ created-at-filter (get-in graph [:all-pages :created-at-min]))))])
-                 (ui/tippy {:html [:div.pr-3 (str (js/Date. (+ created-at-filter (get-in graph [:all-pages :created-at-min]))))]}
-                     ;; Slider keeps track off the range from min created-at to max created-at
-                     ;; because there were bugs with setting min and max directly
-                           (ui/slider created-at-filter
-                                      {:min 0
-                                       :max (- (get-in graph [:all-pages :created-at-max])
-                                               (get-in graph [:all-pages :created-at-min]))
-                                       :on-change #(do
-                                                     (reset! *created-at-filter (int %))
-                                                     (set-setting! :created-at-filter (int %)))}))])
+
+                 (ui/tooltip
+                   ;; Slider keeps track off the range from min created-at to max created-at
+                   ;; because there were bugs with setting min and max directly
+                   (ui/slider created-at-filter
+                     {:min 0
+                      :max (- (get-in graph [:all-pages :created-at-max])
+                             (get-in graph [:all-pages :created-at-min]))
+                      :on-change #(do
+                                    (reset! *created-at-filter (int %))
+                                    (set-setting! :created-at-filter (int %)))})
+                   [:div.px-1 (str (js/Date. (+ created-at-filter (get-in graph [:all-pages :created-at-min]))))])])
+
               (when (seq focus-nodes)
                 [:div.flex.flex-col.mb-2
                  [:p {:title "N hops from selected nodes"}
                   "N hops from selected nodes"]
-                 (ui/tippy {:html [:div.pr-3 n-hops]}
-                           (ui/slider (or n-hops 10)
-                                      {:min 1
-                                       :max 10
-                                       :on-change #(reset! *n-hops (int %))}))])
+                 (ui/tooltip
+                   (ui/slider (or n-hops 10)
+                     {:min 1
+                      :max 10
+                      :on-change #(reset! *n-hops (int %))})
+                   [:div n-hops])])
 
               [:a.opacity-70.opacity-100 {:on-click (fn []
                                                       (swap! *graph-reset? not)
@@ -977,39 +982,45 @@
               [:div.flex.flex-col.mb-2
                [:p {:title "Link Distance"}
                 "Link Distance"]
-               (ui/tippy {:html [:div.pr-3 link-dist]}
-                         (ui/slider (/ link-dist 10)
-                                    {:min 1   ;; 10
-                                     :max 18  ;; 180
-                                     :on-change #(let [value (int %)]
-                                                   (reset! *link-dist (* value 10))
-                                                   (set-forcesetting! :link-dist (* value 10)))}))]
+               (ui/tooltip
+                 (ui/slider (/ link-dist 10)
+                   {:min 1                                  ;; 10
+                    :max 18                                 ;; 180
+                    :on-change #(let [value (int %)]
+                                  (reset! *link-dist (* value 10))
+                                  (set-forcesetting! :link-dist (* value 10)))})
+                 [:div link-dist])]
+
               [:div.flex.flex-col.mb-2
                [:p {:title "Charge Strength"}
                 "Charge Strength"]
-               (ui/tippy {:html [:div.pr-3 charge-strength]}
-                         (ui/slider (/ charge-strength 100)
-                                    {:min -10  ;;-1000
-                                     :max 10   ;;1000
-                                     :on-change #(let [value (int %)]
-                                                   (reset! *charge-strength (* value 100))
-                                                   (set-forcesetting! :charge-strength (* value 100)))}))]
+               (ui/tooltip
+                 (ui/slider (/ charge-strength 100)
+                   {:min -10                                ;;-1000
+                    :max 10                                 ;;1000
+                    :on-change #(let [value (int %)]
+                                  (reset! *charge-strength (* value 100))
+                                  (set-forcesetting! :charge-strength (* value 100)))})
+                 [:div charge-strength])]
+
               [:div.flex.flex-col.mb-2
                [:p {:title "Charge Range"}
                 "Charge Range"]
-               (ui/tippy {:html [:div.pr-3 charge-range]}
-                         (ui/slider (/ charge-range 100)
-                                    {:min 5    ;;500
-                                     :max 40   ;;4000
-                                     :on-change #(let [value (int %)]
-                                                   (reset! *charge-range (* value 100))
-                                                   (set-forcesetting! :charge-range (* value 100)))}))]
-
-              [:a.opacity-70.opacity-100 {:on-click (fn []
-                                                      (swap! *graph-forcereset? not)
-                                                      (reset! *link-dist 70)
-                                                      (reset! *charge-strength -600)
-                                                      (reset! *charge-range 600))}
+               (ui/tooltip
+                 (ui/slider (/ charge-range 100)
+                   {:min 5                                  ;;500
+                    :max 40                                 ;;4000
+                    :on-change #(let [value (int %)]
+                                  (reset! *charge-range (* value 100))
+                                  (set-forcesetting! :charge-range (* value 100)))})
+                 [:div charge-range])]
+
+              [:a
+               {:on-click (fn []
+                            (swap! *graph-forcereset? not)
+                            (reset! *link-dist 70)
+                            (reset! *charge-strength -600)
+                            (reset! *charge-range 600))}
                "Reset Forces"]]]))
          {})
         (graph-filter-section

+ 9 - 11
src/main/frontend/components/plugins.cljs

@@ -585,14 +585,14 @@
 
       (when (and develop-mode? (util/electron?) (not market?))
         [:div
-         (ui/tippy {:html  [:div (t :plugin/unpacked-tips)]
-                    :arrow true}
-                   (ui/button
-                    (t :plugin/load-unpacked)
-                    {:icon "upload"
-                     :intent "link"
-                     :class "load-unpacked"
-                     :on-click plugin-handler/load-unpacked-plugin}))
+         (ui/tooltip
+           (ui/button
+             (t :plugin/load-unpacked)
+             {:icon "upload"
+              :intent "link"
+              :class "load-unpacked"
+              :on-click plugin-handler/load-unpacked-plugin})
+           [:div (t :plugin/unpacked-tips)])
 
          (when (util/electron?)
            (unpacked-plugin-loader selected-unpacked-pkg))])]
@@ -1075,9 +1075,7 @@
 
            [:div.px-4
             (when-not (string/blank? notes)
-              (ui/tippy
-               {:html [:p notes]}
-               [:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")]))]])]
+              (ui/tooltip [:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")] [:p notes]))]])]
 
        ;; all done
        [:div.py-4 [:strong.text-4xl (str "\uD83C\uDF89 " (t :plugin/all-updated))]])

+ 2 - 5
src/main/frontend/components/property.cljs

@@ -145,11 +145,8 @@
                                             (when (= "Enter" (.-key e))
                                               (util/stop-propagation e)))} label)))))
      (when show-type-change-hints?
-       (ui/tippy {:html        "Changing the property type clears some property configurations."
-                  :class       "tippy-hover ml-2"
-                  :interactive true
-                  :disabled    false}
-                 (svg/info)))]))
+       (ui/tooltip (svg/info)
+         [:span "Changing the property type clears some property configurations."]))]))
 
 (rum/defc property-select
   [exclude-properties select-opts]

+ 5 - 7
src/main/frontend/components/property/config.cljs

@@ -601,19 +601,17 @@
        (dropdown-editor-menuitem {:icon :letter-t
                                   :title "Property type"
                                   :desc (if disabled?'
-                                          (ui/tippy {:html        [:div.w-96
-                                                                   "The type of this property is locked once you start using it. This is to make sure all your existing information stays correct if the property type is changed later. To unlock, all uses of a property must be deleted."]
-                                                     :class       "tippy-hover ml-2"
-                                                     :interactive true
-                                                     :disabled    false}
-                                                    (str property-type-label'))
+                                          (ui/tooltip
+                                            [:span (str property-type-label')]
+                                            [:div.w-96
+                                             "The type of this property is locked once you start using it. This is to make sure all your existing information stays correct if the property type is changed later. To unlock, all uses of a property must be deleted."])
                                           (str property-type-label'))
                                   :disabled? disabled?'
                                   :submenu-content (fn [ops]
                                                      (property-type-sub-pane property ops))}))
 
      (when (and (= property-type :node)
-                (not (contains? #{:logseq.property/parent} (:db/ident property))))
+             (not (contains? #{:logseq.property/parent} (:db/ident property))))
        (dropdown-editor-menuitem {:icon :hash
                                   :disabled? disabled?
                                   :title "Specify node tags"

+ 72 - 44
src/main/frontend/components/reference_filters.cljs

@@ -5,12 +5,14 @@
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
+            [frontend.db-mixins :as db-mixins]
             [frontend.handler.page :as page-handler]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [logseq.db.common.view :as db-view]
+            [logseq.shui.hooks :as hooks]
             [promesa.core :as p]
             [rum.core :as rum]))
 
@@ -18,50 +20,71 @@
   [references]
   (sort-by second #(> %1 %2) references))
 
+(rum/defc ref-button
+  [page filters ref-name ref-count]
+  (let [lc-reference (string/lower-case ref-name)]
+    (ui/button
+     [:span
+      ref-name
+      (when ref-count [:sup " " ref-count])]
+     :on-click (fn [e]
+                 (let [db-based? (config/db-based-graph? (state/get-current-repo))
+                       includes (set (map :block/name (:included filters)))
+                       excludes (set (map :block/name (:excluded filters)))
+                       included? (includes lc-reference)
+                       not-in-filters? (and (not included?) (not (excludes lc-reference)))
+                       shift? (.-shiftKey e)]
+                   (if db-based?
+                     (page-handler/db-based-save-filter! page (:db/id (db/get-page lc-reference))
+                                                         {:add? not-in-filters?
+                                                          :include? (if not-in-filters? (not shift?) included?)})
+                     (let [filters-m (->> (concat (map #(vector % true) includes) (map #(vector % false) excludes))
+                                          (into {}))
+                           filters' (if not-in-filters?
+                                      (assoc filters-m lc-reference (not shift?))
+                                      (dissoc filters-m lc-reference))]
+                       (page-handler/file-based-save-filter! page filters')))))
+     :small? true
+     :variant :outline)))
+
 (defn filtered-refs
-  [page filters filtered-references*]
-  [:div.flex.gap-2.flex-wrap.items-center
-   (let [filtered-references (if (de/entity? (first filtered-references*))
-                               (map (fn [e] [(:block/title e)]) filtered-references*)
-                               filtered-references*)] <
-        (for [[ref-name ref-count] filtered-references]
-          (when ref-name
-            (let [lc-reference (string/lower-case ref-name)]
-              (ui/button
-               [:span
-                ref-name
-                (when ref-count [:sup " " ref-count])]
-               :on-click (fn [e]
-                           (let [db-based? (config/db-based-graph? (state/get-current-repo))
-                                 includes (set (map :block/name (:included filters)))
-                                 excludes (set (map :block/name (:excluded filters)))
-                                 included? (includes lc-reference)
-                                 not-in-filters? (and (not included?) (not (excludes lc-reference)))
-                                 shift? (.-shiftKey e)]
-                             (if db-based?
-                               (page-handler/db-based-save-filter! page (:db/id (db/get-page lc-reference))
-                                                                   {:add? not-in-filters?
-                                                                    :include? (if not-in-filters? (not shift?) included?)})
-                               (let [filters-m (->> (concat (map #(vector % true) includes) (map #(vector % false) excludes))
-                                                    (into {}))
-                                     filters' (if not-in-filters?
-                                                (assoc filters-m lc-reference (not shift?))
-                                                (dissoc filters-m lc-reference))]
-                                 (page-handler/file-based-save-filter! page filters')))))
-               :small? true
-               :variant :outline
-               :key ref-name)))))])
+  [page filters filtered-references* virtual?]
+  (let [filtered-references (if (de/entity? (first filtered-references*))
+                              (map (fn [e] [(:block/title e)]) filtered-references*)
+                              filtered-references*)]
+    (if (and (> (count filtered-references) 100)
+             (not (false? virtual?)))
+      (ui/virtualized-list
+       {:style {:height 500
+                :width 500
+                :max-width 500}
+        :total-count (count filtered-references)
+        :compute-item-key (fn [idx]
+                            (str "ref-button-" idx))
+        :item-content (fn [idx]
+                        (let [[ref-name ref-count] (util/nth-safe filtered-references idx)]
+                          (ref-button page filters ref-name ref-count)))})
+      [:div.flex.gap-2.flex-wrap.items-center
+       {:style {:width 500
+                :max-width 500}}
+       (for [[ref-name ref-count] filtered-references]
+         (rum/with-key (ref-button page filters ref-name ref-count)
+           (str "ref-" ref-name)))])))
 
-(rum/defcs filter-dialog < (rum/local "" ::filterSearch) rum/reactive
-  [state page references]
-  (let [page-entity (db/sub-block (:db/id page))
-        filter-search (get state ::filterSearch)
+(rum/defc filter-dialog-aux
+  [page-entity references]
+  (let [[filter-search set-filter-search!] (hooks/use-state "")
+        [filtered-references set-filtered-references!] (hooks/use-state references)
         filters (db-view/get-filters (db/get-db) page-entity)
-        filtered-references  (frequencies-sort
-                              (if (= @filter-search "")
-                                references
-                                (search/fuzzy-search references @filter-search :limit 500 :extract-fn first)))
         {:keys [included excluded]} filters]
+    (hooks/use-effect!
+     (fn []
+       (let [references (if (= filter-search "")
+                          references
+                          (->> (search/fuzzy-search references filter-search :limit 100 :extract-fn first)
+                               frequencies-sort))]
+         (set-filtered-references! references)))
+     [(hooks/use-debounced-value filter-search 200)])
     [:div.ls-filters.filters
      [:div.sm:flex.sm:items-start
       [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-gray-200.text-gray-500.sm:mx-0.sm:h-10.sm:w-10
@@ -75,12 +98,12 @@
         (when (seq included)
           [:div.flex.flex-row.flex-wrap.center-items
            [:div.mr-1.font-medium.py-1 (t :linked-references/filter-includes)]
-           (filtered-refs page-entity filters included)])
+           (filtered-refs page-entity filters included false)])
         (when (seq excluded)
           [:div.flex.flex-row.flex-wrap
            [:div.mr-1.font-medium.py-1 (t :linked-references/filter-excludes)]
 
-           (filtered-refs page-entity filters excluded)])])
+           (filtered-refs page-entity filters excluded false)])])
      [:div.cp__filters-input-panel.flex.focus-within:bg-gray-03
       (ui/icon "search")
       [:input.cp__filters-input.w-full.bg-transparent
@@ -89,7 +112,7 @@
         :ref (fn [^js el] (when el
                             (-> (p/delay 32) (p/then #(.focus el)))))
         :on-change (fn [e]
-                     (reset! filter-search (util/evalue e)))}]]
+                     (set-filter-search! (util/evalue e)))}]]
      (let [all-filters (set
                         (concat (map :block/name included)
                                 (map :block/name excluded)))
@@ -97,4 +120,9 @@
                         filtered-references)]
        (when (seq refs)
          [:div.mt-4
-          (filtered-refs page-entity filters refs)]))]))
+          (filtered-refs page-entity filters refs true)]))]))
+
+(rum/defc filter-dialog < rum/reactive db-mixins/query
+  [page references]
+  (let [page-entity (db/sub-block (:db/id page))]
+    (filter-dialog-aux page-entity references)))

+ 26 - 41
src/main/frontend/components/settings.cljs

@@ -381,13 +381,7 @@
                           :let [active? (= color color-accent)
                                 none? (= color :none)]]
                       [:div.flex.items-center
-                       (ui/tippy
-                        {:html (case color
-                                 :none [:p {:style {:max-width "300px"}}
-                                        "Cancel accent color. This is currently in beta stage and mainly used for compatibility with custom themes."]
-                                 :logseq "Logseq classical color"
-                                 (str (name color) " color"))
-                         :delay [1000, 100]}
+                       (ui/tooltip
                         (shui/button
                          {:class "w-5 h-5 px-1 rounded-full flex justify-center items-center transition ease-in duration-100 hover:cursor-pointer hover:opacity-100"
                           :auto-focus (and _in-modal? active?)
@@ -402,7 +396,13 @@
                           {:class (if none? "h-0.5 w-full bg-red-700"
                                       "w-2 h-2 rounded-full transition ease-in duration-100")
                            :style {:background-color (if-not none? (str "var(--rx-" (name color) "-07)") "")
-                                   :opacity (if (or none? active?) 1 0)}}]))])]]
+                                   :opacity (if (or none? active?) 1 0)}}])
+
+                         (case color
+                           :none [:p {:style {:max-width "300px"}}
+                                  "Cancel accent color. This is currently in beta stage and mainly used for compatibility with custom themes."]
+                           :logseq "Logseq classical color"
+                           (str (name color) " color")))])]]
 
     [:div
      (row-with-button-action
@@ -431,11 +431,8 @@
     {:for "custom_date_format"}
     (t :settings-page/custom-date-format)
     (when-not (config/db-based-graph? (state/get-current-repo))
-      (ui/tippy {:html        (t :settings-page/custom-date-format-warning)
-                 :class       "tippy-hover ml-2"
-                 :interactive true
-                 :disabled    false}
-                (svg/info)))]
+      (ui/tooltip [:span.flex.px-2 (svg/info)]
+        [:span (t :settings-page/custom-date-format-warning)]))]
    [:div.mt-1.sm:mt-0.sm:col-span-2
     [:div.max-w-lg.rounded-md
      [:select.form-select.is-small
@@ -484,11 +481,8 @@
 (defn outdenting-row [t logical-outdenting?]
   (toggle "preferred_outdenting"
           [(t :settings-page/preferred-outdenting)
-           (ui/tippy {:html        (outdenting-hint)
-                      :class       "tippy-hover ml-2"
-                      :interactive true
-                      :disabled    false}
-                     (svg/info))]
+           (ui/tooltip [:span.flex.px-2 (svg/info)]
+             (outdenting-hint) {:content-props {:side "right"}})]
           logical-outdenting?
           config-handler/toggle-logical-outdenting!))
 
@@ -501,22 +495,16 @@
 (defn preferred-pasting-file [t preferred-pasting-file?]
   (toggle "preferred_pasting_file"
           [(t :settings-page/preferred-pasting-file)
-           (ui/tippy {:html        (t :settings-page/preferred-pasting-file-hint)
-                      :class       "tippy-hover ml-2"
-                      :interactive true
-                      :disabled    false}
-                     (svg/info))]
+           (ui/tooltip [:span.flex.px-2 (svg/info)]
+             [:span.block.w-64 (t :settings-page/preferred-pasting-file-hint)])]
           preferred-pasting-file?
           config-handler/toggle-preferred-pasting-file!))
 
 (defn auto-expand-row [t auto-expand-block-refs?]
   (toggle "auto_expand_block_refs"
           [(t :settings-page/auto-expand-block-refs)
-           (ui/tippy {:html        (auto-expand-hint)
-                      :class       "tippy-hover ml-2"
-                      :interactive true
-                      :disabled    false}
-                     (svg/info))]
+           (ui/tooltip [:span.flex.px-2 (svg/info)]
+             (auto-expand-hint))]
           auto-expand-block-refs?
           config-handler/toggle-auto-expand-block-refs!))
 
@@ -866,26 +854,23 @@
   (row-with-button-action
    {:left-label (str (t :settings-page/sync-diff-merge) " (Experimental!)") ;; Not included in i18n to avoid outdating translations
     :action (sync-diff-merge-enabled-switcher enabled?)
-    :desc (ui/tippy {:html        [:div
-                                   [:div (t :settings-page/sync-diff-merge-desc)]
-                                   [:div (t :settings-page/sync-diff-merge-warn)]]
-                     :class       "tippy-hover ml-2"
-                     :interactive true
-                     :disabled    false}
-                    (svg/info))}))
+    :desc (ui/tooltip [:span.inline-flex.px-1 (svg/info)]
+            [:div
+             [:div (t :settings-page/sync-diff-merge-desc)]
+             [:div (t :settings-page/sync-diff-merge-warn)]])}))
 
 (rum/defc rtc-enabled-switcher
   [enabled?]
   (ui/toggle enabled?
-             (fn []
-               (let [value (not enabled?)]
-                 (state/set-rtc-enabled! value)))
-             true))
+    (fn []
+      (let [value (not enabled?)]
+        (state/set-rtc-enabled! value)))
+    true))
 
 (defn rtc-switcher-row [enabled?]
   (row-with-button-action
-   {:left-label "RTC"
-    :action (rtc-enabled-switcher enabled?)}))
+    {:left-label "RTC"
+     :action (rtc-enabled-switcher enabled?)}))
 
 (rum/defc whiteboards-enabled-switcher
   [enabled?]

+ 52 - 51
src/main/frontend/components/views.cljs

@@ -215,7 +215,11 @@
                           {:align :start})
                          (editor-handler/edit-block! block :max {:container-id :unknown-container}))))))}
      (if block
-       (inline-title (:block/title block))
+       [:div (inline-title
+              (some->> (:block/title block)
+                       string/trim
+                       string/split-lines
+                       first))]
        [:div])]))
 
 (defn build-columns
@@ -1220,7 +1224,8 @@
         db-id (cond (map? item) (:db/id item)
                     (number? item) item
                     :else nil)
-        [item set-item!] (hooks/use-state nil)
+        block (some-> db-id db/entity)
+        [item set-item!] (hooks/use-state (when (:block.temp/fully-loaded? block) block))
         opts (if list-view?
                {:skip-refresh? true
                 :children? false}
@@ -1251,7 +1256,7 @@
                                 (gdom/getElement "main-content-container"))
         :compute-item-key (fn [idx]
                             (let [block-id (util/nth-safe rows idx)]
-                              (str "table-row-" (:group-idx option) "-" block-id)))
+                              (str "table-row-" block-id)))
         :skipAnimationFrameInResizeObserver true
         :total-count (count rows)
         :context {:scrolling scrolling?}
@@ -1298,8 +1303,8 @@
                       :compute-item-key (fn [idx]
                                           (let [block-id (util/nth-safe rows idx)]
                                             (str "list-row-" block-id)))
-                      ;; :skipAnimationFrameInResizeObserver true
                       :total-count (count rows)
+                      :skipAnimationFrameInResizeObserver true
                       :item-content (fn [idx] (lazy-item-render rows idx))})))
         breadcrumb (state/get-component :block/breadcrumb)
         all-numbers? (every? number? rows)]
@@ -1339,6 +1344,9 @@
         {:ref #(reset! *scroller-ref %)
          :total-count (count blocks)
          :custom-scroll-parent (gdom/getElement "main-content-container")
+         :skipAnimationFrameInResizeObserver true
+         :compute-item-key (fn [idx]
+                             (str (:db/id view-entity) "-card-" idx))
          :item-content (fn [idx]
                          (lazy-item (:data table) idx {}
                                     (fn [block]
@@ -1692,52 +1700,45 @@
                         :add-new-object! add-new-object!}]
          (if (and group-by-property-ident (not (number? (first (:rows table)))))
            (when (seq (:rows table))
-             [:div.flex.flex-col.border-t.pt-2
-              (ui/virtualized-list
-               {:class (when list-view? "group-list-view")
-                :custom-scroll-parent (gdom/getElement "main-content-container")
-                :increase-viewport-by {:top 300 :bottom 300}
-                :compute-item-key (fn [idx]
-                                    (str "table-group" idx))
-                :skipAnimationFrameInResizeObserver true
-                :total-count (count (:rows table))
-                :item-content (fn [idx]
-                                (let [[value group] (nth (:rows table) idx)
-                                      add-new-object! (when (fn? add-new-object!)
-                                                        (fn [_]
-                                                          (add-new-object! view-entity table
-                                                                           {:properties {(:db/ident group-by-property) (or (and (map? value) (:db/id value)) value)}})))
-                                      table' (shui/table-option (-> table-map
-                                                                    (assoc-in [:data-fns :add-new-object!] add-new-object!)
-                                                                    (assoc :data group ; data for this group
-                                                                           )))
-                                      readable-property-value #(if (and (map? %) (or (:block/title %) (:logseq.property/value %)))
-                                                                 (db-property/property-value-content %)
-                                                                 (str %))
-                                      group-by-page? (or (= :block/page group-by-property-ident)
-                                                         (and (not db-based?) (contains? #{:linked-references :unlinked-references} display-type)))]
-                                  (rum/with-key
-                                    (ui/foldable
-                                     [:div
-                                      (cond
-                                        group-by-page?
-                                        (if value
-                                          (let [c (state/get-component :block/page-cp)]
-                                            (c {:disable-preview? true} value))
-                                          [:div.text-muted-foreground.text-sm
-                                           "Pages"])
-
-                                        (some? value)
-                                        (let [icon (pu/get-block-property-value value :logseq.property/icon)]
-                                          [:div.flex.flex-row.gap-1.items-center
-                                           (when icon (icon-component/icon icon {:color? true}))
-                                           (readable-property-value value)])
-                                        :else
-                                        (str "No " (:block/title group-by-property)))]
-                                     (let [render (view-cp view-entity (assoc table' :rows group :group-idx idx) option view-opts)]
-                                       (if list-view? [:div.-ml-2 render] render))
-                                     {:title-trigger? false})
-                                    (str "group-" idx))))})])
+             [:div.flex.flex-col.border-t.pt-2.gap-2
+              (map-indexed
+               (fn [idx [value group]]
+                 (let [add-new-object! (when (fn? add-new-object!)
+                                         (fn [_]
+                                           (add-new-object! view-entity table
+                                                            {:properties {(:db/ident group-by-property) (or (and (map? value) (:db/id value)) value)}})))
+                       table' (shui/table-option (-> table-map
+                                                     (assoc-in [:data-fns :add-new-object!] add-new-object!)
+                                                     (assoc :data group ; data for this group
+                                                            )))
+                       readable-property-value #(if (and (map? %) (or (:block/title %) (:logseq.property/value %)))
+                                                  (db-property/property-value-content %)
+                                                  (str %))
+                       group-by-page? (or (= :block/page group-by-property-ident)
+                                          (and (not db-based?) (contains? #{:linked-references :unlinked-references} display-type)))]
+                   (rum/with-key
+                     (ui/foldable
+                      [:div
+                       (cond
+                         group-by-page?
+                         (if value
+                           (let [c (state/get-component :block/page-cp)]
+                             (c {:disable-preview? true} value))
+                           [:div.text-muted-foreground.text-sm
+                            "Pages"])
+
+                         (some? value)
+                         (let [icon (pu/get-block-property-value value :logseq.property/icon)]
+                           [:div.flex.flex-row.gap-1.items-center
+                            (when icon (icon-component/icon icon {:color? true}))
+                            (readable-property-value value)])
+                         :else
+                         (str "No " (:block/title group-by-property)))]
+                      (let [render (view-cp view-entity (assoc table' :rows group) option view-opts)]
+                        (if list-view? [:div.-ml-2 render] render))
+                      {:title-trigger? false})
+                     (str (:db/id view-entity) "-group-idx-" idx))))
+               (:rows table))])
            (view-cp view-entity table option view-opts)))]
       (merge {:title-trigger? false} foldable-options))]))
 
@@ -1812,7 +1813,7 @@
        [(:db/id view-entity)
         (hooks/use-debounced-value input 300)
         sorting-filters
-        (:logseq.property.view/group-by-property view-entity)
+        (:db/id (:logseq.property.view/group-by-property view-entity))
         ;; page filters
         (:logseq.property.linked-references/includes view-parent)
         (:logseq.property.linked-references/excludes view-parent)

+ 32 - 38
src/main/frontend/extensions/srs.cljs

@@ -488,16 +488,17 @@
                                    :on-click   #(score-and-next-card 5 card card-index finished? phase review-records cb)})])
 
             (when preview?
-              (ui/tippy {:html [:div.text-sm
-                                (t :flashcards/modal-btn-reset-tip)]
-                         :class "tippy-hover"
-                         :interactive true}
-                        (ui/button [:span (t :flashcards/modal-btn-reset)]
-                                   :id "card-reset"
-                                   :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
-                                   :on-click (fn [e]
-                                               (util/stop e)
-                                               (operation-reset! card)))))]
+              (ui/tooltip
+                (ui/button [:span (t :flashcards/modal-btn-reset)]
+                  :id "card-reset"
+                  :class (util/hiccup->class "opacity-60.hover:opacity-100.card-reset")
+                  :on-click (fn [e]
+                              (util/stop e)
+                              (operation-reset! card)))
+
+                [:div.text-sm
+                 (t :flashcards/modal-btn-reset-tip)]
+                {:trigger-props {:as-child false}}))]
            [:div.my-3 (ui/button "Review cards" :small? true)])]))))
 
 (rum/defc view-modal <
@@ -629,27 +630,20 @@
 
            ;; FIXME: CSS issue
            (if @*preview-mode?
-             (ui/tippy {:html [:div.text-sm (t :flashcards/modal-current-total)]
-                        :interactive true}
-                       [:div.opacity-60.text-sm.mr-2
-                        @*card-index
-                        [:span "/"]
-                        total])
-             (ui/tippy {:html [:div.text-sm (t :flashcards/modal-overdue-total)]
-                        ;; :class "tippy-hover"
-                        :interactive true}
-                       [:div.opacity-60.text-sm.mr-2
-                        (max 0 (- filtered-total @*card-index))
-                        [:span "/"]
-                        total]))
-
-           (ui/tippy
-            {:html [:div.text-sm (t :flashcards/modal-toggle-preview-mode)]
-             :delay [1000, 100]
-             :class "tippy-hover"
-             :interactive true
-             :disabled false}
-
+             (ui/tooltip
+               [:div.opacity-60.text-sm.mr-2
+                @*card-index
+                [:span "/"]
+                total]
+               [:div.text-sm (t :flashcards/modal-current-total)])
+             (ui/tooltip
+               [:div.opacity-60.text-sm.mr-2
+                (max 0 (- filtered-total @*card-index))
+                [:span "/"]
+                total]
+               [:div.text-sm (t :flashcards/modal-overdue-total)]))
+
+           (ui/tooltip
             (ui/button
              (merge
               {:icon "letter-a"
@@ -661,13 +655,11 @@
                :button-props {:id "preview-all-cards"}
                :small? true}
               (when @*preview-mode?
-                {:icon-props {:style {:color "var(--ls-button-background)"}}}))))
+                {:icon-props {:style {:color "var(--ls-button-background)"}}})))
+             [:div.text-sm (t :flashcards/modal-toggle-preview-mode)]
+             {:trigger-props {:as-child false}})
 
-           (ui/tippy
-            {:html [:div.text-sm (t :flashcards/modal-toggle-random-mode)]
-             :delay [1000, 100]
-             :class "tippy-hover"
-             :interactive true}
+           (ui/tooltip
             (ui/button
              (merge
               {:icon "arrows-shuffle"
@@ -677,7 +669,9 @@
                            (swap! *random-mode? not))
                :small? true}
               (when @*random-mode?
-                {:icon-props {:style {:color "var(--ls-button-background)"}}}))))]]
+                {:icon-props {:style {:color "var(--ls-button-background)"}}})))
+             [:div.text-sm (t :flashcards/modal-toggle-random-mode)]
+             {:trigger-props {:as-child false}})]]
          [:div.px-1
           (when (and (not modal?) (not @*preview-mode?))
             {:on-click (fn []

+ 5 - 2
src/main/frontend/handler.cljs

@@ -17,6 +17,7 @@
             [frontend.error :as error]
             [frontend.handler.command-palette :as command-palette]
             [frontend.handler.events :as events]
+            [frontend.handler.events.ui]
             [frontend.handler.file-based.events]
             [frontend.handler.file-based.file :as file-handler]
             [frontend.handler.global-config :as global-config-handler]
@@ -104,7 +105,9 @@
 (defn- handle-connection-change
   [e]
   (let [online? (= (gobj/get e "type") "online")]
-    (state/set-online! online?)))
+    (state/set-online! online?)
+    (state/<invoke-db-worker :thread-api/update-thread-atom
+                             :thread-atom/online-event online?)))
 
 (defn set-network-watcher!
   []
@@ -155,7 +158,6 @@
   (i18n/start)
   (instrument/init)
   (state/set-online! js/navigator.onLine)
-  (set-network-watcher!)
 
   (-> (util/indexeddb-check?)
       (p/catch (fn [_e]
@@ -177,6 +179,7 @@
                _ (if (empty? repos)
                    (repo-handler/new-db! config/demo-repo)
                    (restore-and-setup! repo))]
+         (set-network-watcher!)
          (when (util/electron?)
            (persist-db/run-export-periodically!))
          (when (mobile-util/native-platform?)

+ 3 - 3
src/main/frontend/handler/editor.cljs

@@ -77,8 +77,8 @@
 (def edit-block! block-handler/edit-block!)
 
 (defn- outliner-save-block!
-  [block]
-  (outliner-op/save-block! block))
+  [block & {:as opts}]
+  (outliner-op/save-block! block opts))
 
 (defn get-block-own-order-list-type
   [block]
@@ -3642,7 +3642,7 @@
              (when-not (= current-value value)
                (let [block {:block/uuid block-id
                             :block/collapsed? value}]
-                 (outliner-save-block! block)))))))
+                 (outliner-save-block! block {:outliner-op :collapse-expand-blocks})))))))
       (doseq [block-id block-ids]
         (state/set-collapsed-block! block-id value)))))
 

+ 2 - 599
src/main/frontend/handler/events.cljs

@@ -10,24 +10,7 @@
             [clojure.core.async.interop :refer [p->c]]
             [clojure.string :as string]
             [frontend.commands :as commands]
-            [frontend.components.block :as block]
-            [frontend.components.cmdk.core :as cmdk]
-            [frontend.components.diff :as diff]
-            [frontend.components.encryption :as encryption]
-            [frontend.components.file-based.git :as git-component]
-            [frontend.components.file-sync :as file-sync]
-            [frontend.components.page :as component-page]
-            [frontend.components.plugins :as plugin]
-            [frontend.components.property.dialog :as property-dialog]
-            [frontend.components.repo :as repo]
-            [frontend.components.select :as select]
-            [frontend.components.selection :as selection]
-            [frontend.components.settings :as settings]
-            [frontend.components.shell :as shell]
-            [frontend.components.user.login :as login]
-            [frontend.components.whiteboard :as whiteboard]
             [frontend.config :as config]
-            [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db.async :as db-async]
@@ -36,10 +19,7 @@
             [frontend.db.persist :as db-persist]
             [frontend.db.transact :as db-transact]
             [frontend.extensions.fsrs :as fsrs]
-            [frontend.extensions.srs :as srs]
             [frontend.fs :as fs]
-            [frontend.fs.capacitor-fs :as capacitor-fs]
-            [frontend.fs.nfs :as nfs]
             [frontend.fs.sync :as sync]
             [frontend.fs.watcher-handler :as fs-watcher]
             [frontend.handler.assets :as assets-handler]
@@ -50,8 +30,6 @@
             [frontend.handler.db-based.rtc-flows :as rtc-flows]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.export :as export]
-            [frontend.handler.file-based.file :as file-handler]
-            [frontend.handler.file-based.nfs :as nfs-handler]
             [frontend.handler.file-sync :as file-sync-handler]
             [frontend.handler.graph :as graph-handler]
             [frontend.handler.notification :as notification]
@@ -63,12 +41,9 @@
             [frontend.handler.search :as search-handler]
             [frontend.handler.shell :as shell-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.handler.user :as user-handler]
             [frontend.mobile.core :as mobile]
-            [frontend.mobile.graph-picker :as graph-picker]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.posthog :as posthog]
-            [frontend.modules.instrumentation.sentry :as sentry-event]
             [frontend.modules.outliner.pipeline :as pipeline]
             [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.modules.shortcut.core :as st]
@@ -76,16 +51,11 @@
             [frontend.quick-capture :as quick-capture]
             [frontend.search :as search]
             [frontend.state :as state]
-            [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.persist-var :as persist-var]
             [goog.dom :as gdom]
             [lambdaisland.glogi :as log]
-            [logseq.common.config :as common-config]
-            [logseq.common.util :as common-util]
-            [logseq.shui.ui :as shui]
-            [promesa.core :as p]
-            [rum.core :as rum]))
+            [promesa.core :as p]))
 
 ;; TODO: should we move all events here?
 
@@ -96,54 +66,10 @@
             (async/<! (sync/<sync-stop))
             (some-> (sync/<sync-start) async/<!)))
 
-(defn- file-sync-stop! []
+(defn file-sync-stop! []
   (async/go (async/<! (p->c (persist-var/load-vars)))
             (async/<! (sync/<sync-stop))))
 
-(defn- enable-beta-features!
-  []
-  (when-not (false? (state/enable-sync?)) ; user turns it off
-    (file-sync-handler/set-sync-enabled! true)))
-
-(defmethod handle :user/fetch-info-and-graphs [[_]]
-  (state/set-state! [:ui/loading? :login] false)
-  (async/go
-    (let [result (async/<! (sync/<user-info sync/remoteapi))]
-      (cond
-        (instance? ExceptionInfo result)
-        nil
-        (map? result)
-        (do
-          (state/set-user-info! result)
-          (when-let [uid (user-handler/user-uuid)]
-            (sentry-event/set-user! uid))
-          (let [status (if (user-handler/alpha-or-beta-user?) :welcome :unavailable)]
-            (when (and (= status :welcome) (user-handler/logged-in?))
-              (enable-beta-features!)
-              (async/<! (p->c (rtc-handler/<get-remote-graphs)))
-              (async/<! (file-sync-handler/load-session-graphs))
-              (p/let [repos (repo-handler/refresh-repos!)]
-                (when-let [repo (state/get-current-repo)]
-                  (when (some #(and (= (:url %) repo)
-                                    (vector? (:sync-meta %))
-                                    (util/uuid-string? (first (:sync-meta %)))
-                                    (util/uuid-string? (second (:sync-meta %)))) repos)
-                    (sync/<sync-start)))))
-            (file-sync/maybe-onboarding-show status)))))))
-
-(defmethod handle :user/logout [[_]]
-  (file-sync-handler/reset-session-graphs)
-  (sync/remove-all-pwd!)
-  (file-sync-handler/reset-user-state!)
-  (login/sign-out!))
-
-(defmethod handle :user/login [[_ host-ui?]]
-  (if (or host-ui? (not util/electron?))
-    (js/window.open config/LOGIN-URL)
-    (if (mobile-util/native-platform?)
-      (route-handler/redirect! {:to :user-login})
-      (login/open-login-modal!))))
-
 (defmethod handle :graph/added [[_ repo {:keys [empty-graph?]}]]
   (search-handler/rebuild-indices!)
   (plugin-handler/hook-plugin-app :graph-after-indexed {:repo repo :empty-graph? empty-graph?})
@@ -215,45 +141,6 @@
          :warning))
       (graph-switch-on-persisted graph opts))))
 
-(defmethod handle :graph/pull-down-remote-graph [[_ graph dir-name]]
-  (if (mobile-util/native-ios?)
-    (when-let [graph-name (or dir-name (:GraphName graph))]
-      (let [graph-name (util/safe-sanitize-file-name graph-name)]
-        (if (string/blank? graph-name)
-          (notification/show! "Illegal graph folder name.")
-
-          ;; Create graph directory under Logseq document folder (local)
-          (when-let [root (state/get-local-container-root-url)]
-            (let [graph-path (graph-picker/validate-graph-dirname root graph-name)]
-              (->
-               (p/let [exists? (fs/dir-exists? graph-path)]
-                 (let [overwrite? (if exists?
-                                    (js/confirm (str "There's already a directory with the name \"" graph-name "\", do you want to overwrite it? Make sure to backup it first if you're not sure about it."))
-                                    true)]
-                   (if overwrite?
-                     (p/let [_ (fs/mkdir-if-not-exists graph-path)]
-                       (nfs-handler/ls-dir-files-with-path!
-                        graph-path
-                        {:ok-handler (fn []
-                                       (file-sync-handler/init-remote-graph graph-path graph)
-                                       (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))}))
-                     (let [graph-name (-> (js/prompt "Please specify a new directory name to download the graph:")
-                                          str
-                                          string/trim)]
-                       (when-not (string/blank? graph-name)
-                         (state/pub-event! [:graph/pull-down-remote-graph graph graph-name]))))))
-               (p/catch (fn [^js e]
-                          (notification/show! (str e) :error)
-                          (js/console.error e)))))))))
-    (when (:GraphName graph)
-      (shui/dialog-open!
-       (file-sync/pick-dest-to-sync-panel graph)))))
-
-(defmethod handle :graph/pick-page-histories [[_ graph-uuid page-name]]
-  (shui/dialog-open!
-   (file-sync/pick-page-histories-panel graph-uuid page-name)
-   {:id :page-histories :label "modal-page-histories"}))
-
 (defmethod handle :graph/open-new-window [[_ev target-repo]]
   (p/let [current-repo (state/get-current-repo)]
     (ui-handler/open-new-window-or-tab! current-repo target-repo)))
@@ -261,67 +148,6 @@
 (defmethod handle :graph/migrated [[_ _repo]]
   (js/alert "Graph migrated."))
 
-(defn get-local-repo
-  []
-  (when-let [repo (state/get-current-repo)]
-    (when (config/local-file-based-graph? repo)
-      repo)))
-
-(defn ask-permission
-  [repo]
-  (when
-   (and (not (util/electron?))
-        (not (mobile-util/native-platform?)))
-    (fn [{:keys [close]}]
-      [:div
-       ;; TODO: fn translation with args
-       [:p
-        "Grant native filesystem permission for directory: "
-        [:b (config/get-local-dir repo)]]
-       (ui/button
-        (t :settings-permission/start-granting)
-        :class "ui__modal-enter"
-        :on-click (fn []
-                    (nfs/check-directory-permission! repo)
-                    (close)))])))
-
-(defmethod handle :modal/nfs-ask-permission []
-  (when-let [repo (get-local-repo)]
-    (some-> (ask-permission repo)
-            (shui/dialog-open! {:align :top}))))
-
-(defmethod handle :modal/show-cards [[_ cards-id]]
-  (let [db-based? (config/db-based-graph? (state/get-current-repo))]
-    (shui/dialog-open!
-     (if db-based? (fn [] (fsrs/cards-view cards-id)) srs/global-cards)
-     {:id :srs
-      :label "flashcards__cp"})))
-
-(defmethod handle :modal/show-instruction [_]
-  (shui/dialog-open!
-   capacitor-fs/instruction
-   {:id :instruction
-    :label "instruction__cp"}))
-
-(defmethod handle :modal/show-themes-modal [[_ classic?]]
-  (if classic?
-    (plugin/open-select-theme!)
-    (route-handler/go-to-search! :themes)))
-
-(defmethod handle :ui/toggle-appearance [_]
-  (let [popup-id "appearance_settings"]
-    (if (gdom/getElement popup-id)
-      (shui/popup-hide! popup-id)
-      (shui/popup-show!
-       (js/document.querySelector ".toolbar-dots-btn")
-       (fn []
-         (settings/appearance))
-       {:id popup-id
-        :align :end}))))
-
-(defmethod handle :modal/set-git-username-and-email [[_ _content]]
-  (shui/dialog-open! git-component/set-git-username-and-email))
-
 (defmethod handle :page/create [[_ page-name opts]]
   (if (= page-name (date/today))
     (page-handler/create-today-journal!)
@@ -333,26 +159,10 @@
 (defmethod handle :page/renamed [[_ repo data]]
   (page-common-handler/after-page-renamed! repo data))
 
-(defmethod handle :page/show-delete-dialog [[_ selected-rows ok-handler]]
-  (shui/dialog-open!
-   (component-page/batch-delete-dialog
-    selected-rows false
-    ok-handler)))
-
 (defmethod handle :page/create-today-journal [[_ _repo]]
   (p/let [_ (page-handler/create-today-journal!)]
     (ui-handler/re-render-root!)))
 
-(defmethod handle :file/not-matched-from-disk [[_ path disk-content db-content]]
-  (when-let [repo (state/get-current-repo)]
-    (shui/dialog-open!
-     #(diff/local-file repo path disk-content db-content)
-     {:label "diff__cp"})))
-
-(defmethod handle :modal/display-file-version-selector  [[_ versions path  get-content]]
-  (shui/dialog-open!
-   #(git-component/file-version-selector versions path get-content)))
-
 (defmethod handle :graph/sync-context []
   (let [context {:dev? config/dev?
                  :node-test? util/node-test?
@@ -391,48 +201,6 @@
        (export/auto-db-backup! repo {:backup-now? true})
        (fs-watcher/load-graph-files! repo)))))
 
-(defmethod handle :notification/show [[_ {:keys [content status clear?]}]]
-  (notification/show! content status clear?))
-
-(defmethod handle :command/run [_]
-  (when (util/electron?)
-    (shui/dialog-open! shell/shell)))
-
-(defmethod handle :go/search [_]
-  (shui/dialog-open!
-   cmdk/cmdk-modal
-   {:id :ls-dialog-cmdk
-    :align :top
-    :content-props {:class "ls-dialog-cmdk"}
-    :close-btn? false}))
-
-(defmethod handle :go/plugins [_]
-  (plugin/open-plugins-modal!))
-
-(defmethod handle :go/plugins-waiting-lists [_]
-  (plugin/open-waiting-updates-modal!))
-
-(defmethod handle :go/plugins-from-file [[_ plugins]]
-  (plugin/open-plugins-from-file-modal! plugins))
-
-(defmethod handle :go/install-plugin-from-github [[_]]
-  (shui/dialog-open!
-   (plugin/install-from-github-release-container)))
-
-(defmethod handle :go/plugins-settings [[_ pid nav? title]]
-  (when pid
-    (state/set-state! :plugin/focused-settings pid)
-    (state/set-state! :plugin/navs-settings? (not (false? nav?)))
-    (plugin/open-focused-settings-modal! title)))
-
-(defmethod handle :go/proxy-settings [[_ agent-opts]]
-  (shui/dialog-open!
-   (plugin/user-proxy-settings-container agent-opts)
-   {:id :https-proxy-panel :center? true :class "lg:max-w-2xl"}))
-
-(defmethod handle :redirect-to-home [_]
-  (page-handler/create-today-journal!))
-
 (defmethod handle :instrument [[_ {:keys [type payload] :as opts}]]
   (when-not (empty? (dissoc opts :type :payload))
     (js/console.error "instrument data-map should only contains [:type :payload]"))
@@ -557,57 +325,12 @@
               (file-sync-restart!))))
         (state/pub-event! [:graph/ready (state/get-current-repo)])))))
 
-(defmethod handle :plugin/consume-updates [[_ id prev-pending? updated?]]
-  (let [downloading?   (:plugin/updates-downloading? @state/state)
-        auto-checking? (plugin-handler/get-auto-checking?)]
-    (when-let [coming (and (not downloading?)
-                           (get-in @state/state [:plugin/updates-coming id]))]
-      (let [error-code (:error-code coming)
-            error-code (if (= error-code (str :no-new-version)) nil error-code)
-            title      (:title coming)]
-        (when (and prev-pending? (not auto-checking?))
-          (if-not error-code
-            (plugin/set-updates-sub-content! (str title "...") 0)
-            (notification/show!
-             (str "[Checked]<" title "> " error-code) :error)))))
-
-    (if (and updated? downloading?)
-      ;; try to start consume downloading item
-      (if-let [next-coming (state/get-next-selected-coming-update)]
-        (plugin-handler/check-or-update-marketplace-plugin!
-         (assoc next-coming :only-check false :error-code nil)
-         (fn [^js e] (js/console.error "[Download Err]" next-coming e)))
-        (plugin-handler/close-updates-downloading))
-
-      ;; try to start consume pending item
-      (if-let [next-pending (second (first (:plugin/updates-pending @state/state)))]
-        (do
-          (println "Updates: take next pending - " (:id next-pending))
-          (js/setTimeout
-           #(plugin-handler/check-or-update-marketplace-plugin!
-             (assoc next-pending :only-check true :auto-check auto-checking? :error-code nil)
-             (fn [^js e]
-               (notification/show! (.toString e) :error)
-               (js/console.error "[Check Err]" next-pending e))) 500))
-
-        ;; try to open waiting updates list
-        (do (when (and prev-pending? (not auto-checking?)
-                       (seq (state/all-available-coming-updates)))
-              (plugin/open-waiting-updates-modal!))
-            (plugin-handler/set-auto-checking! false))))))
-
 (defmethod handle :plugin/hook-db-tx [[_ {:keys [blocks tx-data] :as payload}]]
   (when-let [payload (and (seq blocks)
                           (merge payload {:tx-data (map #(into [] %) tx-data)}))]
     (plugin-handler/hook-plugin-db :changed payload)
     (plugin-handler/hook-plugin-block-changes payload)))
 
-(defmethod handle :plugin/loader-perf-tip [[_ {:keys [^js o _s _e]}]]
-  (when-let [opts (.-options o)]
-    (notification/show!
-     (plugin/perf-tip-content (.-id o) (.-name opts) (.-url opts))
-     :warning false (.-id o))))
-
 (defmethod handle :mobile-file-watcher/changed [[_ ^js event]]
   (let [type (.-event event)
         payload (js->clj event :keywordize-keys true)]
@@ -621,51 +344,6 @@
 (defmethod handle :shortcut/refresh [[_]]
   (st/refresh!))
 
-(defn- refresh-cb []
-  (page-handler/create-today-journal!)
-  (file-sync-restart!))
-
-(defmethod handle :graph/ask-for-re-fresh [_]
-  (shui/dialog-open!
-   [:div {:style {:max-width 700}}
-    [:p (t :sync-from-local-changes-detected)]
-    [:div.flex.justify-end
-     (ui/button
-      (t :yes)
-      :autoFocus "on"
-      :class "ui__modal-enter"
-      :on-click (fn []
-                  (shui/dialog-close!)
-                  (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
-
-(defmethod handle :sync/create-remote-graph [[_ current-repo]]
-  (let [graph-name (js/decodeURI (util/node-path.basename current-repo))]
-    (async/go
-      (async/<! (sync/<sync-stop))
-      (state/set-state! [:ui/loading? :graph/create-remote?] true)
-      (when-let [GraphUUID (get (async/<! (file-sync-handler/create-graph graph-name)) 2)]
-        (async/<! (sync/<sync-start))
-        (state/set-state! [:ui/loading? :graph/create-remote?] false)
-        ;; update existing repo
-        (state/set-repos! (map (fn [r]
-                                 (if (= (:url r) current-repo)
-                                   (assoc r
-                                          :GraphUUID GraphUUID
-                                          :GraphName graph-name
-                                          :remote? true)
-                                   r))
-                               (state/get-repos)))))))
-
-(defmethod handle :modal/remote-encryption-input-pw-dialog [[_ repo-url remote-graph-info type opts]]
-  (shui/dialog-open!
-   (encryption/input-password
-    repo-url nil (merge
-                  (assoc remote-graph-info
-                         :type (or type :create-pwd-remote)
-                         :repo repo-url)
-                  opts))
-   {:center? true :close-btn? false :close-backdrop? false}))
-
 (defmethod handle :journal/insert-template [[_ page-name]]
   (let [page-name (util/page-name-sanity-lc page-name)]
     (when-let [page (db/get-page page-name)]
@@ -682,49 +360,6 @@
   (when-let [id (:block/uuid block)]
     (editor-handler/set-heading! id heading)))
 
-(defmethod handle :file-sync-graph/restore-file [[_ graph page-entity content]]
-  (when (db/get-db graph)
-    (let [file (:block/file page-entity)]
-      (when-let [path (:file/path file)]
-        (when (and (not= content (:file/content file))
-                   (:file/content file))
-          (sync/add-new-version-file graph path (:file/content file)))
-        (p/let [_ (file-handler/alter-file graph
-                                           path
-                                           content
-                                           {:re-render-root? true
-                                            :skip-compare? true})]
-          (state/close-modal!)
-          (route-handler/redirect! {:to :page
-                                    :path-params {:name (:block/name page-entity)}}))))))
-
-(defmethod handle :whiteboard/onboarding [[_ opts]]
-  (shui/dialog-open!
-   (fn [{:keys [close]}] (whiteboard/onboarding-welcome close))
-   (merge {:close-btn?      false
-           :center?         true
-           :close-backdrop? false} opts)))
-
-(defmethod handle :file-sync/onboarding-tip [[_ type opts]]
-  (let [type (keyword type)]
-    (when-not (config/db-based-graph? (state/get-current-repo))
-      (shui/dialog-open!
-       (file-sync/make-onboarding-panel type)
-       (merge {:close-btn? false
-               :center? true
-               :close-backdrop? (not= type :welcome)} opts)))))
-
-(defmethod handle :file-sync/maybe-onboarding-show [[_ type]]
-  (file-sync/maybe-onboarding-show type))
-
-(defmethod handle :file-sync/storage-exceed-limit [[_]]
-  (notification/show! "file sync storage exceed limit" :warning false)
-  (file-sync-stop!))
-
-(defmethod handle :file-sync/graph-count-exceed-limit [[_]]
-  (notification/show! "file sync graph count exceed limit" :warning false)
-  (file-sync-stop!))
-
 (defmethod handle :graph/restored [[_ graph]]
   (when graph (assets-handler/ensure-assets-dir! graph))
   (mobile/init!)
@@ -741,143 +376,12 @@
   (route-handler/redirect! {:to :page
                             :path-params {:name link}}))
 
-(defmethod handle :graph/dir-gone [[_ dir]]
-  (state/pub-event! [:notification/show
-                     {:content (str "The directory " dir " has been renamed or deleted, the editor will be disabled for this graph, you can unlink the graph.")
-                      :status :error
-                      :clear? false}])
-  (state/update-state! :file/unlinked-dirs (fn [dirs] (conj dirs dir))))
-
-(defmethod handle :graph/dir-back [[_ repo dir]]
-  (when (contains? (:file/unlinked-dirs @state/state) dir)
-    (notification/clear-all!)
-    (state/pub-event! [:notification/show
-                       {:content (str "The directory " dir " has been back, you can edit your graph now.")
-                        :status :success
-                        :clear? true}])
-    (state/update-state! :file/unlinked-dirs (fn [dirs] (disj dirs dir)))
-    (when (= dir (config/get-repo-dir repo))
-      (fs/watch-dir! dir))))
-
-(defmethod handle :ui/notify-skipped-downloading-files [[_ paths]]
-  (notification/show!
-   [:div
-    [:div.mb-4
-     [:div.font-semibold.mb-4.text-xl "It seems that some of your filenames are in the outdated format."]
-     [:p
-      "The files below that have reserved characters can't be saved on this device."]
-     [:div.overflow-y-auto.max-h-96
-      [:ol.my-2
-       (for [path paths]
-         [:li path])]]
-
-     [:div
-      [:p
-       "Check " [:a {:href "https://docs.logseq.com/#/page/logseq%20file%20and%20folder%20naming%20rules"
-                     :target "_blank"}
-                 "Logseq file and folder naming rules"]
-       " for more details."]]]]
-   :warning
-   false))
-
-(defmethod handle :graph/setup-a-repo [[_ opts]]
-  (let [opts' (merge {:picked-root-fn #(state/close-modal!)
-                      :native-icloud? (not (string/blank? (state/get-icloud-container-root-url)))
-                      :logged?        (user-handler/logged-in?)} opts)]
-    (if (mobile-util/native-ios?)
-      (shui/dialog-open!
-       #(graph-picker/graph-picker-cp opts')
-       {:label "graph-setup"})
-      (page-handler/ls-dir-files! st/refresh! opts'))))
-
-(defmethod handle :graph/new-db-graph [[_ _opts]]
-  (shui/dialog-open!
-   repo/new-db-graph
-   {:id :new-db-graph
-    :title [:h2 "Create a new graph"]
-    :style {:max-width "500px"}}))
-
-(defmethod handle :dialog-select/graph-open []
-  (select/dialog-select! :graph-open))
-
-(defmethod handle :dialog-select/graph-remove []
-  (select/dialog-select! :graph-remove))
-
-(defmethod handle :dialog-select/db-graph-replace []
-  (select/dialog-select! :db-graph-replace))
-
 (defmethod handle :graph/save-db-to-disk [[_ _opts]]
   (persist-db/export-current-graph! {:succ-notification? true}))
 
-(defmethod handle :class/configure [[_ page]]
-  (shui/dialog-open!
-   #(block/block-container {} page)
-   {:label "page-configure"
-    :align :top}))
-
-(defmethod handle :file/alter [[_ repo path content]]
-  (p/let [_ (file-handler/alter-file repo path content {:from-disk? true})]
-    (ui-handler/re-render-root!)))
-
 (defmethod handle :ui/re-render-root [[_]]
   (ui-handler/re-render-root!))
 
-(rum/defcs file-id-conflict-item <
-  (rum/local false ::resolved?)
-  [state repo file data]
-  (let [resolved? (::resolved? state)
-        id (last (:assertion data))]
-    [:li {:key file}
-     [:div
-      [:a {:on-click #(js/window.apis.openPath file)} file]
-      (if @resolved?
-        [:div.flex.flex-row.items-center
-         (ui/icon "circle-check" {:style {:font-size 20}})
-         [:div.ml-1 "Resolved"]]
-        [:div
-         [:p
-          (str "It seems that another whiteboard file already has the ID \"" id
-               "\". You can fix it by changing the ID in this file with another UUID.")]
-         [:p
-          "Or, let me"
-          (ui/button "Fix"
-                     :on-click (fn []
-                                 (let [dir (config/get-repo-dir repo)]
-                                   (p/let [content (fs/read-file dir file)]
-                                     (let [new-content (string/replace content (str id) (str (random-uuid)))]
-                                       (p/let [_ (fs/write-file! repo
-                                                                 dir
-                                                                 file
-                                                                 new-content
-                                                                 {})]
-                                         (reset! resolved? true))))))
-                     :class "inline mx-1")
-          "it."]])]]))
-
-(defmethod handle :file/parse-and-load-error [[_ repo parse-errors]]
-  (state/pub-event! [:notification/show
-                     {:content
-                      [:div
-                       [:h2.title "Oops. These files failed to import to your graph:"]
-                       [:ol.my-2
-                        (for [[file error] parse-errors]
-                          (let [data (ex-data error)]
-                            (cond
-                              (and (common-config/whiteboard? file)
-                                   (= :transact/upsert (:error data))
-                                   (uuid? (last (:assertion data))))
-                              (rum/with-key (file-id-conflict-item repo file data) file)
-
-                              :else
-                              (do
-                                (state/pub-event! [:capture-error {:error error
-                                                                   :payload {:type :file/parse-and-load-error}}])
-                                [:li.my-1 {:key file}
-                                 [:a {:on-click #(js/window.apis.openPath file)} file]
-                                 [:p (.-message error)]]))))]
-                       [:p "Don't forget to re-index your graph when all the conflicts are resolved."]]
-                      :status :error}]))
-
 (defmethod handle :run/cli-command [[_ command content]]
   (when (and command (not (string/blank? content)))
     (shell-handler/run-cli-command-wrapper! command content)))
@@ -930,70 +434,6 @@
   (when-let [blocks (and block (db-model/get-block-immediate-children (state/get-current-repo) (:block/uuid block)))]
     (editor-handler/toggle-blocks-as-own-order-list! blocks)))
 
-(defn- editor-new-property [block target {:keys [selected-blocks] :as opts}]
-  (let [editing-block (state/get-edit-block)
-        pos (state/get-edit-pos)
-        edit-block-or-selected (cond
-                                 editing-block
-                                 [editing-block]
-                                 (seq selected-blocks)
-                                 selected-blocks
-                                 :else
-                                 (seq (keep #(db/entity [:block/uuid %]) (state/get-selection-block-ids))))
-        current-block (when-let [s (state/get-current-page)]
-                        (when (util/uuid-string? s)
-                          (db/entity [:block/uuid (uuid s)])))
-        blocks (or (when block [block])
-                   edit-block-or-selected
-                   (when current-block [current-block]))
-        opts' (cond-> opts
-                editing-block
-                (assoc :original-block editing-block
-                       :edit-original-block
-                       (fn [{:keys [editing-default-property?]}]
-                         (when editing-block
-                           (let [content (:block/title (db/entity (:db/id editing-block)))
-                                 esc? (= "Escape" (state/get-ui-last-key-code))
-                                 [content' pos] (cond
-                                                  esc?
-                                                  [nil pos]
-                                                  (and (>= (count content) pos)
-                                                       (>= pos 2)
-                                                       (= (util/nth-safe content (dec pos))
-                                                          (util/nth-safe content (- pos 2))
-                                                          ";"))
-                                                  [(str (common-util/safe-subs content 0 (- pos 2))
-                                                        (common-util/safe-subs content pos))
-                                                   (- pos 2)]
-                                                  :else
-                                                  [nil pos])]
-                             (when content'
-                               (if editing-default-property?
-                                 (editor-handler/save-block! (state/get-current-repo) (:block/uuid editing-block) content')
-                                 (editor-handler/edit-block! editing-block (or pos :max)
-                                                             (cond-> {}
-                                                               content'
-                                                               (assoc :custom-content content'))))))))))]
-    (when (seq blocks)
-      (let [target' (or target
-                        (some-> (state/get-edit-input-id)
-                                (gdom/getElement))
-                        (first (state/get-selection-blocks)))]
-        (if target'
-          (shui/popup-show! target'
-                            #(property-dialog/dialog blocks opts')
-                            {:align "start"
-                             :auto-focus? true})
-          (shui/dialog-open! #(property-dialog/dialog blocks opts')
-                             {:id :property-dialog
-                              :align "start"}))))))
-
-(defmethod handle :editor/new-property [[_ {:keys [block target] :as opts}]]
-  (when-not config/publishing?
-    (p/do!
-     (editor-handler/save-current-block!)
-     (editor-new-property block target opts))))
-
 (defmethod handle :editor/upsert-type-block [[_ {:keys [block type lang update-current-block?]}]]
   (p/do!
    (when-not update-current-block?
@@ -1026,25 +466,6 @@
                       (db/entity [:block/uuid (:block/uuid block)])))]
        (js/setTimeout #(editor-handler/edit-block! block :max) 100)))))
 
-(rum/defc multi-tabs-dialog
-  []
-  (let [word (if (util/electron?) "window" "tab")]
-    [:div.flex.p-4.flex-col.gap-4.h-64
-     [:span.warning.text-lg
-      (util/format "Logseq doesn't support multiple %ss access to the same graph yet, please close this %s or switch to another graph."
-                   word word)]
-     [:div.text-lg
-      [:p "Switch to another repo: "]
-      [:div.border.rounded.bg-gray-01.overflow-hidden.w-60
-       (repo/repos-dropdown {:on-click (fn [e]
-                                         (util/stop e)
-                                         (state/set-state! :error/multiple-tabs-access-opfs? false)
-                                         (shui/dialog-close!))})]]]))
-
-(defmethod handle :show/multiple-tabs-error-dialog [_]
-  (state/set-state! :error/multiple-tabs-access-opfs? true)
-  (shui/dialog-open! multi-tabs-dialog))
-
 (defmethod handle :rtc/sync-state [[_ state]]
   (state/update-state! :rtc/state (fn [old] (merge old state))))
 
@@ -1075,24 +496,6 @@
 (defmethod handle :editor/run-query-command [_]
   (editor-handler/run-query-command!))
 
-(defmethod handle :editor/show-action-bar []
-  (let [selection (state/get-selection-blocks)
-        first-visible-block (some #(when (util/el-visible-in-viewport? % true) %) selection)]
-    (when first-visible-block
-      (shui/popup-hide! :selection-action-bar)
-      (shui/popup-show!
-       first-visible-block
-       (fn []
-         (selection/action-bar))
-       {:id :selection-action-bar
-        :content-props {:side "top"
-                        :class "!py-0 !px-0 !border-none"}
-        :auto-side? false
-        :align :start}))))
-
-(defmethod handle :editor/hide-action-bar []
-  (shui/popup-hide! :selection-action-bar))
-
 (defmethod handle :editor/load-blocks [[_ ids]]
   (when (seq ids)
     ;; not using `<get-blocks` here becuase because we want to

+ 354 - 0
src/main/frontend/handler/events/ui.cljs

@@ -0,0 +1,354 @@
+(ns frontend.handler.events.ui
+  "UI events"
+  (:require [frontend.components.block :as block]
+            [frontend.components.cmdk.core :as cmdk]
+            [frontend.components.page :as component-page]
+            [frontend.components.plugins :as plugin]
+            [frontend.components.property.dialog :as property-dialog]
+            [frontend.components.repo :as repo]
+            [frontend.components.select :as select]
+            [frontend.components.selection :as selection]
+            [frontend.components.settings :as settings]
+            [frontend.components.shell :as shell]
+            [frontend.components.user.login :as login]
+            [frontend.components.whiteboard :as whiteboard]
+            [frontend.config :as config]
+            [frontend.context.i18n :refer [t]]
+            [frontend.db :as db]
+            [frontend.extensions.fsrs :as fsrs]
+            [frontend.extensions.srs :as srs]
+            [frontend.fs.capacitor-fs :as capacitor-fs]
+            [frontend.fs.nfs :as nfs]
+            [frontend.fs.sync :as sync]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.handler.events :as events]
+            [frontend.handler.file-based.nfs :as nfs-handler]
+            [frontend.handler.file-sync :as file-sync-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.plugin :as plugin-handler]
+            [frontend.handler.route :as route-handler]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [goog.dom :as gdom]
+            [logseq.common.util :as common-util]
+            [logseq.shui.ui :as shui]
+            [promesa.core :as p]
+            [rum.core :as rum]))
+
+(defmethod events/handle :class/configure [[_ page]]
+  (shui/dialog-open!
+   #(block/block-container {} page)
+   {:label "page-configure"
+    :align :top}))
+
+(defmethod events/handle :go/search [_]
+  (shui/dialog-open!
+   cmdk/cmdk-modal
+   {:id :ls-dialog-cmdk
+    :align :top
+    :content-props {:class "ls-dialog-cmdk"}
+    :close-btn? false}))
+
+(defmethod events/handle :command/run [_]
+  (when (util/electron?)
+    (shui/dialog-open! shell/shell)))
+
+(defmethod events/handle :notification/show [[_ {:keys [content status clear?]}]]
+  (notification/show! content status clear?))
+
+(defmethod events/handle :command/run [_]
+  (when (util/electron?)
+    (shui/dialog-open! shell/shell)))
+
+(defmethod events/handle :go/plugins [_]
+  (plugin/open-plugins-modal!))
+
+(defmethod events/handle :go/plugins-waiting-lists [_]
+  (plugin/open-waiting-updates-modal!))
+
+(defmethod events/handle :go/plugins-from-file [[_ plugins]]
+  (plugin/open-plugins-from-file-modal! plugins))
+
+(defmethod events/handle :go/install-plugin-from-github [[_]]
+  (shui/dialog-open!
+   (plugin/install-from-github-release-container)))
+
+(defmethod events/handle :go/plugins-settings [[_ pid nav? title]]
+  (when pid
+    (state/set-state! :plugin/focused-settings pid)
+    (state/set-state! :plugin/navs-settings? (not (false? nav?)))
+    (plugin/open-focused-settings-modal! title)))
+
+(defmethod events/handle :go/proxy-settings [[_ agent-opts]]
+  (shui/dialog-open!
+   (plugin/user-proxy-settings-container agent-opts)
+   {:id :https-proxy-panel :center? true :class "lg:max-w-2xl"}))
+
+(defmethod events/handle :redirect-to-home [_]
+  (page-handler/create-today-journal!))
+
+(defmethod events/handle :page/show-delete-dialog [[_ selected-rows ok-handler]]
+  (shui/dialog-open!
+   (component-page/batch-delete-dialog
+    selected-rows false
+    ok-handler)))
+
+(defn ask-permission
+  [repo]
+  (when
+   (and (not (util/electron?))
+        (not (mobile-util/native-platform?)))
+    (fn [{:keys [close]}]
+      [:div
+       ;; TODO: fn translation with args
+       [:p
+        "Grant native filesystem permission for directory: "
+        [:b (config/get-local-dir repo)]]
+       (ui/button
+        (t :settings-permission/start-granting)
+        :class "ui__modal-enter"
+        :on-click (fn []
+                    (nfs/check-directory-permission! repo)
+                    (close)))])))
+
+(defn get-local-repo
+  []
+  (when-let [repo (state/get-current-repo)]
+    (when (config/local-file-based-graph? repo)
+      repo)))
+
+(defmethod events/handle :modal/nfs-ask-permission []
+  (when-let [repo (get-local-repo)]
+    (some-> (ask-permission repo)
+            (shui/dialog-open! {:align :top}))))
+
+(defmethod events/handle :modal/show-cards [[_ cards-id]]
+  (let [db-based? (config/db-based-graph? (state/get-current-repo))]
+    (shui/dialog-open!
+     (if db-based? (fn [] (fsrs/cards-view cards-id)) srs/global-cards)
+     {:id :srs
+      :label "flashcards__cp"})))
+
+(defmethod events/handle :modal/show-instruction [_]
+  (shui/dialog-open!
+   capacitor-fs/instruction
+   {:id :instruction
+    :label "instruction__cp"}))
+
+(defmethod events/handle :modal/show-themes-modal [[_ classic?]]
+  (if classic?
+    (plugin/open-select-theme!)
+    (route-handler/go-to-search! :themes)))
+
+(defmethod events/handle :ui/toggle-appearance [_]
+  (let [popup-id "appearance_settings"]
+    (if (gdom/getElement popup-id)
+      (shui/popup-hide! popup-id)
+      (shui/popup-show!
+       (js/document.querySelector ".toolbar-dots-btn")
+       (fn []
+         (settings/appearance))
+       {:id popup-id
+        :align :end}))))
+
+(defmethod events/handle :plugin/consume-updates [[_ id prev-pending? updated?]]
+  (let [downloading?   (:plugin/updates-downloading? @state/state)
+        auto-checking? (plugin-handler/get-auto-checking?)]
+    (when-let [coming (and (not downloading?)
+                           (get-in @state/state [:plugin/updates-coming id]))]
+      (let [error-code (:error-code coming)
+            error-code (if (= error-code (str :no-new-version)) nil error-code)
+            title      (:title coming)]
+        (when (and prev-pending? (not auto-checking?))
+          (if-not error-code
+            (plugin/set-updates-sub-content! (str title "...") 0)
+            (notification/show!
+             (str "[Checked]<" title "> " error-code) :error)))))
+
+    (if (and updated? downloading?)
+      ;; try to start consume downloading item
+      (if-let [next-coming (state/get-next-selected-coming-update)]
+        (plugin-handler/check-or-update-marketplace-plugin!
+         (assoc next-coming :only-check false :error-code nil)
+         (fn [^js e] (js/console.error "[Download Err]" next-coming e)))
+        (plugin-handler/close-updates-downloading))
+
+      ;; try to start consume pending item
+      (if-let [next-pending (second (first (:plugin/updates-pending @state/state)))]
+        (do
+          (println "Updates: take next pending - " (:id next-pending))
+          (js/setTimeout
+           #(plugin-handler/check-or-update-marketplace-plugin!
+             (assoc next-pending :only-check true :auto-check auto-checking? :error-code nil)
+             (fn [^js e]
+               (notification/show! (.toString e) :error)
+               (js/console.error "[Check Err]" next-pending e))) 500))
+
+        ;; try to open waiting updates list
+        (do (when (and prev-pending? (not auto-checking?)
+                       (seq (state/all-available-coming-updates)))
+              (plugin/open-waiting-updates-modal!))
+            (plugin-handler/set-auto-checking! false))))))
+
+(defmethod events/handle :plugin/loader-perf-tip [[_ {:keys [^js o _s _e]}]]
+  (when-let [opts (.-options o)]
+    (notification/show!
+     (plugin/perf-tip-content (.-id o) (.-name opts) (.-url opts))
+     :warning false (.-id o))))
+
+(defn- refresh-cb []
+  (page-handler/create-today-journal!)
+  (events/file-sync-restart!))
+
+(defmethod events/handle :graph/ask-for-re-fresh [_]
+  (shui/dialog-open!
+   [:div {:style {:max-width 700}}
+    [:p (t :sync-from-local-changes-detected)]
+    [:div.flex.justify-end
+     (ui/button
+      (t :yes)
+      :autoFocus "on"
+      :class "ui__modal-enter"
+      :on-click (fn []
+                  (shui/dialog-close!)
+                  (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
+
+(defn- editor-new-property [block target {:keys [selected-blocks] :as opts}]
+  (let [editing-block (state/get-edit-block)
+        pos (state/get-edit-pos)
+        edit-block-or-selected (cond
+                                 editing-block
+                                 [editing-block]
+                                 (seq selected-blocks)
+                                 selected-blocks
+                                 :else
+                                 (seq (keep #(db/entity [:block/uuid %]) (state/get-selection-block-ids))))
+        current-block (when-let [s (state/get-current-page)]
+                        (when (util/uuid-string? s)
+                          (db/entity [:block/uuid (uuid s)])))
+        blocks (or (when block [block])
+                   edit-block-or-selected
+                   (when current-block [current-block]))
+        opts' (cond-> opts
+                editing-block
+                (assoc :original-block editing-block
+                       :edit-original-block
+                       (fn [{:keys [editing-default-property?]}]
+                         (when editing-block
+                           (let [content (:block/title (db/entity (:db/id editing-block)))
+                                 esc? (= "Escape" (state/get-ui-last-key-code))
+                                 [content' pos] (cond
+                                                  esc?
+                                                  [nil pos]
+                                                  (and (>= (count content) pos)
+                                                       (>= pos 2)
+                                                       (= (util/nth-safe content (dec pos))
+                                                          (util/nth-safe content (- pos 2))
+                                                          ";"))
+                                                  [(str (common-util/safe-subs content 0 (- pos 2))
+                                                        (common-util/safe-subs content pos))
+                                                   (- pos 2)]
+                                                  :else
+                                                  [nil pos])]
+                             (when content'
+                               (if editing-default-property?
+                                 (editor-handler/save-block! (state/get-current-repo) (:block/uuid editing-block) content')
+                                 (editor-handler/edit-block! editing-block (or pos :max)
+                                                             (cond-> {}
+                                                               content'
+                                                               (assoc :custom-content content'))))))))))]
+    (when (seq blocks)
+      (let [target' (or target
+                        (some-> (state/get-edit-input-id)
+                                (gdom/getElement))
+                        (first (state/get-selection-blocks)))]
+        (if target'
+          (shui/popup-show! target'
+                            #(property-dialog/dialog blocks opts')
+                            {:align "start"
+                             :auto-focus? true})
+          (shui/dialog-open! #(property-dialog/dialog blocks opts')
+                             {:id :property-dialog
+                              :align "start"}))))))
+
+(defmethod events/handle :editor/new-property [[_ {:keys [block target] :as opts}]]
+  (when-not config/publishing?
+    (p/do!
+     (editor-handler/save-current-block!)
+     (editor-new-property block target opts))))
+
+(defmethod events/handle :graph/new-db-graph [[_ _opts]]
+  (shui/dialog-open!
+   repo/new-db-graph
+   {:id :new-db-graph
+    :title [:h2 "Create a new graph"]
+    :style {:max-width "500px"}}))
+
+(defmethod events/handle :dialog-select/graph-open []
+  (select/dialog-select! :graph-open))
+
+(defmethod events/handle :dialog-select/graph-remove []
+  (select/dialog-select! :graph-remove))
+
+(defmethod events/handle :dialog-select/db-graph-replace []
+  (select/dialog-select! :db-graph-replace))
+
+(rum/defc multi-tabs-dialog
+  []
+  (let [word (if (util/electron?) "window" "tab")]
+    [:div.flex.p-4.flex-col.gap-4.h-64
+     [:span.warning.text-lg
+      (util/format "Logseq doesn't support multiple %ss access to the same graph yet, please close this %s or switch to another graph."
+                   word word)]
+     [:div.text-lg
+      [:p "Switch to another repo: "]
+      [:div.border.rounded.bg-gray-01.overflow-hidden.w-60
+       (repo/repos-dropdown {:on-click (fn [e]
+                                         (util/stop e)
+                                         (state/set-state! :error/multiple-tabs-access-opfs? false)
+                                         (shui/dialog-close!))})]]]))
+
+(defmethod events/handle :show/multiple-tabs-error-dialog [_]
+  (state/set-state! :error/multiple-tabs-access-opfs? true)
+  (shui/dialog-open! multi-tabs-dialog))
+
+(defmethod events/handle :editor/show-action-bar []
+  (let [selection (state/get-selection-blocks)
+        first-visible-block (some #(when (util/el-visible-in-viewport? % true) %) selection)]
+    (when first-visible-block
+      (shui/popup-hide! :selection-action-bar)
+      (shui/popup-show!
+       first-visible-block
+       (fn []
+         (selection/action-bar))
+       {:id :selection-action-bar
+        :content-props {:side "top"
+                        :class "!py-0 !px-0 !border-none"}
+        :auto-side? false
+        :align :start}))))
+
+(defmethod events/handle :editor/hide-action-bar []
+  (shui/popup-hide! :selection-action-bar))
+
+(defmethod events/handle :user/logout [[_]]
+  (file-sync-handler/reset-session-graphs)
+  (sync/remove-all-pwd!)
+  (file-sync-handler/reset-user-state!)
+  (login/sign-out!))
+
+(defmethod events/handle :user/login [[_ host-ui?]]
+  (if (or host-ui? (not util/electron?))
+    (js/window.open config/LOGIN-URL)
+    (if (mobile-util/native-platform?)
+      (route-handler/redirect! {:to :user-login})
+      (login/open-login-modal!))))
+
+(defmethod events/handle :whiteboard/onboarding [[_ opts]]
+  (shui/dialog-open!
+   (fn [{:keys [close]}] (whiteboard/onboarding-welcome close))
+   (merge {:close-btn?      false
+           :center?         true
+           :close-backdrop? false} opts)))

+ 302 - 26
src/main/frontend/handler/file_based/events.cljs

@@ -1,21 +1,41 @@
 (ns frontend.handler.file-based.events
   "Events that are only for file graphs"
   (:require [clojure.core.async :as async]
+            [clojure.core.async.interop :refer [p->c]]
             [clojure.set :as set]
+            [clojure.string :as string]
+            [frontend.components.diff :as diff]
+            [frontend.components.encryption :as encryption]
+            [frontend.components.file-based.git :as git-component]
+            [frontend.components.file-sync :as file-sync]
+            [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
+            [frontend.fs :as fs]
+            [frontend.fs.sync :as sync]
+            [frontend.handler.common :as common-handler]
+            [frontend.handler.db-based.rtc :as rtc-handler]
             [frontend.handler.events :as events]
-            [frontend.handler.page :as page-handler]
-            [frontend.handler.repo :as repo-handler]
+            [frontend.handler.file-based.file :as file-handler]
             [frontend.handler.file-based.nfs :as nfs-handler]
-            [frontend.handler.common :as common-handler]
+            [frontend.handler.file-sync :as file-sync-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.handler.page :as page-handler]
             [frontend.handler.property :as property-handler]
-            [frontend.fs.sync :as sync]
+            [frontend.handler.repo :as repo-handler]
+            [frontend.handler.route :as route-handler]
+            [frontend.handler.ui :as ui-handler]
+            [frontend.handler.user :as user-handler]
+            [frontend.mobile.graph-picker :as graph-picker]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.modules.instrumentation.sentry :as sentry-event]
+            [frontend.modules.shortcut.core :as st]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [frontend.config :as config]
+            [logseq.common.config :as common-config]
             [logseq.shui.ui :as shui]
+            [promesa.core :as p]
             [rum.core :as rum]))
 
 (defmethod events/handle :graph/ask-for-re-index [[_ *multiple-windows? ui]]
@@ -23,32 +43,32 @@
   ;; ui - custom message to show on asking for re-index
   (if (and (util/atom? *multiple-windows?) @*multiple-windows?)
     (shui/dialog-open!
-      [:div
-       (when (not (nil? ui)) ui)
-       [:p (t :re-index-multiple-windows-warning)]])
+     [:div
+      (when (not (nil? ui)) ui)
+      [:p (t :re-index-multiple-windows-warning)]])
 
     (shui/dialog-open!
-      [:div {:style {:max-width 700}}
-       (when (not (nil? ui)) ui)
-       [:p (t :re-index-discard-unsaved-changes-warning)]
-       [:div.flex.justify-end.pt-2
-        (ui/button
-          (t :yes)
-          :autoFocus "on"
-          :class "ui__modal-enter"
-          :on-click (fn []
-                      (shui/dialog-close!)
-                      (state/pub-event! [:graph/re-index])))]])))
+     [:div {:style {:max-width 700}}
+      (when (not (nil? ui)) ui)
+      [:p (t :re-index-discard-unsaved-changes-warning)]
+      [:div.flex.justify-end.pt-2
+       (ui/button
+        (t :yes)
+        :autoFocus "on"
+        :class "ui__modal-enter"
+        :on-click (fn []
+                    (shui/dialog-close!)
+                    (state/pub-event! [:graph/re-index])))]])))
 
 (defmethod events/handle :graph/re-index [[_]]
   ;; Ensure the graph only has ONE window instance
   (when (config/local-file-based-graph? (state/get-current-repo))
     (async/go
-     (async/<! (sync/<sync-stop))
-     (repo-handler/re-index!
-      nfs-handler/rebuild-index!
-      #(do (page-handler/create-today-journal!)
-           (events/file-sync-restart!))))))
+      (async/<! (sync/<sync-stop))
+      (repo-handler/re-index!
+       nfs-handler/rebuild-index!
+       #(do (page-handler/create-today-journal!)
+            (events/file-sync-restart!))))))
 
 (defn set-block-query-properties!
   [block-id all-properties key add?]
@@ -118,5 +138,261 @@
                            (set all-properties))
         shown-properties (set/intersection (set all-properties) shown-properties)]
     (shui/dialog-open!
-      (query-properties-settings block shown-properties all-properties)
-      {})))
+     (query-properties-settings block shown-properties all-properties)
+     {})))
+
+(defmethod events/handle :modal/set-git-username-and-email [[_ _content]]
+  (shui/dialog-open! git-component/set-git-username-and-email))
+
+(defmethod events/handle :file/not-matched-from-disk [[_ path disk-content db-content]]
+  (when-let [repo (state/get-current-repo)]
+    (shui/dialog-open!
+     #(diff/local-file repo path disk-content db-content)
+     {:label "diff__cp"})))
+
+(defmethod events/handle :modal/display-file-version-selector  [[_ versions path  get-content]]
+  (shui/dialog-open!
+   #(git-component/file-version-selector versions path get-content)))
+
+(defmethod events/handle :modal/remote-encryption-input-pw-dialog [[_ repo-url remote-graph-info type opts]]
+  (shui/dialog-open!
+   (encryption/input-password
+    repo-url nil (merge
+                  (assoc remote-graph-info
+                         :type (or type :create-pwd-remote)
+                         :repo repo-url)
+                  opts))
+   {:center? true :close-btn? false :close-backdrop? false}))
+
+(defn- enable-beta-features!
+  []
+  (when-not (false? (state/enable-sync?)) ; user turns it off
+    (file-sync-handler/set-sync-enabled! true)))
+
+(defmethod events/handle :user/fetch-info-and-graphs [[_]]
+  (state/set-state! [:ui/loading? :login] false)
+  (async/go
+    (let [result (async/<! (sync/<user-info sync/remoteapi))]
+      (cond
+        (instance? ExceptionInfo result)
+        nil
+        (map? result)
+        (do
+          (state/set-user-info! result)
+          (when-let [uid (user-handler/user-uuid)]
+            (sentry-event/set-user! uid))
+          (let [status (if (user-handler/alpha-or-beta-user?) :welcome :unavailable)]
+            (when (and (= status :welcome) (user-handler/logged-in?))
+              (enable-beta-features!)
+              (async/<! (p->c (rtc-handler/<get-remote-graphs)))
+              (async/<! (file-sync-handler/load-session-graphs))
+              (p/let [repos (repo-handler/refresh-repos!)]
+                (when-let [repo (state/get-current-repo)]
+                  (when (some #(and (= (:url %) repo)
+                                    (vector? (:sync-meta %))
+                                    (util/uuid-string? (first (:sync-meta %)))
+                                    (util/uuid-string? (second (:sync-meta %)))) repos)
+                    (sync/<sync-start)))))
+            (file-sync/maybe-onboarding-show status)))))))
+
+(defmethod events/handle :graph/pull-down-remote-graph [[_ graph dir-name]]
+  (if (mobile-util/native-ios?)
+    (when-let [graph-name (or dir-name (:GraphName graph))]
+      (let [graph-name (util/safe-sanitize-file-name graph-name)]
+        (if (string/blank? graph-name)
+          (notification/show! "Illegal graph folder name.")
+
+          ;; Create graph directory under Logseq document folder (local)
+          (when-let [root (state/get-local-container-root-url)]
+            (let [graph-path (graph-picker/validate-graph-dirname root graph-name)]
+              (->
+               (p/let [exists? (fs/dir-exists? graph-path)]
+                 (let [overwrite? (if exists?
+                                    (js/confirm (str "There's already a directory with the name \"" graph-name "\", do you want to overwrite it? Make sure to backup it first if you're not sure about it."))
+                                    true)]
+                   (if overwrite?
+                     (p/let [_ (fs/mkdir-if-not-exists graph-path)]
+                       (nfs-handler/ls-dir-files-with-path!
+                        graph-path
+                        {:ok-handler (fn []
+                                       (file-sync-handler/init-remote-graph graph-path graph)
+                                       (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))}))
+                     (let [graph-name (-> (js/prompt "Please specify a new directory name to download the graph:")
+                                          str
+                                          string/trim)]
+                       (when-not (string/blank? graph-name)
+                         (state/pub-event! [:graph/pull-down-remote-graph graph graph-name]))))))
+               (p/catch (fn [^js e]
+                          (notification/show! (str e) :error)
+                          (js/console.error e)))))))))
+    (when (:GraphName graph)
+      (shui/dialog-open!
+       (file-sync/pick-dest-to-sync-panel graph)))))
+
+(defmethod events/handle :graph/pick-page-histories [[_ graph-uuid page-name]]
+  (shui/dialog-open!
+   (file-sync/pick-page-histories-panel graph-uuid page-name)
+   {:id :page-histories :label "modal-page-histories"}))
+
+(defmethod events/handle :file-sync/onboarding-tip [[_ type opts]]
+  (let [type (keyword type)]
+    (when-not (config/db-based-graph? (state/get-current-repo))
+      (shui/dialog-open!
+       (file-sync/make-onboarding-panel type)
+       (merge {:close-btn? false
+               :center? true
+               :close-backdrop? (not= type :welcome)} opts)))))
+
+(defmethod events/handle :file-sync/maybe-onboarding-show [[_ type]]
+  (file-sync/maybe-onboarding-show type))
+
+(defmethod events/handle :file-sync/storage-exceed-limit [[_]]
+  (notification/show! "file sync storage exceed limit" :warning false)
+  (events/file-sync-stop!))
+
+(defmethod events/handle :file-sync/graph-count-exceed-limit [[_]]
+  (notification/show! "file sync graph count exceed limit" :warning false)
+  (events/file-sync-stop!))
+
+(defmethod events/handle :graph/dir-gone [[_ dir]]
+  (state/pub-event! [:notification/show
+                     {:content (str "The directory " dir " has been renamed or deleted, the editor will be disabled for this graph, you can unlink the graph.")
+                      :status :error
+                      :clear? false}])
+  (state/update-state! :file/unlinked-dirs (fn [dirs] (conj dirs dir))))
+
+(defmethod events/handle :graph/dir-back [[_ repo dir]]
+  (when (contains? (:file/unlinked-dirs @state/state) dir)
+    (notification/clear-all!)
+    (state/pub-event! [:notification/show
+                       {:content (str "The directory " dir " has been back, you can edit your graph now.")
+                        :status :success
+                        :clear? true}])
+    (state/update-state! :file/unlinked-dirs (fn [dirs] (disj dirs dir)))
+    (when (= dir (config/get-repo-dir repo))
+      (fs/watch-dir! dir))))
+
+(defmethod events/handle :ui/notify-skipped-downloading-files [[_ paths]]
+  (notification/show!
+   [:div
+    [:div.mb-4
+     [:div.font-semibold.mb-4.text-xl "It seems that some of your filenames are in the outdated format."]
+     [:p
+      "The files below that have reserved characters can't be saved on this device."]
+     [:div.overflow-y-auto.max-h-96
+      [:ol.my-2
+       (for [path paths]
+         [:li path])]]
+
+     [:div
+      [:p
+       "Check " [:a {:href "https://docs.logseq.com/#/page/logseq%20file%20and%20folder%20naming%20rules"
+                     :target "_blank"}
+                 "Logseq file and folder naming rules"]
+       " for more details."]]]]
+   :warning
+   false))
+
+(defmethod events/handle :graph/setup-a-repo [[_ opts]]
+  (let [opts' (merge {:picked-root-fn #(state/close-modal!)
+                      :native-icloud? (not (string/blank? (state/get-icloud-container-root-url)))
+                      :logged?        (user-handler/logged-in?)} opts)]
+    (if (mobile-util/native-ios?)
+      (shui/dialog-open!
+       #(graph-picker/graph-picker-cp opts')
+       {:label "graph-setup"})
+      (page-handler/ls-dir-files! st/refresh! opts'))))
+
+(defmethod events/handle :file/alter [[_ repo path content]]
+  (p/let [_ (file-handler/alter-file repo path content {:from-disk? true})]
+    (ui-handler/re-render-root!)))
+
+(rum/defcs file-id-conflict-item <
+  (rum/local false ::resolved?)
+  [state repo file data]
+  (let [resolved? (::resolved? state)
+        id (last (:assertion data))]
+    [:li {:key file}
+     [:div
+      [:a {:on-click #(js/window.apis.openPath file)} file]
+      (if @resolved?
+        [:div.flex.flex-row.items-center
+         (ui/icon "circle-check" {:style {:font-size 20}})
+         [:div.ml-1 "Resolved"]]
+        [:div
+         [:p
+          (str "It seems that another whiteboard file already has the ID \"" id
+               "\". You can fix it by changing the ID in this file with another UUID.")]
+         [:p
+          "Or, let me"
+          (ui/button "Fix"
+                     :on-click (fn []
+                                 (let [dir (config/get-repo-dir repo)]
+                                   (p/let [content (fs/read-file dir file)]
+                                     (let [new-content (string/replace content (str id) (str (random-uuid)))]
+                                       (p/let [_ (fs/write-file! repo
+                                                                 dir
+                                                                 file
+                                                                 new-content
+                                                                 {})]
+                                         (reset! resolved? true))))))
+                     :class "inline mx-1")
+          "it."]])]]))
+
+(defmethod events/handle :file/parse-and-load-error [[_ repo parse-errors]]
+  (state/pub-event! [:notification/show
+                     {:content
+                      [:div
+                       [:h2.title "Oops. These files failed to import to your graph:"]
+                       [:ol.my-2
+                        (for [[file error] parse-errors]
+                          (let [data (ex-data error)]
+                            (cond
+                              (and (common-config/whiteboard? file)
+                                   (= :transact/upsert (:error data))
+                                   (uuid? (last (:assertion data))))
+                              (rum/with-key (file-id-conflict-item repo file data) file)
+
+                              :else
+                              (do
+                                (state/pub-event! [:capture-error {:error error
+                                                                   :payload {:type :file/parse-and-load-error}}])
+                                [:li.my-1 {:key file}
+                                 [:a {:on-click #(js/window.apis.openPath file)} file]
+                                 [:p (.-message error)]]))))]
+                       [:p "Don't forget to re-index your graph when all the conflicts are resolved."]]
+                      :status :error}]))
+
+(defmethod events/handle :file-sync-graph/restore-file [[_ graph page-entity content]]
+  (when (db/get-db graph)
+    (let [file (:block/file page-entity)]
+      (when-let [path (:file/path file)]
+        (when (and (not= content (:file/content file))
+                   (:file/content file))
+          (sync/add-new-version-file graph path (:file/content file)))
+        (p/let [_ (file-handler/alter-file graph
+                                           path
+                                           content
+                                           {:re-render-root? true
+                                            :skip-compare? true})]
+          (state/close-modal!)
+          (route-handler/redirect! {:to :page
+                                    :path-params {:name (:block/name page-entity)}}))))))
+
+(defmethod events/handle :sync/create-remote-graph [[_ current-repo]]
+  (let [graph-name (js/decodeURI (util/node-path.basename current-repo))]
+    (async/go
+      (async/<! (sync/<sync-stop))
+      (state/set-state! [:ui/loading? :graph/create-remote?] true)
+      (when-let [GraphUUID (get (async/<! (file-sync-handler/create-graph graph-name)) 2)]
+        (async/<! (sync/<sync-start))
+        (state/set-state! [:ui/loading? :graph/create-remote?] false)
+        ;; update existing repo
+        (state/set-repos! (map (fn [r]
+                                 (if (= (:url r) current-repo)
+                                   (assoc r
+                                          :GraphUUID GraphUUID
+                                          :GraphName graph-name
+                                          :remote? true)
+                                   r))
+                               (state/get-repos)))))))

+ 0 - 79
src/main/frontend/tippy-tooltip.css

@@ -1,79 +0,0 @@
-.tippy-popper {
-  max-width: 800px;
-}
-
-.tippy-popper[x-placement^=top] [x-arrow],
-.tippy-popper[x-placement^=top] [x-arrow].arrow-small,
-.tippy-popper[x-placement^=top] [x-arrow].arrow-big {
-  border-top-color: var(--ls-tertiary-background-color);
-}
-
-.tippy-popper[x-placement^=top] .tippy-tooltip.transparent-theme [x-circle],
-.tippy-popper[x-placement^=bottom] .tippy-tooltip.transparent-theme [x-circle],
-.tippy-popper[x-placement^=left] .tippy-tooltip.transparent-theme [x-circle],
-.tippy-popper[x-placement^=right] .tippy-tooltip.transparent-theme [x-circle],
-.tippy-popper .tippy-tooltip.transparent-theme {
-  background-color: var(--ls-secondary-background-color);
-}
-
-.tippy-popper[x-placement^=top] .tippy-tooltip.transparent-theme [x-arrow],
-.tippy-popper[x-placement^=top] .tippy-tooltip.transparent-theme [x-arrow].arrow-small,
-.tippy-popper[x-placement^=top] .tippy-tooltip.transparent-theme [x-arrow].arrow-big {
-  border-top-color: var(--ls-secondary-background-color);
-}
-
-.tippy-popper[x-placement^=bottom] [x-arrow],
-.tippy-popper[x-placement^=bottom] [x-arrow].arrow-small,
-.tippy-popper[x-placement^=bottom] [x-arrow].arrow-big {
-  border-bottom-color: var(--ls-tertiary-background-color);
-}
-
-.tippy-popper[x-placement^=bottom] .tippy-tooltip.transparent-theme [x-arrow],
-.tippy-popper[x-placement^=bottom] .tippy-tooltip.transparent-theme [x-arrow].arrow-small,
-.tippy-popper[x-placement^=bottom] .tippy-tooltip.transparent-theme [x-arrow].arrow-big {
-  border-bottom-color: var(--ls-secondary-background-color);
-}
-
-.tippy-popper[x-placement^=left] [x-arrow],
-.tippy-popper[x-placement^=left] [x-arrow].arrow-small,
-.tippy-popper[x-placement^=left] [x-arrow].arrow-big {
-  border-left-color: var(--ls-tertiary-background-color);
-}
-
-.tippy-popper[x-placement^=left] .tippy-tooltip.transparent-theme [x-arrow],
-.tippy-popper[x-placement^=left] .tippy-tooltip.transparent-theme [x-arrow].arrow-small,
-.tippy-popper[x-placement^=left] .tippy-tooltip.transparent-theme [x-arrow].arrow-big {
-  border-left-color: var(--ls-secondary-background-color);
-}
-
-.tippy-popper[x-placement^=right] [x-arrow],
-.tippy-popper[x-placement^=right] [x-arrow].arrow-small,
-.tippy-popper[x-placement^=right] [x-arrow].arrow-big {
-  border-right-color: var(--ls-tertiary-background-color);
-}
-
-.tippy-popper[x-placement^=right] .tippy-tooltip.transparent-theme [x-arrow],
-.tippy-popper[x-placement^=right] .tippy-tooltip.transparent-theme [x-arrow].arrow-small,
-.tippy-popper[x-placement^=right] .tippy-tooltip.transparent-theme [x-arrow].arrow-big {
-  border-right-color: var(--ls-tertiary-background-color);
-}
-
-.tippy-tooltip {
-  @apply shadow border border-gray-07 dark:border-gray-05 px-2 py-1;
-
-  will-change: auto;
-  color: var(--ls-primary-text-color, hsl(var(--foreground)));
-  background-color: var(--lx-gray-03, var(--ls-tertiary-background-color, var(--rx-gray-03)));
-}
-
-.tippy-tooltip [x-circle] {
-  will-change: auto;
-}
-
-.tippy-popper .tippy-tooltip.customized-theme * {
-  text-align: left;
-}
-
-.tippy-popper .tippy-tooltip.monospace-theme {
-  font-family: 'Fira Code', Monaco, Menlo, Consolas, 'COURIER NEW', monospace;
-}

+ 12 - 54
src/main/frontend/ui.cljs

@@ -5,7 +5,6 @@
             ["emoji-mart" :as emoji-mart]
             ["react-intersection-observer" :as react-intersection-observer]
             ["react-textarea-autosize" :as TextareaAutosize]
-            ["react-tippy" :as react-tippy]
             ["react-transition-group" :refer [CSSTransition TransitionGroup]]
             ["react-virtuoso" :refer [Virtuoso VirtuosoGrid]]
             [cljs-bean.core :as bean]
@@ -40,6 +39,7 @@
             [rum.core :as rum]))
 
 (declare icon)
+(declare tooltip)
 
 (defonce transition-group (r/adapt-class TransitionGroup))
 (defonce css-transition (r/adapt-class CSSTransition))
@@ -47,7 +47,6 @@
 (defonce virtualized-list (r/adapt-class Virtuoso))
 (defonce virtualized-grid (r/adapt-class VirtuosoGrid))
 
-(def Tippy (r/adapt-class (gobj/get react-tippy "Tooltip")))
 (def ReactTweetEmbed (r/adapt-class react-tweet-embed))
 (def useInView (gobj/get react-intersection-observer "useInView"))
 (defonce _emoji-init-data ((gobj/get emoji-mart "init") #js {:data emoji-data}))
@@ -791,45 +790,6 @@
           :checked selected}]
         label])]))
 
-(rum/defcs tippy < rum/static
-  (rum/local false ::mounted?)
-  [state {:keys [fixed-position? open? html] :as opts} child]
-  (let [*mounted? (::mounted? state)
-        manual (not= open? nil)
-        open? (if manual open? @*mounted?)
-        disabled? (not (state/enable-tooltip?))]
-    (Tippy (->
-            (merge {:arrow true
-                    :sticky true
-                    :delay 600
-                    :theme "customized"
-                    :disabled disabled?
-                    :unmountHTMLWhenHide true
-                    :open (if disabled? false open?)
-                    :trigger (if manual "manual" "mouseenter focus")
-                    ;; See https://github.com/tvkhoa/react-tippy/issues/13
-                    :popperOptions {:modifiers {:flip {:enabled (not fixed-position?)}
-                                                :hide {:enabled false}
-                                                :preventOverflow {:enabled false}}}
-                    :onShow #(when-not (or (state/editing?)
-                                           @(:ui/scrolling? @state/state))
-                               (reset! *mounted? true))
-                    :onHide #(reset! *mounted? false)}
-                   opts)
-            (assoc :html (or
-                          (when open?
-                            (try
-                              (when html
-                                (if (fn? html)
-                                  (html)
-                                  [:div.px-2.py-1
-                                   html]))
-                              (catch :default e
-                                (log/error :exception e)
-                                [:div])))
-                          [:div {:key "tippy"} ""])))
-           (rum/fragment {:key "tippy-children"} child))))
-
 (rum/defcs slider < rum/reactive
   {:init (fn [state]
            (assoc state ::value (atom (first (:rum/args state)))))}
@@ -911,18 +871,13 @@
 
 (rum/defc with-shortcut < rum/reactive
   < {:key-fn (fn [key pos] (str "shortcut-" key pos))}
-  [shortcut-key position content]
+  [shortcut-key _position content]
   (let [shortcut-tooltip? (state/sub :ui/shortcut-tooltip?)
         enabled-tooltip? (state/enable-tooltip?)]
     (if (and enabled-tooltip? shortcut-tooltip?)
-      (tippy
-       {:html [:div.text-sm.font-medium (keyboard-shortcut-from-config shortcut-key)]
-        :interactive true
-        :position    position
-        :theme       "monospace"
-        :delay       [1000, 100]
-        :arrow       true}
-       content)
+      (tooltip content
+        [:div.text-sm.font-medium (keyboard-shortcut-from-config shortcut-key)]
+        {:trigger-props {:as-child true}})
       content)))
 
 (rum/defc progress-bar
@@ -1020,11 +975,14 @@
       :small? true)]]))
 
 (rum/defc tooltip
-  [trigger tooltip-content & {:keys [trigger-props]}]
+  [trigger tooltip-content & {:keys [portal? root-props trigger-props content-props]}]
   (shui/tooltip-provider
-   (shui/tooltip
-    (shui/tooltip-trigger trigger-props trigger)
-    (shui/tooltip-content tooltip-content))))
+    (shui/tooltip root-props
+      (shui/tooltip-trigger (merge {:as-child true} trigger-props) trigger)
+      (if (not (false? portal?))
+        (shui/tooltip-portal
+          (shui/tooltip-content content-props tooltip-content))
+        (shui/tooltip-content content-props tooltip-content)))))
 
 (rum/defc DelDateButton
   [on-delete]

+ 8 - 1
src/main/frontend/worker/db_worker.cljs

@@ -25,6 +25,7 @@
             [frontend.worker.rtc.db-listener]
             [frontend.worker.search :as search]
             [frontend.worker.state :as worker-state]
+            [frontend.worker.thread-atom]
             [frontend.worker.undo-redo :as undo-redo]
             [frontend.worker.util :as worker-util]
             [goog.object :as gobj]
@@ -532,7 +533,13 @@
         (boolean
          (some
           ;; check if there's any entity reference this `block` except the view-entity
-          (fn [ref] (not= id (:db/id (:logseq.property/view-for ref))))
+          (fn [ref]
+            (not
+             (or (= id (:db/id (:logseq.property/view-for ref)))
+                 (ldb/hidden? (:block/page ref))
+                 (ldb/hidden? ref)
+                 (contains? (set (map :db/id (:block/tags ref))) id)
+                 (some? (get ref (:db/ident block))))))
           (:block/_refs block)))))))
 
 (def-thread-api :thread-api/get-block-parents

+ 2 - 1
src/main/frontend/worker/export.cljs

@@ -65,6 +65,7 @@
                   (if (and (contains? #{:block/title :block/name} a)
                            (let [entity (d/entity @conn e)]
                              (and (not (:db/ident entity))
-                                  (not (ldb/journal? entity)))))
+                                  (not (ldb/journal? entity))
+                                  (not (:logseq.property/built-in? entity)))))
                     (d/datom e a (str "debug " e) t)
                     (d/datom e a v t))))))

+ 13 - 0
src/main/frontend/worker/flows.cljs

@@ -0,0 +1,13 @@
+(ns frontend.worker.flows
+  "common flows in worker thread"
+  (:require [frontend.worker.state :as worker-state]
+            [missionary.core :as m]))
+
+(def online-event-flow
+  (->> (m/watch (get @worker-state/*state :thread-atom/online-event))
+       (m/eduction
+        (drop-while nil?)
+        (filter true?))))
+
+(comment
+  ((m/reduce (fn [_ x] (prn :xxx x)) online-event-flow) prn prn))

+ 5 - 1
src/main/frontend/worker/rtc/client.cljs

@@ -3,6 +3,7 @@
   (:require [clojure.string :as string]
             [datascript.core :as d]
             [frontend.common.missionary :as c.m]
+            [frontend.worker.flows :as worker-flows]
             [frontend.worker.rtc.branch-graph :as r.branch-graph]
             [frontend.worker.rtc.client-op :as client-op]
             [frontend.worker.rtc.exception :as r.ex]
@@ -64,7 +65,10 @@
           (let [{:keys [max-remote-schema-version]}
                 (m/?
                  (c.m/backoff
-                  (take 5 (drop 2 c.m/delays)) ;retry 5 times if remote-graph is creating (4000 8000 16000 32000 64000)
+                  {:delay-seq
+                   ;retry 5 times if remote-graph is creating (4000 8000 16000 32000 64000)
+                   (take 5 (drop 2 c.m/delays))
+                   :reset-flow worker-flows/online-event-flow}
                   (new-task--register-graph-updates get-ws-create-task graph-uuid major-schema-version repo)))]
             (when max-remote-schema-version
               (add-log-fn :rtc.log/higher-remote-schema-version-exists

+ 3 - 1
src/main/frontend/worker/rtc/ws.cljs

@@ -4,6 +4,7 @@
   https://github.com/ReilySiegel/missionary-websocket/blob/master/src/com/reilysiegel/missionary/websocket.cljs"
   (:require [cljs-http-missionary.client :as http]
             [frontend.common.missionary :as c.m]
+            [frontend.worker.flows :as worker-flows]
             [frontend.worker.rtc.exception :as r.ex]
             [frontend.worker.rtc.malli-schema :as rtc-schema]
             [missionary.core :as m]))
@@ -88,7 +89,8 @@
                (pos-int? open-ws-timeout))
           [retry-count open-ws-timeout])
   (c.m/backoff
-   (take retry-count c.m/delays)
+   {:delay-seq (take retry-count c.m/delays)
+    :reset-flow worker-flows/online-event-flow}
    (m/sp
      (try
        (if-let [ws (m/? (m/timeout (create-mws* url) open-ws-timeout))]

+ 6 - 1
src/main/frontend/worker/state.cljs

@@ -48,7 +48,12 @@
 
                        ;; new implementation
                        :undo/repo->ops (atom {})
-                       :redo/repo->ops (atom {})}))
+                       :redo/repo->ops (atom {})
+
+
+                       ;; thread atoms, these atoms' value are syncing from ui-thread
+                       :thread-atom/online-event (atom nil)
+                       }))
 
 (defonce *rtc-ws-url (atom nil))
 

+ 12 - 0
src/main/frontend/worker/thread_atom.cljs

@@ -0,0 +1,12 @@
+(ns frontend.worker.thread-atom
+  "atoms from ui-thread"
+  (:require [frontend.common.thread-api :as thread-api :refer [def-thread-api]]
+            [frontend.worker.state :as worker-state]))
+
+(def-thread-api :thread-api/update-thread-atom
+  [atom-key new-value]
+  (assert (and (keyword? atom-key)
+               (identical? "thread-atom" (namespace atom-key))))
+  (when-let [a (get @worker-state/*state atom-key)]
+    (reset! a new-value)
+    nil))

+ 4 - 4
src/rtc_e2e_test/client_steps.cljs

@@ -44,7 +44,7 @@
        (is (nil? r)))
      (m/?
       (c.m/backoff
-       (take 4 c.m/delays)
+       {}
        (m/sp
          (let [conn (helper/get-downloaded-test-conn)
                page1 (d/pull @conn '[*] [:block/uuid const/page1-uuid])
@@ -72,7 +72,7 @@
        (m/? (helper/new-task--wait-all-client-ops-sent))))
    :client2
    (c.m/backoff
-    (take 4 c.m/delays)
+    {}
     (m/sp
       (let [conn (helper/get-downloaded-test-conn)
             page (d/pull @conn '[*] [:block/uuid const/page2-uuid])]
@@ -107,7 +107,7 @@
        (m/? (helper/new-task--wait-all-client-ops-sent))))
    :client2
    (c.m/backoff
-    (take 4 c.m/delays)
+    {}
     (m/sp
       (let [conn (helper/get-downloaded-test-conn)
             block1 (d/pull @conn
@@ -231,7 +231,7 @@ client2:
        (m/? (helper/new-task--client2-sync-barrier-1->2 "step6"))
        (m/?
         (c.m/backoff
-         (take 4 c.m/delays)
+         {}
          (m/sp
            (let [page (d/pull @conn '[*] [:block/uuid const/step6-page-uuid])
                  page-blocks (when-let [page-id (:db/id page)]

+ 7 - 7
src/rtc_e2e_test/helper.cljs

@@ -25,7 +25,7 @@
 (defn new-task--wait-creating-graph
   [graph-uuid]
   (c.m/backoff
-   (take 4 c.m/delays)
+   {}
    (m/sp
      (let [graphs (m/? (rtc.core/new-task--get-graphs const/test-token))
            graph (some (fn [graph] (when (= graph-uuid (:graph-uuid graph)) graph)) graphs)]
@@ -47,7 +47,7 @@
 
 (def new-task--get-remote-example-graph-uuid
   (c.m/backoff
-   (take 5 c.m/delays)
+   {}
    (m/sp
      (let [graphs (m/? (rtc.core/new-task--get-graphs const/test-token))
            graph
@@ -94,11 +94,11 @@
   #_:clj-kondo/ignore
   (me/find
    client-op
-   [?op-type _ {:block-uuid ?block-uuid :av-coll [[!a !v _ !add] ...]}]
-   [?op-type ?block-uuid (map vector !a !v !add)]
+    [?op-type _ {:block-uuid ?block-uuid :av-coll [[!a !v _ !add] ...]}]
+    [?op-type ?block-uuid (map vector !a !v !add)]
 
-   [?op-type _ {:block-uuid ?block-uuid}]
-   [?op-type ?block-uuid]))
+    [?op-type _ {:block-uuid ?block-uuid}]
+    [?op-type ?block-uuid]))
 
 (defn new-task--wait-all-client-ops-sent
   [& {:keys [timeout] :or {timeout 10000}}]
@@ -144,7 +144,7 @@
   "Return a task that return message from other client"
   [block-title-pred-fn & {:keys [retry-message retry-count] :or {retry-count 4}}]
   (c.m/backoff
-   (take retry-count c.m/delays)
+   {:delay-seq (take retry-count c.m/delays)}
    (m/sp
      (let [conn (get-downloaded-test-conn)
            message-page-id (:db/id (ldb/get-page @conn const/message-page-uuid))

+ 0 - 1
tailwind.all.css

@@ -17,7 +17,6 @@
 @import "codemirror/lib/codemirror.css";
 @import "codemirror/theme/solarized.css";
 @import "codemirror/addon/hint/show-hint.css";
-@import "react-tippy/dist/tippy.css";
 @import "pdfjs-dist/web/pdf_viewer.css";
 @import "resources/css/tabler-extension.css";
 @import "resources/css/codemirror.lsradix.css";

+ 0 - 12
yarn.lock

@@ -6584,11 +6584,6 @@ plugin-error@^2.0.1:
   dependencies:
     ansi-colors "^1.0.1"
 
-popper.js@^1.11.1:
-  version "1.16.1"
-  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
-  integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
-
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -7249,13 +7244,6 @@ [email protected]:
     use-composed-ref "^1.0.0"
     use-latest "^1.0.0"
 
[email protected]:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/react-tippy/-/react-tippy-1.4.0.tgz#e8a8b4085ec985e5c94fe128918b733b588a1465"
-  integrity sha512-r/hM5XK9Ztr2ZY7IWKuRmISTlUPS/R6ddz6PO2EuxCgW+4JBcGZRPU06XcVPRDCOIiio8ryBQFrXMhFMhsuaHA==
-  dependencies:
-    popper.js "^1.11.1"
-
 [email protected]:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683"