Quellcode durchsuchen

Merge branch 'ksqsf-master'

Tienson Qin vor 2 Jahren
Ursprung
Commit
d589e7b890

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 50
-        versionName "0.8.16"
+        versionCode 51
+        versionName "0.8.17"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 3 - 1
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -248,7 +248,7 @@
                               (assoc :repeated? true))))))]
     (apply merge m)))
 
-(defn convert-page-if-journal
+(defn- convert-page-if-journal-impl
   "Convert journal file name to user' custom date format"
   [original-page-name date-formatter]
   (when original-page-name
@@ -259,6 +259,8 @@
          [original-page-name (gp-util/page-name-sanity-lc original-page-name) day])
        [original-page-name page-name day]))))
 
+(def convert-page-if-journal (memoize convert-page-if-journal-impl))
+
 ;; TODO: refactor
 (defn page-name->map
   "Create a page's map structure given a original page name (string).

+ 10 - 0
deps/graph-parser/src/logseq/graph_parser/mldoc.cljc

@@ -164,3 +164,13 @@
   (when (string? link)
     (some-> (first (inline->edn link (default-config format)))
             ast-link?)))
+
+(defn mldoc-link?
+  "Check whether s is a link (including page/block refs)."
+  [format s]
+  (let [result (inline->edn s (default-config format))]
+    (and
+     (= 1 (count result))
+     (let [result' (first result)]
+       (or (contains? #{"Nested_link"} (first result'))
+           (contains? #{"Page_ref" "Block_ref" "Complex"} (first (:url (second result')))))))))

+ 2 - 1
e2e-tests/basic.spec.ts

@@ -143,6 +143,7 @@ test('template', async ({ page, block }) => {
   await block.waitForBlocks(5)
 
   // NOTE: use delay to type slower, to trigger auto-completion UI.
+  await block.clickNext()
   await block.mustType('/template')
 
   await page.click('[title="Insert a created template here"]')
@@ -153,7 +154,7 @@ test('template', async ({ page, block }) => {
   await popupMenuItem.waitFor({ timeout: 2000 }) // wait for template search
   await popupMenuItem.click()
 
-  await block.waitForBlocks(8)
+  await block.waitForBlocks(9)
 })
 
 test('auto completion square brackets', async ({ page, block }) => {

+ 4 - 4
ios/App/App.xcodeproj/project.pbxproj

@@ -515,7 +515,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.8.16;
+				MARKETING_VERSION = 0.8.17;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -542,7 +542,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.8.16;
+				MARKETING_VERSION = 0.8.17;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -567,7 +567,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.8.16;
+				MARKETING_VERSION = 0.8.17;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -594,7 +594,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.8.16;
+				MARKETING_VERSION = 0.8.17;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 1
resources/package.json

@@ -1,7 +1,7 @@
 {
   "name": "Logseq",
   "productName": "Logseq",
-  "version": "0.8.16",
+  "version": "0.8.17",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",

+ 1 - 2
src/main/frontend/components/block.cljs

@@ -1002,8 +1002,7 @@
         (nil? metadata-show)
         (or
          (gp-config/local-asset? s)
-         (text-util/media-link? media-formats s)
-         (= (first s) \@)))
+         (text-util/media-link? media-formats s)))
        (true? (boolean metadata-show))))
 
      ;; markdown

+ 40 - 18
src/main/frontend/components/file.cljs

@@ -11,11 +11,15 @@
             [frontend.handler.export :as export-handler]
             [frontend.state :as state]
             [frontend.util :as util]
+            [frontend.fs :as fs]
+            [frontend.config :as config]
+            [frontend.ui :as ui]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util :as gp-util]
             [goog.object :as gobj]
             [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [promesa.core :as p]))
 
 (defn- get-path
   [state]
@@ -66,18 +70,25 @@
    (files-all)
    ])
 
-(rum/defcs file < rum/reactive
-  {:did-mount (fn [state]
+(rum/defcs file-inner < rum/reactive
+  {:will-mount (fn [state]
+                 (let [*content (atom nil)
+                       [path format] (:rum/args state)]
+                   (when (and format (contains? (gp-config/text-formats) format))
+                     (p/let [content (fs/read-file
+                                      (config/get-repo-dir (state/get-current-repo)) path)]
+                       (reset! *content content)))
+                   (assoc state ::file-content *content)))
+   :did-mount (fn [state]
                 (state/set-file-component! (:rum/react-component state))
                 state)
    :will-unmount (fn [state]
                    (state/clear-file-component!)
                    state)}
-  [state]
-  (let [path (get-path state)
-        format (gp-util/get-format path)
-        original-name (db/get-file-page path)
-        random-id (str (d/squuid))]
+  [state path format]
+  (let [original-name (db/get-file-page path)
+        random-id (str (d/squuid))
+        content (rum/react (::file-content state))]
     [:div.file {:id (str "file-edit-wrapper-" random-id)
                 :key path}
      [:h1.title
@@ -107,16 +118,27 @@
        (and format (contains? (gp-config/img-formats) format))
        [:img {:src (util/node-path.join "file://" path)}]
 
-       (and format (contains? (gp-config/text-formats) format))
-       (when-let [file-content (or (db/get-file path) "")]
-         (let [content (string/trim file-content)
-               mode (util/get-file-ext path)]
-            (lazy-editor/editor {:file?     true
-                                 :file-path path}
-                                (str "file-edit-" random-id)
-                                {:data-lang mode}
-                                content
-                               {})))
+       (and format
+            (contains? (gp-config/text-formats) format)
+            content)
+       (let [content' (string/trim content)
+             mode (util/get-file-ext path)]
+         (lazy-editor/editor {:file?     true
+                              :file-path path}
+                             (str "file-edit-" random-id)
+                             {:data-lang mode}
+                             content'
+                             {}))
+
+       (and format
+            (contains? (gp-config/text-formats) format))
+       (ui/loading "Loading ...")
 
        :else
        [:div (t :file/format-not-supported (name format))])]))
+
+(rum/defcs file
+  [state]
+  (let [path (get-path state)
+        format (gp-util/get-format path)]
+    (rum/with-key (file-inner path format) path)))

+ 43 - 41
src/main/frontend/extensions/pdf/assets.cljs

@@ -56,8 +56,8 @@
   (when hls-file
     (let [repo-cur (state/get-current-repo)
           repo-dir (config/get-repo-dir repo-cur)]
-      (p/let [_ (fs/create-if-not-exists repo-cur repo-dir hls-file "{:highlights []}")
-              res (fs/read-file repo-dir hls-file)
+      (p/let [_    (fs/create-if-not-exists repo-cur repo-dir hls-file "{:highlights []}")
+              res  (fs/read-file repo-dir hls-file)
               data (if res (reader/read-string res) {})]
         data))))
 
@@ -66,7 +66,7 @@
   (when hls-file
     (let [repo-cur (state/get-current-repo)
           repo-dir (config/get-repo-dir repo-cur)
-          data (pr-str {:highlights highlights :extra extra})]
+          data     (pr-str {:highlights highlights :extra extra})]
       (fs/write-file! repo-cur repo-dir hls-file data {:skip-compare? true}))))
 
 (defn resolve-hls-data-by-key$
@@ -82,13 +82,13 @@
 (defn persist-hl-area-image$
   [^js viewer current new-hl old-hl {:keys [top left width height]}]
   (when-let [^js canvas (and (:key current) (.-canvas (.getPageView viewer (dec (:page new-hl)))))]
-    (let [^js doc (.-ownerDocument canvas)
+    (let [^js doc     (.-ownerDocument canvas)
           ^js canvas' (.createElement doc "canvas")
-          dpr js/window.devicePixelRatio
-          repo-cur (state/get-current-repo)
-          repo-dir (config/get-repo-dir repo-cur)
-          dw (* dpr width)
-          dh (* dpr height)]
+          dpr         js/window.devicePixelRatio
+          repo-cur    (state/get-current-repo)
+          repo-dir    (config/get-repo-dir repo-cur)
+          dw          (* dpr width)
+          dh          (* dpr height)]
 
       (set! (. canvas' -width) dw)
       (set! (. canvas' -height) dh)
@@ -96,31 +96,31 @@
       (when-let [^js ctx (.getContext canvas' "2d" #js{:alpha false})]
         (set! (. ctx -imageSmoothingEnabled) false)
         (.drawImage
-          ctx canvas
-          (* left dpr) (* top dpr) (* width dpr) (* height dpr)
-          0 0 dw dh)
+         ctx canvas
+         (* left dpr) (* top dpr) (* width dpr) (* height dpr)
+         0 0 dw dh)
 
         (let [callback (fn [^js png]
                          ;; write image file
                          (p/catch
-                           (p/let [_ (js/console.time :write-area-image)
-                                   ^js png (.arrayBuffer png)
-                                   {:keys [key]} current
-                                   ;; dir
-                                   fstamp (get-in new-hl [:content :image])
-                                   old-fstamp (and old-hl (get-in old-hl [:content :image]))
-                                   fname (str (:page new-hl) "_" (:id new-hl))
-                                   fdir (str gp-config/local-assets-dir "/" key)
-                                   _ (fs/mkdir-if-not-exists (str repo-dir "/" fdir))
-                                   new-fpath (str fdir "/" fname "_" fstamp ".png")
-                                   old-fpath (and old-fstamp (str fdir "/" fname "_" old-fstamp ".png"))
-                                   _ (and old-fpath (apply fs/rename! repo-cur (map #(util/node-path.join repo-dir %) [old-fpath new-fpath])))
-                                   _ (fs/write-file! repo-cur repo-dir new-fpath png {:skip-compare? true})]
-
-                             (js/console.timeEnd :write-area-image))
-
-                           (fn [err]
-                             (js/console.error "[write area image Error]" err))))]
+                          (p/let [_          (js/console.time :write-area-image)
+                                  ^js png    (.arrayBuffer png)
+                                  {:keys [key]} current
+                                  ;; dir
+                                  fstamp     (get-in new-hl [:content :image])
+                                  old-fstamp (and old-hl (get-in old-hl [:content :image]))
+                                  fname      (str (:page new-hl) "_" (:id new-hl))
+                                  fdir       (str gp-config/local-assets-dir "/" key)
+                                  _          (fs/mkdir-if-not-exists (str repo-dir "/" fdir))
+                                  new-fpath  (str fdir "/" fname "_" fstamp ".png")
+                                  old-fpath  (and old-fstamp (str fdir "/" fname "_" old-fstamp ".png"))
+                                  _          (and old-fpath (apply fs/rename! repo-cur (map #(util/node-path.join repo-dir %) [old-fpath new-fpath])))
+                                  _          (fs/write-file! repo-cur repo-dir new-fpath png {:skip-compare? true})]
+
+                            (js/console.timeEnd :write-area-image))
+
+                          (fn [err]
+                            (js/console.error "[write area image Error]" err))))]
 
           (.toBlob canvas' callback))
         ))))
@@ -139,10 +139,10 @@
   (when-let [fkey (and (area-highlight? hl) (:key current))]
     (let [repo-cur (state/get-current-repo)
           repo-dir (config/get-repo-dir repo-cur)
-          fstamp (get-in hl [:content :image])
-          fname (str (:page hl) "_" (:id hl))
-          fdir (str gp-config/local-assets-dir "/" fkey)
-          fpath (util/node-path.join repo-dir (str fdir "/" fname "_" fstamp ".png"))]
+          fstamp   (get-in hl [:content :image])
+          fname    (str (:page hl) "_" (:id hl))
+          fdir     (str gp-config/local-assets-dir "/" fkey)
+          fpath    (util/node-path.join repo-dir (str fdir "/" fname "_" fstamp ".png"))]
 
       (fs/unlink! repo-cur fpath {}))))
 
@@ -218,15 +218,17 @@
 
 (defn open-block-ref!
   [block]
-  (let [id (:block/uuid block)
-        page (db-utils/pull (:db/id (:block/page block)))
+  (let [id        (:block/uuid block)
+        page      (db-utils/pull (:db/id (:block/page block)))
         page-name (:block/original-name page)
-        file-path (:file-path (:block/properties page))]
+        file-path (:file-path (:block/properties page))
+        hl-page   (:hl-page (:block/properties block))]
     (when-let [target-key (and page-name (subs page-name 5))]
       (p/let [hls (resolve-hls-data-by-key$ target-key)
               hls (and hls (:highlights hls))]
         (let [file-path (or file-path (str "../assets/" target-key ".pdf"))]
-          (if-let [matched (and hls (medley/find-first #(= id (:id %)) hls))]
+          (if-let [matched (or (and hls (medley/find-first #(= id (:id %)) hls))
+                               (and hl-page {:page hl-page}))]
             (do
               (state/set-state! :pdf/ref-highlight matched)
               ;; open pdf viewer
@@ -252,7 +254,7 @@
         images (to-array images)
         images (if-not (= (count images) 1)
                  (let [^js image (.closest (.-target e) ".hl-area")
-                       image (. image querySelector "img")]
+                       image     (. image querySelector "img")]
                    (->> images
                         (sort-by (juxt #(.-y %) #(.-x %)))
                         (split-with (complement #{image}))
@@ -260,8 +262,8 @@
                         (apply concat)))
                  images)
         images (for [^js it images] {:src (.-src it)
-                                     :w (.-naturalWidth it)
-                                     :h (.-naturalHeight it)})]
+                                     :w   (.-naturalWidth it)
+                                     :h   (.-naturalHeight it)})]
 
     (when (seq images)
       (lightbox/preview-images! images))))

+ 84 - 63
src/main/frontend/extensions/pdf/highlights.cljs

@@ -35,8 +35,15 @@
     (when viewer
       (when-let [ref-hl (state/sub :pdf/ref-highlight)]
         ;; delay handle: aim to fix page blink
-        (js/setTimeout #(pdf-utils/scroll-to-highlight viewer ref-hl) (if @*mounted? 50 500))
-        (js/setTimeout #(state/set-state! :pdf/ref-highlight nil) 1000)))
+        (js/setTimeout
+         (fn []
+           (if (:id ref-hl)
+             (pdf-utils/scroll-to-highlight viewer ref-hl)
+             (set! (.-currentPageNumber viewer) (or (:page ref-hl) 1))))
+         (if @*mounted? 50 500))
+
+        (js/setTimeout
+         #(state/set-state! :pdf/ref-highlight nil) 1000)))
     (reset! *mounted? true)))
 
 (rum/defc pdf-page-finder < rum/static
@@ -49,7 +56,8 @@
            (when-not active-hl
              (.on (.-eventBus viewer) (name :restore-last-page)
                   (fn [last-page]
-                    (set! (.-currentPageNumber viewer) (util/safe-parse-int last-page)))))))))
+                    (when last-page
+                      (set! (.-currentPageNumber viewer) (util/safe-parse-int last-page))))))))))
    [viewer])
   nil)
 
@@ -120,7 +128,7 @@
         action-fn!  (fn [action clear?]
                       (when-let [action (and action (name action))]
                         (let [highlight (if (fn? highlight) (highlight) highlight)
-                              content (:content highlight)]
+                              content   (:content highlight)]
                           (case action
                             "ref"
                             (pdf-assets/copy-hl-ref! highlight)
@@ -248,15 +256,15 @@
   [^js viewer vw-hl hl {:keys [show-ctx-menu! upd-hl!]}]
 
   (let [{:keys [id]} hl
-        *el    (rum/use-ref nil)
-        *dirty (rum/use-ref nil)
-        open-ctx-menu! (fn [^js/MouseEvent e]
-                         (.preventDefault e)
-                         (when-not (rum/deref *dirty)
-                           (let [x (.-clientX e)
-                                 y (.-clientY e)]
+        *el               (rum/use-ref nil)
+        *dirty            (rum/use-ref nil)
+        open-ctx-menu!    (fn [^js/MouseEvent e]
+                            (.preventDefault e)
+                            (when-not (rum/deref *dirty)
+                              (let [x (.-clientX e)
+                                    y (.-clientY e)]
 
-                             (show-ctx-menu! viewer hl {:x x :y y}))))
+                                (show-ctx-menu! viewer hl {:x x :y y}))))
 
         dragstart-handle! (fn [^js e]
                             (when-let [^js dt (and id (.-dataTransfer e))]
@@ -494,43 +502,43 @@
 (rum/defc ^:large-vars/cleanup-todo pdf-highlights
   [^js el ^js viewer initial-hls loaded-pages {:keys [set-dirty-hls!]}]
 
-  (let [^js doc        (.-ownerDocument el)
-        ^js win        (.-defaultView doc)
-        *mounted       (rum/use-ref false)
+  (let [^js doc         (.-ownerDocument el)
+        ^js win         (.-defaultView doc)
+        *mounted        (rum/use-ref false)
         [sel-state, set-sel-state!] (rum/use-state {:selection nil :range nil :collapsed nil :point nil})
         [highlights, set-highlights!] (rum/use-state initial-hls)
         [ctx-menu-state, set-ctx-menu-state!] (rum/use-state {:highlight nil :vw-pos nil :selection nil :point nil :reset-fn nil})
 
         clear-ctx-menu! (rum/use-callback
-                        #(let [reset-fn (:reset-fn ctx-menu-state)]
-                           (set-ctx-menu-state! {})
-                           (and (fn? reset-fn) (reset-fn)))
-                        [ctx-menu-state])
+                         #(let [reset-fn (:reset-fn ctx-menu-state)]
+                            (set-ctx-menu-state! {})
+                            (and (fn? reset-fn) (reset-fn)))
+                         [ctx-menu-state])
 
         show-ctx-menu!  (fn [^js viewer hl point & ops]
-                         (let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
-                           (set-ctx-menu-state! (apply merge (list* {:highlight hl :vw-pos vw-pos :point point} ops)))))
-
-        add-hl!        (fn [hl] (when (:id hl)
-                                  ;; fix js object
-                                  (let [highlights (pdf-utils/fix-nested-js highlights)]
-                                    (set-highlights! (conj highlights hl)))
-
-                                  (when-let [vw-pos (and (pdf-assets/area-highlight? hl)
-                                                         (pdf-utils/scaled-to-vw-pos viewer (:position hl)))]
-                                    ;; exceptions
-                                    (pdf-assets/persist-hl-area-image$ viewer (:pdf/current @state/state)
-                                                                       hl nil (:bounding vw-pos)))))
-
-        upd-hl!        (fn [hl]
-                         (let [highlights (pdf-utils/fix-nested-js highlights)]
-                           (when-let [[target-idx] (medley/find-first
-                                                    #(= (:id (second %)) (:id hl))
-                                                    (medley/indexed highlights))]
-                             (set-highlights! (assoc-in highlights [target-idx] hl))
-                             (pdf-assets/update-hl-block! hl))))
-
-        del-hl!        (fn [hl] (when-let [id (:id hl)] (set-highlights! (into [] (remove #(= id (:id %)) highlights)))))]
+                          (let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
+                            (set-ctx-menu-state! (apply merge (list* {:highlight hl :vw-pos vw-pos :point point} ops)))))
+
+        add-hl!         (fn [hl] (when (:id hl)
+                                   ;; fix js object
+                                   (let [highlights (pdf-utils/fix-nested-js highlights)]
+                                     (set-highlights! (conj highlights hl)))
+
+                                   (when-let [vw-pos (and (pdf-assets/area-highlight? hl)
+                                                          (pdf-utils/scaled-to-vw-pos viewer (:position hl)))]
+                                     ;; exceptions
+                                     (pdf-assets/persist-hl-area-image$ viewer (:pdf/current @state/state)
+                                                                        hl nil (:bounding vw-pos)))))
+
+        upd-hl!         (fn [hl]
+                          (let [highlights (pdf-utils/fix-nested-js highlights)]
+                            (when-let [[target-idx] (medley/find-first
+                                                     #(= (:id (second %)) (:id hl))
+                                                     (medley/indexed highlights))]
+                              (set-highlights! (assoc-in highlights [target-idx] hl))
+                              (pdf-assets/update-hl-block! hl))))
+
+        del-hl!         (fn [hl] (when-let [id (:id hl)] (set-highlights! (into [] (remove #(= id (:id %)) highlights)))))]
 
     ;; consume dirtied
     (rum/use-effect!
@@ -622,8 +630,8 @@
            ;; show ctx menu
            (js/setTimeout (fn []
                             (set-ctx-menu-state! {:highlight hl-fn
-                                             :selection selection
-                                             :point     point})))) 0))
+                                                  :selection selection
+                                                  :point     point})))) 0))
 
      [(:range sel-state)])
 
@@ -640,7 +648,7 @@
                (rum/mount
                 (pdf-highlights-region-container
                  viewer page-hls {:show-ctx-menu! show-ctx-menu!
-                                  :upd-hl!       upd-hl!})
+                                  :upd-hl!        upd-hl!})
 
                 hls-layer)))))
 
@@ -655,9 +663,9 @@
        (js/ReactDOM.createPortal
         (pdf-highlights-ctx-menu viewer ctx-menu-state
                                  {:clear-ctx-menu! clear-ctx-menu!
-                                  :add-hl!        add-hl!
-                                  :del-hl!        del-hl!
-                                  :upd-hl!        upd-hl!})
+                                  :add-hl!         add-hl!
+                                  :del-hl!         del-hl!
+                                  :upd-hl!         upd-hl!})
 
         (.querySelector el ".pp-holder")))
 
@@ -671,8 +679,7 @@
      ;;       (str "#" (:id hl) "#  ")]
      ;;      (:text (:content hl))])
      ;;   ])
-     ;; refs
-     (pdf-highlight-finder viewer)
+
      (pdf-page-finder viewer)
 
      ;; area selection container
@@ -680,11 +687,11 @@
       viewer
       {:clear-ctx-menu! clear-ctx-menu!
        :show-ctx-menu!  show-ctx-menu!
-       :add-hl!        add-hl!
+       :add-hl!         add-hl!
        })]))
 
 (rum/defc pdf-viewer
-  [_url initial-hls initial-page ^js pdf-document ops]
+  [_url ^js pdf-document {:keys [initial-hls initial-page initial-error]} ops]
 
   (let [*el-ref (rum/create-ref)
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
@@ -765,7 +772,10 @@
         [:div.pdfViewer "viewer pdf"]
         [:div.pp-holder]
 
-        (when (and page-ready? viewer)
+        ;; block hls refs
+        (pdf-highlight-finder viewer)
+
+        (when (and page-ready? viewer (not initial-error))
           [(rum/with-key
             (pdf-highlights
              (:el state) viewer
@@ -780,9 +790,9 @@
   [{:keys [url hls-file] :as pdf-current}]
   (let [*doc-ref       (rum/use-ref nil)
         [loader-state, set-loader-state!] (rum/use-state {:error nil :pdf-document nil :status nil})
-        [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil :extra nil :loaded false})
+        [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil :extra nil :loaded false :error nil})
         [initial-page, set-initial-page!] (rum/use-state 1)
-        set-dirty-hls! (fn [latest-hls]  ;; TODO: incremental
+        set-dirty-hls! (fn [latest-hls]                     ;; TODO: incremental
                          (set-hls-state! #(merge % {:initial-hls [] :latest-hls latest-hls})))
         set-hls-extra! (fn [extra]
                          (set-hls-state! #(merge % {:extra extra})))]
@@ -793,13 +803,19 @@
        (p/catch
         (p/let [data (pdf-assets/load-hls-data$ pdf-current)
                 {:keys [highlights extra]} data]
-          (set-initial-page! (or (util/safe-parse-int (:page extra)) 1))
+          (set-initial-page! (or (when-let [page (:page extra)]
+                                   (util/safe-parse-int page)) 1))
           (set-hls-state! {:initial-hls highlights :latest-hls highlights :extra extra :loaded true}))
 
         ;; error
-        (fn [e]
+        (fn [^js e]
           (js/console.error "[load hls error]" e)
-          (set-hls-state! {:initial-hls [] :loaded true})))
+
+          (let [msg (str (util/format "Error: failed to load the highlights file: \"%s\". \n"
+                                      (:hls-file pdf-current))
+                         e)]
+            (notification/show! msg :error)
+            (set-hls-state! {:loaded true :error e}))))
 
        ;; cancel
        #())
@@ -810,8 +826,9 @@
      (fn []
        (when (= :completed (:status loader-state))
          (p/catch
-          (pdf-assets/persist-hls-data$
-           pdf-current (:latest-hls hls-state) (:extra hls-state))
+          (when-not (:error hls-state)
+            (pdf-assets/persist-hls-data$
+             pdf-current (:latest-hls hls-state) (:extra hls-state)))
 
           ;; write hls file error
           (fn [e]
@@ -872,8 +889,9 @@
     (rum/bind-context
      [*highlights-ctx* hls-state]
      [:div.extensions__pdf-loader {:ref *doc-ref}
-      (let [status-doc  (:status loader-state)
-            initial-hls (:initial-hls hls-state)]
+      (let [status-doc    (:status loader-state)
+            initial-hls   (:initial-hls hls-state)
+            initial-error (:error hls-state)]
 
         (if (= status-doc :loading)
 
@@ -882,7 +900,10 @@
 
           (when-let [pdf-document (and (:loaded hls-state) (:pdf-document loader-state))]
             [(rum/with-key (pdf-viewer
-                            url initial-hls initial-page pdf-document
+                            url pdf-document
+                            {:initial-hls   initial-hls
+                             :initial-page  initial-page
+                             :initial-error initial-error}
                             {:set-dirty-hls! set-dirty-hls!
                              :set-hls-extra! set-hls-extra!}) "pdf-viewer")])))])))
 

+ 9 - 3
src/main/frontend/extensions/srs.cljs

@@ -315,9 +315,15 @@
          (satisfies? ICard card)]}
   (let [block (.-block card)
         props (get-block-card-properties block)
-        last-interval (or (util/safe-parse-float (get props card-last-interval-property)) 0)
-        repeats (or (util/safe-parse-int (get props card-repeats-property)) 0)
-        last-ef (or (util/safe-parse-float (get props card-last-easiness-factor-property)) 2.5)
+        last-interval (or
+                       (when-let [v (get props card-last-interval-property)]
+                         (util/safe-parse-float v))
+                       0)
+        repeats (or (when-let [v (get props card-repeats-property)]
+                      (util/safe-parse-int v))
+                    0)
+        last-ef (or (when-let [v (get props card-last-easiness-factor-property)]
+                      (util/safe-parse-float v)) 2.5)
         [next-interval next-repeats next-ef of-matrix*]
         (next-interval last-interval repeats last-ef score @of-matrix)
         next-interval* (if (< next-interval 0) 0 next-interval)

+ 84 - 74
src/main/frontend/handler/editor.cljs

@@ -131,7 +131,7 @@
      (let [{:keys [selection-start selection-end format selection value edit-id input]} m
            cur-pos (cursor/pos input)
            empty-selection? (= selection-start selection-end)
-           selection-link? (and selection (gp-util/url? selection))
+           selection-link? (and selection (gp-mldoc/mldoc-link? format selection))
            [content forward-pos] (cond
                                    empty-selection?
                                    (config/get-empty-link-and-forward-pos format)
@@ -311,7 +311,7 @@
         block (merge block
                      (block/parse-title-and-body uuid format pre-block? (:block/content block)))
         properties (:block/properties block)
-        properties (if (and (= format :markdown) 
+        properties (if (and (= format :markdown)
                             (number? (:heading properties)))
                      (dissoc properties :heading)
                      properties)
@@ -1810,6 +1810,10 @@
                  (state/set-editor-op! nil)))
              500))))
 
+(defn- start-of-new-word?
+  [input pos]
+  (contains? #{" " "\t"} (get (.-value input) (- pos 2))))
+
 (defn handle-last-input []
   (let [input           (state/get-input)
         pos             (cursor/pos input)
@@ -1820,9 +1824,8 @@
     ;; TODO: is it cross-browser compatible?
     ;; (not= (gobj/get native-e "inputType") "insertFromPaste")
     (cond
-      ;; By default, "/" is also used as namespace separator in Logseq.
       (and (= last-input-char (state/get-editor-command-trigger))
-           (not (contains? #{:page-search-hashtag} (state/sub :editor/action))))
+           (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos)))
       (do
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
         (commands/reinit-matched-commands!)
@@ -1855,8 +1858,8 @@
 
       ;; Open "Search page or New page" auto-complete
       (and (= last-input-char commands/hashtag)
-           ;; Only trigger at beginning of line or before whitespace
-           (or (= 1 pos) (contains? #{" " "\t"} (get (.-value input) (- pos 2)))))
+           ;; Only trigger at beginning of a line or before whitespace
+           (or (re-find #"(?m)^#" (str (.-value input))) (start-of-new-word? input pos)))
       (do
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
         (state/set-editor-last-pos! pos)
@@ -2807,7 +2810,68 @@
         :else
         nil))))
 
-(defn ^:large-vars/cleanup-todo keyup-handler
+(defn- default-case-for-keyup-handler
+  [input current-pos k code is-processed? c]
+  (let [last-key-code (state/get-last-key-code)
+        blank-selected? (string/blank? (util/get-selected-text))
+        non-enter-processed? (and is-processed? ;; #3251
+                                  (not= code keycode/enter-code))  ;; #3459
+        editor-action (state/get-editor-action)]
+    (when (and (not editor-action) (not non-enter-processed?))
+      (cond
+        ;; When you type text inside square brackets
+        (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp" "Escape"} k))
+             (wrapped-by? input page-ref/left-brackets page-ref/right-brackets))
+        (let [orig-pos (cursor/get-caret-pos input)
+              value (gobj/get input "value")
+              square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) page-ref/left-brackets)
+              pos (+ square-pos 2)
+              _ (state/set-editor-last-pos! pos)
+              pos (assoc orig-pos :pos pos)
+              command-step (if (= \# (util/nth-safe value (dec square-pos)))
+                             :editor/search-page-hashtag
+                             :editor/search-page)]
+          (commands/handle-step [command-step])
+          (state/set-editor-action-data! {:pos pos}))
+
+        ;; Handle non-ascii square brackets
+        (and blank-selected?
+             (contains? keycode/left-square-brackets-keys k)
+             (= (:key last-key-code) k)
+             (> current-pos 0)
+             (not (wrapped-by? input page-ref/left-brackets page-ref/right-brackets)))
+        (do
+          (commands/handle-step [:editor/input page-ref/left-and-right-brackets {:backward-truncate-number 2
+                                                                                 :backward-pos 2}])
+          (commands/handle-step [:editor/search-page])
+          (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
+
+        ;; Handle non-ascii parentheses
+        (and blank-selected?
+             (contains? keycode/left-paren-keys k)
+             (= (:key last-key-code) k)
+             (> current-pos 0)
+             (not (wrapped-by? input block-ref/left-parens block-ref/right-parens)))
+        (do
+          (commands/handle-step [:editor/input block-ref/left-and-right-parens {:backward-truncate-number 2
+                                                                                :backward-pos 2}])
+          (commands/handle-step [:editor/search-block :reference])
+          (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
+
+        ;; Handle non-ascii angle brackets
+        (and (= "〈" c)
+             (= "《" (util/nth-safe (gobj/get input "value") (dec (dec current-pos))))
+             (> current-pos 0))
+        (do
+          (commands/handle-step [:editor/input commands/angle-bracket {:last-pattern "《〈"
+                                                                       :backward-pos 0}])
+          (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+          (state/set-editor-show-block-commands!))
+
+        :else
+        nil))))
+
+(defn keyup-handler
   [_state input input-id]
   (fn [e key-code]
     (when-not (util/event-is-composing? e)
@@ -2836,24 +2900,22 @@
                (if (mobile-util/native-android?)
                  (gobj/get e "key")
                  (gobj/getValueByKeys e "event_" "code"))
-               (util/event-is-composing? e true)]) ;; #3440
-            format (:format (get-state))
-            last-key-code (state/get-last-key-code)
-            blank-selected? (string/blank? (util/get-selected-text))
-            non-enter-processed? (and is-processed? ;; #3251
-                                      (not= code keycode/enter-code))  ;; #3459
-            editor-action (state/get-editor-action)]
+                ;; #3440
+               (util/event-is-composing? e true)])]
         (cond
           ;; When you type something after /
           (and (= :commands (state/get-editor-action)) (not= k (state/get-editor-command-trigger)))
-          (let [matched-commands (get-matched-commands input)]
-            (if (seq matched-commands)
-              (reset! commands/*matched-commands matched-commands)
-              (state/clear-editor-action!)))
+          (if (= (state/get-editor-command-trigger) (second (re-find #"(\S+)\s+$" value)))
+            (state/clear-editor-action!)
+            (let [matched-commands (get-matched-commands input)]
+              (if (seq matched-commands)
+                (reset! commands/*matched-commands matched-commands)
+                (state/clear-editor-action!))))
 
           ;; When you type search text after < (and when you release shift after typing <)
-          (and (= :block-commands editor-action) (not= key-code 188)) ; not <
-          (let [matched-block-commands (get-matched-block-commands input)]
+          (and (= :block-commands (state/get-editor-action)) (not= key-code 188)) ; not <
+          (let [matched-block-commands (get-matched-block-commands input)
+                format (:format (get-state))]
             (if (seq matched-block-commands)
               (cond
                 (= key-code 9)          ;tab
@@ -2870,7 +2932,7 @@
               (state/clear-editor-action!)))
 
           ;; When you type two spaces after a command character (may always just be handled by the above instead?)
-          (and (contains? #{:commands :block-commands} (state/get-editor-action))
+          (and (contains? #{:block-commands} (state/get-editor-action))
                (= c (util/nth-safe value (dec (dec current-pos))) " "))
           (state/clear-editor-action!)
 
@@ -2880,59 +2942,7 @@
           (state/clear-editor-action!)
 
           :else
-          (when (and (not editor-action) (not non-enter-processed?))
-            (cond
-              ;; When you type text inside square brackets
-              (and (not (contains? #{"ArrowDown" "ArrowLeft" "ArrowRight" "ArrowUp" "Escape"} k))
-                   (wrapped-by? input page-ref/left-brackets page-ref/right-brackets))
-              (let [orig-pos (cursor/get-caret-pos input)
-                    value (gobj/get input "value")
-                    square-pos (string/last-index-of (subs value 0 (:pos orig-pos)) page-ref/left-brackets)
-                    pos (+ square-pos 2)
-                    _ (state/set-editor-last-pos! pos)
-                    pos (assoc orig-pos :pos pos)
-                    command-step (if (= \# (util/nth-safe value (dec square-pos)))
-                                   :editor/search-page-hashtag
-                                   :editor/search-page)]
-                (commands/handle-step [command-step])
-                (state/set-editor-action-data! {:pos pos}))
-
-              ;; Handle non-ascii square brackets
-              (and blank-selected?
-                   (contains? keycode/left-square-brackets-keys k)
-                   (= (:key last-key-code) k)
-                   (> current-pos 0)
-                   (not (wrapped-by? input page-ref/left-brackets page-ref/right-brackets)))
-              (do
-                (commands/handle-step [:editor/input page-ref/left-and-right-brackets {:backward-truncate-number 2
-                                                             :backward-pos 2}])
-                (commands/handle-step [:editor/search-page])
-                (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
-
-              ;; Handle non-ascii parentheses
-              (and blank-selected?
-                   (contains? keycode/left-paren-keys k)
-                   (= (:key last-key-code) k)
-                   (> current-pos 0)
-                   (not (wrapped-by? input block-ref/left-parens block-ref/right-parens)))
-              (do
-                (commands/handle-step [:editor/input block-ref/left-and-right-parens {:backward-truncate-number 2
-                                                             :backward-pos 2}])
-                (commands/handle-step [:editor/search-block :reference])
-                (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)}))
-
-              ;; Handle non-ascii angle brackets
-              (and (= "〈" c)
-                   (= "《" (util/nth-safe value (dec (dec current-pos))))
-                   (> current-pos 0))
-              (do
-                (commands/handle-step [:editor/input commands/angle-bracket {:last-pattern "《〈"
-                                                                             :backward-pos 0}])
-                (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
-                (state/set-editor-show-block-commands!))
-
-              :else
-              nil)))
+          (default-case-for-keyup-handler input current-pos k code is-processed? c))
 
         (close-autocomplete-if-outside input)
 

+ 55 - 60
src/main/frontend/handler/file.cljs

@@ -21,6 +21,7 @@
             [promesa.core :as p]
             [frontend.mobile.util :as mobile-util]
             [logseq.graph-parser.config :as gp-config]
+            [clojure.string :as string]
             ["path" :as path]))
 
 ;; TODO: extract all git ops using a channel
@@ -104,8 +105,7 @@
     :else
     true))
 
-(defn- validate-and-write-file
-  "Validates and if valid writes file. Returns boolean indicating if file content was valid"
+(defn- write-file-aux!
   [repo path content write-file-options]
   (let [original-content (db/get-file repo path)
         path-dir (if (and
@@ -117,12 +117,7 @@
                    (config/get-repo-dir repo))
         write-file-options' (merge write-file-options
                                    (when original-content {:old-content original-content}))]
-    (p/do!
-     (if (validate-file repo path content)
-       (do
-         (fs/write-file! repo path-dir path content write-file-options')
-         true)
-       false))))
+    (fs/write-file! repo path-dir path content write-file-options')))
 
 ;; TODO: Remove this function in favor of `alter-files`
 (defn alter-file
@@ -133,58 +128,58 @@
                            from-disk? false
                            skip-compare? false}}]
   (let [path (gp-util/path-normalize path)
-        write-file! (if from-disk?
-                      #(p/promise (validate-file repo path content))
-                      #(validate-and-write-file repo path content {:skip-compare? skip-compare?}))
-        opts {:new-graph? new-graph?
-              :from-disk? from-disk?
-              :skip-db-transact? skip-db-transact?}
-        result (if reset?
-                 (do
-                   (when-not skip-db-transact?
-                     (when-let [page-id (db/get-file-page-id path)]
-                       (db/transact! repo
-                                     [[:db/retract page-id :block/alias]
-                                      [:db/retract page-id :block/tags]]
-                                     opts)))
-                   (file-common-handler/reset-file! repo path content (merge opts
-                                                                             (when (some? verbose) {:verbose verbose}))))
-                 (db/set-file-content! repo path content opts))]
-    (util/p-handle (write-file!)
-                   (fn [valid-result?]
-                     (when re-render-root? (ui-handler/re-render-root!))
-
-                     (cond
-                       (= path (config/get-custom-css-path repo))
-                       (ui-handler/add-style-if-exists!)
-
-                       (and (= path (config/get-repo-config-path repo))
-                            valid-result?)
-                       (p/let [_ (repo-config-handler/restore-repo-config! repo content)]
-                              (state/pub-event! [:shortcut/refresh]))
-
-                       (and (config/global-config-enabled?)
-                            (= path (global-config-handler/global-config-path))
-                            valid-result?)
-                       (p/let [_ (global-config-handler/restore-global-config!)]
-                              (state/pub-event! [:shortcut/refresh]))))
-                   (fn [error]
-                     (when (and (config/global-config-enabled?)
-                                ;; Global-config not started correctly but don't
-                                ;; know root cause yet
-                                ;; https://sentry.io/organizations/logseq/issues/3587411237/events/4b5da8b8e58b4f929bd9e43562213d32/events/?cursor=0%3A0%3A1&project=5311485&statsPeriod=14d
-                                (global-config-handler/global-config-dir-exists?)
-                                (= path (global-config-handler/global-config-path)))
-                       (state/pub-event! [:notification/show
-                                          {:content (str "Failed to write to file " path)
-                                           :status :error}]))
-
-                     (println "Write file failed, path: " path ", content: " content)
-                     (log/error :write/failed error)
-                     (state/pub-event! [:capture-error
-                                        {:error error
-                                         :payload {:type :write-file/failed-for-alter-file}}])))
-    result))
+        config-file? (string/ends-with? path config/config-file)
+        config-valid? (and config-file? (validate-file repo path content))]
+    (when-not (and config-file? (not config-valid?)) ; non-config file or valid config
+      (let [opts {:new-graph? new-graph?
+                  :from-disk? from-disk?
+                  :skip-db-transact? skip-db-transact?}
+            result (if reset?
+                     (do
+                       (when-not skip-db-transact?
+                         (when-let [page-id (db/get-file-page-id path)]
+                           (db/transact! repo
+                             [[:db/retract page-id :block/alias]
+                              [:db/retract page-id :block/tags]]
+                             opts)))
+                       (file-common-handler/reset-file!
+                        repo path content (merge opts
+                                                 (when (some? verbose) {:verbose verbose}))))
+                     (db/set-file-content! repo path content opts))]
+        (-> (p/let [_ (when-not from-disk?
+                        (write-file-aux! repo path content {:skip-compare? skip-compare?}))]
+              (when re-render-root? (ui-handler/re-render-root!))
+
+              (cond
+                (= path (config/get-custom-css-path repo))
+                (ui-handler/add-style-if-exists!)
+
+                (= path (config/get-repo-config-path repo))
+                (p/let [_ (repo-config-handler/restore-repo-config! repo content)]
+                  (state/pub-event! [:shortcut/refresh]))
+
+                (and (config/global-config-enabled?)
+                     (= path (global-config-handler/global-config-path)))
+                (p/let [_ (global-config-handler/restore-global-config!)]
+                  (state/pub-event! [:shortcut/refresh]))))
+            (p/catch
+                (fn [error]
+                  (when (and (config/global-config-enabled?)
+                             ;; Global-config not started correctly but don't
+                             ;; know root cause yet
+                             ;; https://sentry.io/organizations/logseq/issues/3587411237/events/4b5da8b8e58b4f929bd9e43562213d32/events/?cursor=0%3A0%3A1&project=5311485&statsPeriod=14d
+                             (global-config-handler/global-config-dir-exists?)
+                             (= path (global-config-handler/global-config-path)))
+                    (state/pub-event! [:notification/show
+                                       {:content (str "Failed to write to file " path)
+                                        :status :error}]))
+
+                  (println "Write file failed, path: " path ", content: " content)
+                  (log/error :write/failed error)
+                  (state/pub-event! [:capture-error
+                                     {:error error
+                                      :payload {:type :write-file/failed-for-alter-file}}]))))
+        result))))
 
 (defn set-file-content!
   [repo path new-content]

+ 1 - 1
src/main/frontend/handler/plugin.cljs

@@ -177,7 +177,7 @@
 
 (defn get-enabled-plugins-if-setting-schema
   []
-  (when-let [plugins (seq (state/get-enabled?-installed-plugins false nil true true))]
+  (when-let [plugins (seq (state/get-enabled?-installed-plugins false true true true))]
     (filter #(has-setting-schema? (:id %)) plugins)))
 
 (defn setup-install-listener!

+ 1 - 0
src/main/frontend/handler/user.cljs

@@ -186,6 +186,7 @@
 
 (defn logout []
   (clear-tokens)
+  (state/clear-user-info!)
   (state/pub-event! [:user/logout]))
 
 (defn <ensure-id&access-token

+ 11 - 11
src/main/frontend/mobile/index.css

@@ -76,18 +76,18 @@
 
   button {
     @apply flex items-center py-2 px-2;
+  }
 
-    .submenu {
-      z-index: 100;
-      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.02);
-      background-color: var(--ls-secondary-background-color);
-      overflow-x: overlay;
-      overflow-y: hidden;
-      height: 40px;
-    }
+  .submenu {
+    @apply fixed left-0 bottom-0 w-full flex-row justify-evenly items-center z-10 bg-base-2;
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.02);
+    overflow-x: overlay;
+    overflow-y: hidden;
+    height: 40px;
+    display: none;
 
-    .show-submenu {
-      display: block;
+    &.show-submenu {
+      display: flex;
     }
   }
 
@@ -245,4 +245,4 @@ html.is-zoomed-native-ios {
       }
     }
   }
-}
+}

+ 41 - 4
src/main/frontend/mobile/intent.cljs

@@ -13,16 +13,53 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             [frontend.util :as util]
-            [lambdaisland.glogi :as log]
-            [logseq.graph-parser.util :as gp-util]
             [frontend.util.fs :as fs-util]
+            [goog.string :as gstring]
+            [lambdaisland.glogi :as log]
             [logseq.graph-parser.config :as gp-config]
+            [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util.page-ref :as page-ref]
             [promesa.core :as p]))
 
+(defn- is-link
+  [url]
+  (when (not-empty url)
+    (re-matches #"^[a-zA-Z0-9]+://.*$" url)))
+
+(defn- extract-highlight
+  "Extract highlighted text and url from mobile browser intent share.
+   - url can be prefixed with the highlighted text.
+   - url can be highlighted text only in some cases."
+  [url]
+  (let [[_ link] (re-find #"\s+([a-zA-Z0-9]+://[\S]*)$" url)
+        highlight (when (not-empty link)
+                    (let [quoted (string/replace url link "")
+                          quoted (gstring/trimRight quoted)]
+                      (gstring/stripQuotes quoted "\"")))]
+    (cond
+      (not-empty highlight)
+      [highlight link]
+
+      (is-link url)
+      [nil url]
+
+      :else
+      [url nil])))
+
+(defn- transform-args
+  [args]
+  (let [{:keys [url]} args]
+    (if (is-link url)
+      args
+      (let [[highlight url'] (extract-highlight url)]
+        (assoc args :url url' :content highlight)))))
+
 (defn- handle-received-text [args]
-  ;; {:title :type :url}
-  (state/pub-event! [:editor/quick-capture args]))
+  ;; Keys: :title :type :url
+  ;; :content is added if there's highlighted text
+  (let [args (transform-args args)]
+    (state/pub-event! [:editor/quick-capture args])))
+
 
 (defn- embed-asset-file [url format]
   (p/let [basename (path/basename url)

+ 3 - 3
src/main/frontend/mobile/mobile_bar.cljs

@@ -36,7 +36,7 @@
   [parent-id]
   (let [callback (fn [event]
                    (util/stop event)
-                   (let [target (.-parentNode (.-target event))]
+                   (let [target (gdom/getElement "mobile-toolbar-timestamp-submenu")]
                      (dom/remove-class! target "show-submenu")))
         command-cp (fn [action description]
                      [:button
@@ -48,10 +48,10 @@
      [:button.bottom-action
       {:on-mouse-down (fn [event]
                         (util/stop event)
-                        (let [target (gdom/getNextElementSibling (gdom/getParentElement (.-target event)))]
+                        (let [target (gdom/getElement "mobile-toolbar-timestamp-submenu")]
                           (dom/add-class! target "show-submenu")))}
       (ui/icon "calendar" {:size ui/icon-size})]
-     [:div.submenu.fixed.left-0.bottom-0.hidden.w-full.flex-row.justify-evenly.items-center
+     [:div#mobile-toolbar-timestamp-submenu.submenu
       {:style {:bottom @util/keyboard-height}}
       (command-cp #(let [today (page-handler/get-page-ref-text (date/today))]
                      (commands/simple-insert! parent-id today {}))

+ 30 - 22
src/main/frontend/quick_capture.cljs

@@ -11,18 +11,10 @@
             [frontend.util :as util]
             [frontend.util.text :as text-util]))
 
-
-(defn- extract-highlight
-  "Extract highlighted text and url from mobile browser intent share"
-  [url]
-  (let [[_ highlight link] (re-matches #"(?s)\"(.*)\"\s+([a-z0-9]+://.*)$" url)]
-    (if (not-empty highlight)
-      [highlight link]
-      [nil url])))
-
 (defn- is-tweet-link
   [url]
-  (re-matches #"^https://twitter\.com/.*?/status/.*?$" url))
+  (when (not-empty url)
+    (re-matches #"^https://twitter\.com/.*?/status/.*?$" url)))
 
 (defn quick-capture [args]
   (let [{:keys [url title content page append]} (bean/->clj args)
@@ -32,22 +24,36 @@
         redirect-page? (get-in (state/get-config)
                                [:quick-capture-options :redirect-page?]
                                false)
-        today-page (when (state/enable-journals?)
-                     (string/lower-case (date/today)))
-        page (if (or (= page "TODAY")
-                     (and (string/blank? page) insert-today?))
+        today-page (string/lower-case (date/today))
+        current-page (state/get-current-page) ;; empty when in journals page
+        default-page (get-in (state/get-config)
+                             [:quick-capture-options :default-page])
+        page (cond
+               (and (state/enable-journals?)
+                    (or (= page "TODAY")
+                        (and (string/blank? page) insert-today?)))
                today-page
-               (or (not-empty page)
-                   (state/get-current-page)
-                   today-page))
-        [content url] (if (string/blank? content)
-                        (extract-highlight url)
-                        [content url])
-        page (or page "quick capture") ;; default to "quick capture" page, if journals are not enabled
+
+               (not-empty page)
+               page
+
+               (not-empty default-page)
+               default-page
+
+               (not-empty current-page)
+               current-page
+
+               :else
+               (if (state/enable-journals?) ;; default to "quick capture" page if journals are not enabled
+                 today-page
+                 "quick capture"))
         format (db/get-page-format page)
         time (date/get-current-time)
         text (or (and content (not-empty (string/trim content))) "")
         link (cond
+               (string/blank? url)
+               title
+
                (boolean (text-util/get-matched-video url))
                (str title " {{video " url "}}")
 
@@ -62,8 +68,10 @@
         template (get-in (state/get-config)
                          [:quick-capture-templates :text]
                          "**{time}** [[quick capture]]: {text} {url}")
+        date-ref-name (date/today)
         content (-> template
                     (string/replace "{time}" time)
+                    (string/replace "{date}" date-ref-name)
                     (string/replace "{url}" link)
                     (string/replace "{text}" text))
         edit-content (state/get-edit-content)
@@ -83,4 +91,4 @@
         (js/setTimeout #(editor-handler/api-insert-new-block! content {:page page
                                                                        :edit-block? true
                                                                        :replace-empty-target? true})
-                       100)))))
+                       100)))))

+ 9 - 4
src/main/frontend/state.cljs

@@ -324,12 +324,13 @@
   "Merges user configs in given orders. All values are overridden except for maps
   which are merged."
   [& configs]
-  (apply merge-with
+  (->> configs
+       (filter map?)
+       (apply merge-with
          (fn merge-config [current new]
            (if (and (map? current) (map? new))
              (merge current new)
-             new))
-         configs))
+             new)))))
 
 (defn get-config
   "User config for the given repo or current repo if none given. All config fetching
@@ -596,7 +597,7 @@ Similar to re-frame subscriptions"
    (enable-whiteboards? (get-current-repo)))
   ([repo]
    (and
-    ((resolve 'frontend.handler.user/alpha-or-beta-user?)) ;; using resolve to avoid circular dependency
+    ((resolve 'frontend.handler.user/feature-available?) :whiteboard) ;; using resolve to avoid circular dependency
     (:feature/enable-whiteboards? (sub-config repo)))))
 
 (defn export-heading-to-list?
@@ -2113,3 +2114,7 @@ Similar to re-frame subscriptions"
     (let [groups (:UserGroups info)]
       (when (seq groups)
         (storage/set :user-groups groups)))))
+
+(defn clear-user-info!
+  []
+  (storage/remove :user-groups))

+ 5 - 4
src/main/frontend/util/text.cljs

@@ -14,10 +14,11 @@
 
 (defn get-matched-video
   [url]
-  (or (re-find youtube-regex url)
-      (re-find loom-regex url)
-      (re-find vimeo-regex url)
-      (re-find bilibili-regex url)))
+  (when (not-empty url)
+    (or (re-find youtube-regex url)
+        (re-find loom-regex url)
+        (re-find vimeo-regex url)
+        (re-find bilibili-regex url))))
 
 (defn build-data-value
   [col]

+ 1 - 1
src/main/frontend/version.cljs

@@ -1,3 +1,3 @@
 (ns ^:no-doc frontend.version)
 
-(defonce version "0.8.16")
+(defonce version "0.8.17")

+ 63 - 1
src/test/frontend/handler/editor_test.cljs

@@ -66,8 +66,49 @@
     "TODO" "## TODO content" "## DOING content"
     "DONE" "DONE content" "content"))
 
+(defn- keyup-handler
+  "Spied version of editor/keyup-handler"
+  [{:keys [value cursor-pos action commands]
+    ;; Default to some commands matching which matches default behavior for most
+    ;; completion scenarios
+    :or {commands [:fake-command]}}]
+  ;; Reset editor action in order to test result
+  (state/set-editor-action! action)
+  ;; Default cursor pos to end of line
+  (let [pos (or cursor-pos (count value))
+        input #js {:value value}]
+    (with-redefs [editor/get-matched-commands (constantly commands)
+                  cursor/pos (constantly pos)]
+      ((editor/keyup-handler nil input nil)
+       #js {:key (subs value (dec (count value)))}
+       nil))))
+
+(deftest keyup-handler-test
+  (testing "Command completion"
+    (keyup-handler {:value "/b"
+                    :action :commands
+                    :commands [:fake-command]})
+    (is (= :commands (state/get-editor-action))
+        "Completion stays open if there is a matching command")
+
+    (keyup-handler {:value "/zz"
+                    :action :commands
+                    :commands []})
+    (is (= nil (state/get-editor-action))
+        "Completion closed if there no matching commands")
+
+    (keyup-handler {:value "/ " :action :commands})
+    (is (= nil (state/get-editor-action))
+        "Completion closed after a space follows /")
+
+    (keyup-handler {:value "/block " :action :commands})
+    (is (= :commands (state/get-editor-action))
+        "Completion stays open if space is part of the search term for /"))
+  ;; Reset state
+  (state/set-editor-action! nil))
+
 (defn- handle-last-input-handler
-  "Spied version of editor/keydown-not-matched-handler"
+  "Spied version of editor/handle-last-input"
   [{:keys [value cursor-pos]}]
   ;; Reset editor action in order to test result
   (state/set-editor-action! nil)
@@ -98,6 +139,27 @@
     (is (= nil (state/get-editor-action))
         "Don't autocomplete properties if typing in a block where properties already exist"))
 
+  (testing "Command autocompletion"
+    (handle-last-input-handler {:value "/"})
+    (is (= :commands (state/get-editor-action))
+        "Command search if only / has been typed")
+
+    (handle-last-input-handler {:value "some words /"})
+    (is (= :commands (state/get-editor-action))
+        "Command search on start of new word")
+
+    (handle-last-input-handler {:value "a line\n/"})
+    (is (= :commands (state/get-editor-action))
+        "Command search on start of a new line")
+
+    (handle-last-input-handler {:value "https://"})
+    (is (= nil (state/get-editor-action))
+        "No command search in middle of a word")
+
+    (handle-last-input-handler {:value "#blah/"})
+    (is (= nil (state/get-editor-action))
+        "No command search after a tag search to allow for namespace completion"))
+
   (testing "Tag autocompletion"
     (handle-last-input-handler {:value "#"
                                 :cursor-pos 1})

+ 2 - 1
templates/config.edn

@@ -300,6 +300,7 @@
  ;; Each template contains three elements {time}, {text} and {url}, which can be auto-expanded
  ;; by received contents from other apps. Note: the {} cannot be omitted.
  ;; - {time}: capture time
+ ;; - {date}: capture date using current date format, use `[[{date}]]` to get a page reference
  ;; - {text}: text that users selected before sharing.
  ;; - {url}: url or assets path for media files stored in Logseq.
  ;; You can also reorder them, or even only use one or two of them in the template.
@@ -309,7 +310,7 @@
  ;;  :media "[[quick capture]] **{time}**: {url}"}
 
  ;; Quick capture options
- ;; :quick-capture-options {:insert-today? false :redirect-page? false}
+ ;; :quick-capture-options {:insert-today? false :redirect-page? false :default-page nil}
 
  ;; File sync options
  ;; Ignore these files when syncing, regexp is supported.