Kaynağa Gözat

Merge branch 'master' into fix/dsl-query-boolean-operators

Gabriel Horner 3 yıl önce
ebeveyn
işleme
3987395ab5

+ 1 - 1
deps.edn

@@ -26,7 +26,7 @@
   thheller/shadow-cljs                  {:mvn/version "2.17.5"}
   expound/expound                       {:mvn/version "0.8.6"}
   com.lambdaisland/glogi                {:mvn/version "1.1.144"}
-  binaryage/devtools                    {:mvn/version "1.0.2"}
+  binaryage/devtools                    {:mvn/version "1.0.5"}
   camel-snake-kebab/camel-snake-kebab   {:mvn/version "0.4.2"}
   instaparse/instaparse                 {:mvn/version "1.4.10"}
   nubank/workspaces                     {:mvn/version "1.1.1"}

+ 8 - 0
docs/dev-practices.md

@@ -99,3 +99,11 @@ appear where shadow-cljs was first invoked e.g. where `yarn watch` is running.
 Specific namespace(s) can be auto run with the `:ns-regexp` option e.g. `npx
 shadow-cljs watch test --config-merge '{:autorun true :ns-regexp
 "frontend.text-test"}'`.
+
+## Logging
+
+For logging, we use https://github.com/lambdaisland/glogi. When in development,
+be sure to have [enabled custom
+formatters](https://github.com/binaryage/cljs-devtools/blob/master/docs/installation.md#enable-custom-formatters-in-chrome)
+in the desktop app and browser. Without this enabled, most of the log messages
+aren't readable.

+ 8 - 0
ios/App/App.xcodeproj/project.pbxproj

@@ -22,6 +22,8 @@
 		D32752BE275496C60039291C /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D32752BD275496C60039291C /* CloudKit.framework */; };
 		D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A09275C92880003FBDC /* FileContainer.swift */; };
 		D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
+		FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
+		FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
@@ -47,6 +49,8 @@
 		D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
 		D3D62A0B275C928F0003FBDC /* FileContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileContainer.m; sourceTree = "<group>"; };
 		DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
+		FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
+		FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -96,6 +100,8 @@
 				2FAD9762203C412B000D30F8 /* config.xml */,
 				50B271D01FEDC1A000F3C39B /* public */,
 				7435D10B2704659F00AB88E0 /* FolderPicker.swift */,
+				FE647FF327BDFEDE00F3206B /* FsWatcher.swift */,
+				FE647FF527BDFEF500F3206B /* FsWatcher.m */,
 				7435D10E2704660B00AB88E0 /* FolderPicker.m */,
 				D3D62A09275C92880003FBDC /* FileContainer.swift */,
 				D3D62A0B275C928F0003FBDC /* FileContainer.m */,
@@ -241,11 +247,13 @@
 			files = (
 				504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
 				5FD5BB71278579F5008E6875 /* DownloadiCloudFiles.swift in Sources */,
+				FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */,
 				5FD5BB73278579FF008E6875 /* DownloadiCloudFiles.m in Sources */,
 				D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */,
 				D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */,
 				7435D10F2704660B00AB88E0 /* FolderPicker.m in Sources */,
 				7435D10C2704659F00AB88E0 /* FolderPicker.swift in Sources */,
+				FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 1 - 1
ios/App/App/DownloadiCloudFiles.m

@@ -1,5 +1,5 @@
 //
-//  DowloadiCloudFiles.m
+//  DownloadiCloudFiles.m
 //  Logseq
 //
 //  Created by leizhe on 2021/12/29.

+ 2 - 4
ios/App/App/FileContainer.swift

@@ -36,7 +36,7 @@ public class FileContainer: CAPPlugin, UIDocumentPickerDelegate {
         guard let filename = self.containerUrl?.appendingPathComponent(".logseq") else {
             return
         }
-        
+
         if !FileManager.default.fileExists(atPath: filename.path) {
             do {
                 try str.write(to: filename, atomically: true, encoding:  String.Encoding.utf8)
@@ -45,8 +45,6 @@ public class FileContainer: CAPPlugin, UIDocumentPickerDelegate {
                 // failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
             }
         }
-        self._call?.resolve([
-            "path": self.containerUrl?.path
-                            ])
+        self._call?.resolve(["path": self.containerUrl?.path as Any])
     }
 }

+ 13 - 0
ios/App/App/FsWatcher.m

@@ -0,0 +1,13 @@
+//
+//  FsWatcher.m
+//  Logseq
+//
+//  Created by Mono Wang on 2/17/R4.
+//
+
+#import <Capacitor/Capacitor.h>
+
+CAP_PLUGIN(FsWatcher, "FsWatcher",
+           CAP_PLUGIN_METHOD(watch, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(unwatch, CAPPluginReturnPromise);
+)

+ 222 - 0
ios/App/App/FsWatcher.swift

@@ -0,0 +1,222 @@
+//
+//  FsWatcher.swift
+//  Logseq
+//
+//  Created by Mono Wang on 2/17/R4.
+//
+
+import Foundation
+import Capacitor
+
+// MARK: Watcher Plugin
+
+@objc(FsWatcher)
+public class FsWatcher: CAPPlugin, PollingWatcherDelegate {
+    private var watcher: PollingWatcher? = nil
+    private var baseUrl: URL? = nil
+    
+    override public func load() {
+        print("debug FsWatcher iOS plugin loaded!")
+    }
+    
+    @objc func watch(_ call: CAPPluginCall) {
+        if let path = call.getString("path") {
+            guard let url = URL(string: path) else {
+                call.reject("can not parse url")
+                return
+            }
+            self.baseUrl = url
+            self.watcher = PollingWatcher(at: url)
+            self.watcher?.delegate = self
+            
+            call.resolve(["ok": true])
+            
+        } else {
+            call.reject("missing path string parameter")
+        }
+    }
+    
+    @objc func unwatch(_ call: CAPPluginCall) {
+        watcher?.stop()
+        watcher = nil
+        baseUrl = nil
+        
+        call.resolve()
+    }
+    
+    public func recevedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?) {
+        // NOTE: Event in js {dir path content stat{mtime}}
+        switch event {
+        case .Unlink:
+            self.notifyListeners("watcher", data: ["event": "unlink",
+                                                   "dir": baseUrl?.description as Any,
+                                                   "path": url.description,
+                                                  ])
+        case .Add:
+            let content = try? String(contentsOf: url, encoding: .utf8)
+            self.notifyListeners("watcher", data: ["event": "add",
+                                                   "dir": baseUrl?.description as Any,
+                                                   "path": url.description,
+                                                   "content": content as Any,
+                                                   "stat": ["mtime": metadata?.contentModificationTimestamp,
+                                                            "ctime": metadata?.creationTimestamp]
+                                                  ])
+        case .Change:
+            let content = try? String(contentsOf: url, encoding: .utf8)
+            self.notifyListeners("watcher", data: ["event": "change",
+                                                   "dir": baseUrl?.description as Any,
+                                                   "path": url.description,
+                                                   "content": content as Any,
+                                                   "stat": ["mtime": metadata?.contentModificationTimestamp,
+                                                            "ctime": metadata?.creationTimestamp]])
+        case .Error:
+            // TODO: handle error?
+            break
+        }
+    }
+}
+
+// MARK: URL extension
+
+extension URL {
+    func isSkipped() -> Bool {
+        // skip hidden file
+        if self.lastPathComponent.starts(with: ".") {
+            return true
+        }
+        let allowedPathExtensions: Set = ["md", "markdown", "org", "css", "edn", "excalidraw"]
+        if allowedPathExtensions.contains(self.pathExtension.lowercased()) {
+            return false
+        }
+        // skip for other file types
+        return true
+    }
+    
+    func isICloudPlaceholder() -> Bool {
+        if self.lastPathComponent.starts(with: ".") && self.pathExtension.lowercased() == "icloud" {
+            return true
+        }
+        return false
+    }
+}
+
+// MARK: PollingWatcher
+
+public protocol PollingWatcherDelegate {
+    func recevedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?)
+}
+
+public enum PollingWatcherEvent: String {
+    case Add
+    case Change
+    case Unlink
+    case Error
+}
+
+public struct SimpleFileMetadata: CustomStringConvertible, Equatable {
+    var contentModificationTimestamp: Double
+    var creationTimestamp: Double
+    var fileSize: Int
+    
+    public init?(of fileURL: URL) {
+        do {
+            let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey, .contentModificationDateKey, .creationDateKey])
+            if fileAttributes.isRegularFile! {
+                contentModificationTimestamp = fileAttributes.contentModificationDate?.timeIntervalSince1970 ?? 0.0
+                creationTimestamp = fileAttributes.creationDate?.timeIntervalSince1970 ?? 0.0
+                fileSize = fileAttributes.fileSize ?? 0
+            } else {
+                return nil
+            }
+        } catch {
+            return nil
+        }
+    }
+    
+    public var description: String {
+        return "Meta(size=\(self.fileSize), mtime=\(self.contentModificationTimestamp), ctime=\(self.creationTimestamp)"
+    }
+}
+
+public class PollingWatcher {
+    private let url: URL
+    private var timer: DispatchSourceTimer?
+    public var delegate: PollingWatcherDelegate? = nil
+    private var metaDb: [URL: SimpleFileMetadata] = [:]
+    
+    public init?(at: URL) {
+        url = at
+        
+        let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".timer")
+        timer = DispatchSource.makeTimerSource(queue: queue)
+        timer!.setEventHandler(qos: .background, flags: []) { [weak self] in
+            self?.tick()
+        }
+        timer!.schedule(deadline: .now())
+        timer!.resume()
+        
+    }
+    
+    deinit {
+        self.stop()
+    }
+    
+    public func stop() {
+        timer?.cancel()
+        timer = nil
+    }
+    
+    private func tick() {
+        let startTime = DispatchTime.now()
+        
+        if let enumerator = FileManager.default.enumerator(
+            at: url,
+            includingPropertiesForKeys: [.isRegularFileKey],
+            // NOTE: icloud downloading requires non-skipsHiddenFiles
+            options: [.skipsPackageDescendants]) {
+            
+            var newMetaDb: [URL: SimpleFileMetadata] = [:]
+            
+            for case let fileURL as URL in enumerator {
+                if !fileURL.isSkipped() {
+                    if let meta = SimpleFileMetadata(of: fileURL) {
+                        newMetaDb[fileURL] = meta
+                    }
+                } else if fileURL.isICloudPlaceholder() {
+                    try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
+                }
+            }
+            
+            self.updateMetaDb(with: newMetaDb)
+        }
+        
+        let elapsedNanoseconds = DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds
+        let elapsedInMs = Double(elapsedNanoseconds) / 1_000_000
+        print("debug ticker elapsed=\(elapsedInMs)ms")
+        
+        if #available(iOS 13.0, *) {
+            timer!.schedule(deadline: .now().advanced(by: .seconds(2)), leeway: .milliseconds(100))
+        } else {
+            // Fallback on earlier versions
+            timer!.schedule(deadline: .now() + 2.0, leeway: .milliseconds(100))
+        }
+    }
+    
+    // TODO: batch?
+    private func updateMetaDb(with newMetaDb: [URL: SimpleFileMetadata]) {
+        for (url, meta) in newMetaDb {
+            if let idx = self.metaDb.index(forKey: url) {
+                let (_, oldMeta) = self.metaDb.remove(at: idx)
+                if oldMeta != meta {
+                    self.delegate?.recevedNotification(url, .Change, meta)
+                }
+            } else {
+                self.delegate?.recevedNotification(url, .Add, meta)
+            }
+        }
+        for url in self.metaDb.keys {
+            self.delegate?.recevedNotification(url, .Unlink, nil)
+        }
+        self.metaDb = newMetaDb
+    }
+}

+ 2 - 1
package.json

@@ -96,7 +96,7 @@
         "ignore": "5.1.8",
         "is-svg": "4.3.0",
         "jszip": "3.5.0",
-        "mldoc": "1.3.1",
+        "mldoc": "1.3.2",
         "path": "0.12.7",
         "pixi-graph-fork": "0.2.0",
         "pixi.js": "6.2.0",
@@ -104,6 +104,7 @@
         "react": "17.0.2",
         "react-dom": "17.0.2",
         "react-grid-layout": "0.16.6",
+        "react-icon-base": "^2.1.2",
         "react-icons": "2.2.7",
         "react-resize-context": "3.0.0",
         "react-textarea-autosize": "8.3.3",

+ 2 - 0
shadow-cljs.edn

@@ -62,6 +62,8 @@
          :output-to       "static/tests.js"
          :closure-defines {frontend.util/NODETEST true}
          :devtools        {:enabled false}
+         ;; disable :static-fns to allow for with-redefs and repl development
+         :compiler-options {:static-fns false}
          :main            frontend.node-test-runner/main}
 
   :publishing {:target        :browser

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

@@ -554,7 +554,7 @@
                     (count (util/safe-re-find re-pattern prefix))))
             new-value (str (subs edit-content 0 pos)
                            (string/replace-first (subs edit-content pos)
-                                                 marker/marker-pattern
+                                                 (marker/marker-pattern format)
                                                  (str marker " ")))]
         (state/set-edit-content! input-id new-value)
         (let [new-pos (compute-pos-delta-when-change-marker

+ 51 - 168
src/main/frontend/components/editor.cljs

@@ -229,7 +229,32 @@
                            template)
             :class       "black"}))))))
 
-(rum/defc ^:large-vars/cleanup-todo mobile-bar < rum/reactive
+(rum/defc mobile-bar-indent-outdent [indent? icon]
+  [:div
+   [:button.bottom-action
+    {:on-mouse-down (fn [e]
+                      (util/stop e)
+                      (state/set-state! :editor/pos (cursor/pos (state/get-input)))
+                      (editor-handler/indent-outdent indent?))}
+    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
+
+(rum/defc mobile-bar-command [command-handler icon]
+  [:div
+   [:button.bottom-action
+    {:on-mouse-down (fn [e]
+                      (util/stop e)
+                      (command-handler))}
+    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
+
+(rum/defc mobile-bar-command-with-event [command-handler icon]
+  [:div
+   [:button.bottom-action
+    {:on-mouse-down (fn [e]
+                      (util/stop e)
+                      (command-handler e))}
+    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
+
+(rum/defc mobile-bar < rum/reactive
   [_parent-state parent-id]
   (let [vw-state (state/sub :ui/visual-viewport-state)
         vw-pending? (state/sub :ui/visual-viewport-pending?)
@@ -244,173 +269,31 @@
                            (:offset-top vw-state))
                         0)}
       :class (util/classnames [{:is-vw-pending (boolean vw-pending?)}])}
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (state/set-state! :editor/pos (cursor/pos (state/get-input)))
-                         (editor-handler/indent-outdent true))}
-       (ui/icon "arrow-bar-right"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (state/set-state! :editor/pos (cursor/pos (state/get-input)))
-                         (editor-handler/indent-outdent false))}
-       (ui/icon "arrow-bar-left"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         ((editor-handler/move-up-down true)))}
-       (ui/icon "arrow-bar-to-up"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         ((editor-handler/move-up-down false)))}
-       (ui/icon "arrow-bar-to-down"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (commands/simple-insert! parent-id "\n" {}))}
-       (ui/icon "arrow-back"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (editor-handler/cycle-todo!))}
-       (ui/icon "checkbox"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (viewport-fn)
-                         (editor-handler/toggle-page-reference-embed parent-id))}
-       (ui/icon "brackets"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (viewport-fn)
-                         (editor-handler/toggle-block-reference-embed parent-id))}
-       (ui/icon "parentheses"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (viewport-fn)
-                         (commands/simple-insert! parent-id "/" {}))}
-       (ui/icon "command"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (viewport-fn)
-                         (commands/simple-insert!
-                          parent-id "#"
-                          {:check-fn  (fn []
-                                        (commands/handle-step [:editor/search-page-hashtag]))}))}
-       (ui/icon "tag"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (editor-handler/cycle-priority!))}
-       (ui/icon "a-b"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (editor-handler/toggle-list!))}
-       (ui/icon "list"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (mobile-camera/embed-photo parent-id))}
-       (ui/icon "camera"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (commands/insert-youtube-timestamp))}
-       (ui/icon "brand-youtube"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (editor-handler/html-link-format!))}
-       (ui/icon "link"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (history/undo! e))}
-       (ui/icon "rotate"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (history/redo! e))}
-       (ui/icon "rotate-clockwise"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (viewport-fn)
-                         (commands/simple-insert!
-                          parent-id "<"
-                          {:check-fn (fn [_]
-                                       (commands/block-commands-map))}))}
-       (ui/icon "code"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (editor-handler/bold-format!))}
-       (ui/icon "bold"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (editor-handler/italics-format!))}
-       (ui/icon "italic"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (editor-handler/strike-through-format!))}
-       (ui/icon "strikethrough"
-                {:style {:fontSize ui/icon-size}})]]
-     [:div
-      [:button.bottom-action
-       {:on-mouse-down (fn [e]
-                         (util/stop e)
-                         (editor-handler/highlight-format!))}
-       (ui/icon "paint"
-                {:style {:fontSize ui/icon-size}})]]]))
+     [:div.toolbar-commands
+      (mobile-bar-indent-outdent true "arrow-bar-right")
+      (mobile-bar-indent-outdent false "arrow-bar-left")
+      (mobile-bar-command (editor-handler/move-up-down true) "arrow-bar-to-up")
+      (mobile-bar-command (editor-handler/move-up-down false) "arrow-bar-to-down")
+      (mobile-bar-command #(commands/simple-insert! parent-id "\n" {}) "arrow-back")
+      (mobile-bar-command editor-handler/cycle-todo! "checkbox")
+      (mobile-bar-command #(editor-handler/toggle-page-reference-embed parent-id) "brackets")
+      (mobile-bar-command #(editor-handler/toggle-block-reference-embed parent-id) "parentheses")
+      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "/" {})) "command")
+      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "#" {})) "tag")
+      (mobile-bar-command editor-handler/cycle-priority! "a-b")
+      (mobile-bar-command editor-handler/toggle-list! "list")
+      (mobile-bar-command #(mobile-camera/embed-photo parent-id) "camera")
+      (mobile-bar-command commands/insert-youtube-timestamp "brand-youtube")
+      (mobile-bar-command editor-handler/html-link-format! "link")
+      (mobile-bar-command-with-event history/undo! "rotate")
+      (mobile-bar-command-with-event history/redo! "rotate-clockwise")
+      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "<" {})) "code")
+      (mobile-bar-command editor-handler/bold-format! "bold")
+      (mobile-bar-command editor-handler/italics-format! "italic")
+      (mobile-bar-command editor-handler/strike-through-format! "strikethrough")
+      (mobile-bar-command editor-handler/highlight-format! "paint")]
+     [:div.toolbar-hide-keyboard
+      (mobile-bar-command #(state/clear-edit!) "keyboard-show")]]))
 
 (rum/defcs input < rum/reactive
   (rum/local {} ::input-value)

+ 17 - 7
src/main/frontend/components/editor.css

@@ -3,17 +3,27 @@
   bottom: 0;
   left: 0;
   width: 100%;
-  justify-content: space-between;
-  height: 2.5rem;
-  display: flex;
-  align-items: center;
+  /* height: 2.5rem; */
   z-index: 9999;
-  overflow-x: overlay;
-  overflow-y: hidden;
   transition: none;
+  display: flex;
+  justify-content: space-between;
 
   button {
-    padding: 10px;
+    padding: 7px 10px;
+  }
+  
+  .toolbar-commands {
+    justify-content: space-between;
+    display: flex;
+    align-items: center;
+    overflow-x: overlay;
+    overflow-y: hidden;
+  }
+
+  .toolbar-hide-keyboard {
+    border-left: 1px solid;
+    border-color: var(--ls-quaternary-background-color);
   }
 }
 

+ 1 - 1
src/main/frontend/components/shortcut.cljs

@@ -132,7 +132,7 @@
                          :pre "```clojure\n  (println \"Hello world!\")\n```"
                          :img "![image](https://asset.logseq.com/static/img/logo.png)"}
               :org {:bold (str "*" (t :bold) "*")
-                    :italic (str "/" (t :italics) "/")
+                    :italics (str "/" (t :italics) "/")
                     :del (str "+" (t :strikethrough) "+")
                     :pre [:pre "#+BEGIN_SRC clojure\n  (println \"Hello world!\")\n#+END_SRC"]
                     :link "[[https://www.example.com][Link]]"

+ 1 - 1
src/main/frontend/components/sidebar.cljs

@@ -45,7 +45,7 @@
                        ^js parent (.closest target ".nav-content-item")]
                    (.toggle (.-classList parent) "is-expand")))}
 
-    [:a.font-medium.fade-link name]
+    [:div.font-medium.fade-link name]
     [:span
      [:a.more svg/arrow-down-v2]]]
    [:div.bd child]])

+ 13 - 6
src/main/frontend/db/query_dsl.cljs

@@ -561,12 +561,19 @@
                                         (take @sample (shuffle col)))
                                       identity)
                      transform-fn (comp sort-by random-samples)]
-                 (react/react-query repo
-                                    {:query query
-                                     :rules rules
-                                     :query-string query-string}
-                                    {:use-cache? false
-                                     :transform-fn transform-fn}))))))))))
+                 (try
+                   (react/react-query repo
+                                      {:query query
+                                       :query-string query-string
+                                       :rules rules
+                                       :throw-exception true}
+                                      {:use-cache? false
+                                       :transform-fn transform-fn})
+                   (catch ExceptionInfo e
+                     ;; Allow non-existent page queries to be ignored
+                     (if (string/includes? (str (.-message e)) "Nothing found for entity")
+                       (log/error :query-dsl-error e)
+                       (throw e)))))))))))))
 
 (defn custom-query
   [repo query-m query-opts]

+ 3 - 1
src/main/frontend/db/query_react.cljs

@@ -112,7 +112,7 @@
          f)) query)))
 
 (defn react-query
-  [repo {:keys [query inputs rules] :as query'} query-opts]
+  [repo {:keys [query inputs throw-exception rules] :as query'} query-opts]
   (let [pprint (if config/dev? (fn [_] nil) debug/pprint)]
     (pprint "================")
     (pprint "Use the following to debug your datalog queries:")
@@ -129,5 +129,7 @@
         (pprint "query-opts:" query-opts)
         (apply react/q repo k query-opts query inputs))
       (catch js/Error e
+        (when throw-exception
+          (throw (ex-info (.-message e) {})))
         (pprint "Custom query failed: " {:query query'})
         (js/console.dir e)))))

+ 43 - 32
src/main/frontend/format/block.cljs

@@ -238,38 +238,49 @@
        [original-page-name page-name day]))))
 
 (defn page-name->map
-  [original-page-name with-id?]
-  (cond
-    (and original-page-name (string? original-page-name))
-    (let [original-page-name (util/remove-boundary-slashes original-page-name)
-          [original-page-name page-name journal-day] (convert-page-if-journal original-page-name)
-          namespace? (and (not (boolean (text/get-nested-page-name original-page-name)))
-                          (text/namespace-page? original-page-name))
-          m (merge
-             {:block/name page-name
-              :block/original-name original-page-name}
-             (when with-id?
-               (if (db/entity [:block/name page-name])
-                 {}
-                 {:block/uuid (db/new-block-id)}))
-             (when namespace?
-               (let [namespace (first (util/split-last "/" original-page-name))]
-                 (when-not (string/blank? namespace)
-                   {:block/namespace {:block/name (util/page-name-sanity-lc namespace)}}))))]
-      (if journal-day
-        (merge m
-               {:block/journal? true
-                :block/journal-day journal-day})
-        (assoc m :block/journal? false)))
-
-    (and (map? original-page-name) (:block/uuid original-page-name))
-    original-page-name
-
-    (and (map? original-page-name) with-id?)
-    (assoc original-page-name :block/uuid (db/new-block-id))
-
-    :else
-    nil))
+  "Create a page's map structure given a original page name (string).
+   map as input is supported for legacy compatibility.
+   with-timestamp?: assign timestampes to the map structure. 
+    Useful when creating new pages from references or namespaces, 
+    as there's no chance to introduce timestamps via editing in page"
+  ([original-page-name with-id?]
+   (page-name->map original-page-name with-id? true))
+  ([original-page-name with-id? with-timestamp?]
+   (cond
+     (and original-page-name (string? original-page-name))
+     (let [original-page-name (util/remove-boundary-slashes original-page-name)
+           [original-page-name page-name journal-day] (convert-page-if-journal original-page-name)
+           namespace? (and (not (boolean (text/get-nested-page-name original-page-name)))
+                           (text/namespace-page? original-page-name))
+           page-entity (db/entity [:block/name page-name])]
+       (merge
+        {:block/name page-name
+         :block/original-name original-page-name}
+        (when with-id?
+          (if page-entity
+            {}
+            {:block/uuid (db/new-block-id)}))
+        (when namespace?
+          (let [namespace (first (util/split-last "/" original-page-name))]
+            (when-not (string/blank? namespace)
+              {:block/namespace {:block/name (util/page-name-sanity-lc namespace)}})))
+        (when (and with-timestamp? (not page-entity)) ;; Only assign timestamp on creating new entity
+          (let [current-ms (util/time-ms)]
+            {:block/created-at current-ms
+             :block/updated-at current-ms}))
+        (if journal-day
+          {:block/journal? true
+           :block/journal-day journal-day}
+          {:block/journal? false})))
+
+     (and (map? original-page-name) (:block/uuid original-page-name))
+     original-page-name
+
+     (and (map? original-page-name) with-id?)
+     (assoc original-page-name :block/uuid (db/new-block-id))
+
+     :else
+     nil)))
 
 (defn with-page-refs
   [{:keys [title body tags refs marker priority] :as block} with-id?]

+ 9 - 9
src/main/frontend/fs.cljs

@@ -114,11 +114,11 @@
 
     :else
     (let [[old-path new-path]
-        (map #(if (or (util/electron?) (mobile-util/is-native-platform?))
-                %
-                (str (config/get-repo-dir repo) "/" %))
-             [old-path new-path])]
-     (protocol/rename! (get-fs old-path) repo old-path new-path))))
+          (map #(if (or (util/electron?) (mobile-util/is-native-platform?))
+                  %
+                  (str (config/get-repo-dir repo) "/" %))
+               [old-path new-path])]
+      (protocol/rename! (get-fs old-path) repo old-path new-path))))
 
 (defn stat
   [dir path]
@@ -159,7 +159,7 @@
 
 (defn watch-dir!
   [dir]
-  (protocol/watch-dir! node-record dir))
+  (protocol/watch-dir! (get-record) dir))
 
 (defn mkdir-if-not-exists
   [dir]
@@ -184,9 +184,9 @@
       (p/let [_stat (stat dir path)]
         true)
       (p/catch
-          (fn [_error]
-            (p/let [_ (write-file! repo dir path initial-content nil)]
-              false)))))))
+       (fn [_error]
+         (p/let [_ (write-file! repo dir path initial-content nil)]
+           false)))))))
 
 (defn file-exists?
   [dir path]

+ 45 - 64
src/main/frontend/fs/capacitor_fs.cljs

@@ -16,6 +16,20 @@
     []
     (.ensureDocuments mobile-util/ios-file-container)))
 
+(when (mobile-util/native-ios?)
+  ;; NOTE: avoid circular dependency
+  #_:clj-kondo/ignore
+  (def handle-changed! (delay frontend.fs.watcher-handler/handle-changed!))
+
+  (p/do!
+   (.addListener mobile-util/fs-watcher "watcher"
+                 (fn [^js event]
+                   (@handle-changed!
+                    (.-event event)
+                    (update (js->clj event :keywordize-keys true)
+                            :path
+                            js/decodeURI))))))
+
 (defn check-permission-android []
   (p/let [permission (.checkPermissions Filesystem)
           permission (-> permission
@@ -117,9 +131,9 @@
          (log/error :write-file-failed error))))
 
     (p/let [disk-content (-> (p/chain (.readFile Filesystem (clj->js {:path path
-                                                                   :encoding (.-UTF8 Encoding)}))
-                                   #(js->clj % :keywordize-keys true)
-                                   :data)
+                                                                      :encoding (.-UTF8 Encoding)}))
+                                      #(js->clj % :keywordize-keys true)
+                                      :data)
                              (p/catch (fn [error]
                                         (js/console.error error)
                                         nil)))
@@ -161,6 +175,7 @@
                                  js/decodeURI)
                         [dir path])
         dir (string/replace dir #"/+$" "")
+        path (some-> path (string/replace #"^/+" ""))
         path (cond (nil? path)
                    dir
 
@@ -168,7 +183,7 @@
                    path
 
                    :else
-                   (str dir path))]
+                   (str dir "/" path))]
     (if (mobile-util/native-ios?)
       (js/encodeURI (js/decodeURI path))
       path)))
@@ -205,37 +220,40 @@
                                   (clj->js
                                    {:path path
                                     ;; :directory (.-ExternalStorage Directory)
-                                    :encoding (.-UTF8 Encoding)}))]
+                                    :encoding (.-UTF8 Encoding)}))
+               content (-> (js->clj content :keywordize-keys true)
+                           :data
+                           clj->js)]
          content)
        (p/catch (fn [error]
                   (log/error :read-file-failed error))))))
   (delete-file! [_this repo dir path {:keys [ok-handler error-handler]}]
     (let [path (get-file-path dir path)]
       (p/catch
-          (p/let [result (.deleteFile Filesystem
-                                      (clj->js
-                                       {:path path}))]
-            (when ok-handler
-              (ok-handler repo path result)))
-          (fn [error]
-            (if error-handler
-              (error-handler error)
-              (log/error :delete-file-failed error))))))
+       (p/let [result (.deleteFile Filesystem
+                                   (clj->js
+                                    {:path path}))]
+         (when ok-handler
+           (ok-handler repo path result)))
+       (fn [error]
+         (if error-handler
+           (error-handler error)
+           (log/error :delete-file-failed error))))))
   (write-file! [this repo dir path content opts]
     (let [path (get-file-path dir path)]
       (p/let [stat (p/catch
-                       (.stat Filesystem (clj->js {:path path}))
-                       (fn [_e] :not-found))]
+                    (.stat Filesystem (clj->js {:path path}))
+                    (fn [_e] :not-found))]
         (write-file-impl! this repo dir path content opts stat))))
   (rename! [_this _repo old-path new-path]
     (let [[old-path new-path] (map #(get-file-path "" %) [old-path new-path])]
       (p/catch
-          (p/let [_ (.rename Filesystem
-                             (clj->js
-                              {:from old-path
-                               :to new-path}))])
-          (fn [error]
-            (log/error :rename-file-failed error)))))
+       (p/let [_ (.rename Filesystem
+                          (clj->js
+                           {:from old-path
+                            :to new-path}))])
+       (fn [error]
+         (log/error :rename-file-failed error)))))
   (stat [_this dir path]
     (let [path (get-file-path dir path)]
       (p/let [result (.stat Filesystem (clj->js
@@ -255,45 +273,8 @@
       (into [] (concat [{:path path}] files))))
   (get-files [_this path-or-handle _ok-handler]
     (readdir path-or-handle))
-  (watch-dir! [_this _dir]
-    nil))
-
-
-(comment
-  ;;open-dir result
-  #_
-  ["/storage/emulated/0/untitled folder 21"
-   {:type    "file",
-    :size    2,
-    :mtime   1630049904000,
-    :uri     "file:///storage/emulated/0/untitled%20folder%2021/pages/contents.md",
-    :ctime   1630049904000,
-    :content "-\n"}
-   {:type    "file",
-    :size    0,
-    :mtime   1630049904000,
-    :uri     "file:///storage/emulated/0/untitled%20folder%2021/logseq/custom.css",
-    :ctime   1630049904000,
-    :content ""}
-   {:type    "file",
-    :size    2,
-    :mtime   1630049904000,
-    :uri     "file:///storage/emulated/0/untitled%20folder%2021/logseq/metadata.edn",
-    :ctime   1630049904000,
-    :content "{}"}
-   {:type  "file",
-    :size  181,
-    :mtime 1630050535000,
-    :uri
-    "file:///storage/emulated/0/untitled%20folder%2021/journals/2021_08_27.md",
-    :ctime 1630050535000,
-    :content
-    "- xx\n- xxx\n- xxx\n- xxxxxxxx\n- xxx\n- xzcxz\n- xzcxzc\n- asdsad\n- asdsadasda\n- asdsdaasdsad\n- asdasasdas\n- asdsad\n- sad\n- asd\n- asdsad\n- asdasd\n- sadsd\n-\n- asd\n- saddsa\n- asdsaasd\n- asd"}
-   {:type  "file",
-    :size  132,
-    :mtime 1630311293000,
-    :uri
-    "file:///storage/emulated/0/untitled%20folder%2021/journals/2021_08_30.md",
-    :ctime 1630311293000,
-    :content
-    "- ccc\n- sadsa\n- sadasd\n- asdasd\n- asdasd\n\t- asdasd\n\t\t- asdasdsasd\n\t\t\t- sdsad\n\t\t-\n- sadasd\n- asdas\n- sadasd\n-\n-\n\t- sadasdasd\n\t- asdsd"}])
+  (watch-dir! [_this dir]
+    (when (mobile-util/native-ios?)
+      (p/do!
+       (.unwatch mobile-util/fs-watcher)
+       (.watch mobile-util/fs-watcher #js {:path dir})))))

+ 17 - 2
src/main/frontend/fs/watcher_handler.cljs

@@ -8,6 +8,7 @@
             [frontend.handler.extract :as extract]
             [frontend.handler.file :as file-handler]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.repo :as repo-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
@@ -44,6 +45,7 @@
   (when dir
     (let [path (util/path-normalize path)
           repo (config/get-local-repo dir)
+          pages-metadata-path (config/get-pages-metadata-path)
           {:keys [mtime]} stat
           db-content (or (db/get-file repo path) "")]
       (when (and (or content (= type "unlink"))
@@ -52,7 +54,7 @@
         (cond
           (and (= "add" type)
                (not= (string/trim content) (string/trim db-content))
-               (not (string/includes? path "logseq/pages-metadata.edn")))
+               (not= path pages-metadata-path))
           (let [backup? (not (string/blank? db-content))]
             (handle-add-and-change! repo path content db-content mtime backup?))
 
@@ -62,7 +64,7 @@
 
           (and (= "change" type)
                (not= (string/trim content) (string/trim db-content))
-               (not (string/includes? path "logseq/pages-metadata.edn")))
+               (not= path pages-metadata-path))
           (when-not (and
                      (string/includes? path (str "/" (config/get-journals-directory) "/"))
                      (or
@@ -84,6 +86,19 @@
             (println "reloading custom.css")
             (ui-handler/add-style-if-exists!))
 
+          ;; When metadata is added to watcher, update timestamps in db accordingly
+          ;; This event is not triggered on re-index
+          ;; Persistent metadata is gold standard when db is offline, so it's forced
+          (and (contains? #{"add"} type)
+               (= path pages-metadata-path))
+          (p/do! (repo-handler/update-pages-metadata! repo content true))
+          
+          ;; Change is triggered by external changes, so update to the db
+          ;; Don't forced update when db is online, but resolving conflicts
+          (and (contains? #{"change"} type)
+               (= path pages-metadata-path))
+          (p/do! (repo-handler/update-pages-metadata! repo content false))
+
           (contains? #{"add" "change" "unlink"} type)
           nil
 

+ 19 - 17
src/main/frontend/handler/editor.cljs

@@ -29,7 +29,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.image :as image]
             [frontend.idb :as idb]
-            [frontend.mobile.util :as mobile]
+            [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.core :as outliner-core]
             [frontend.modules.outliner.datascript :as ds]
             [frontend.modules.outliner.tree :as tree]
@@ -339,8 +339,9 @@
   [block value]
   (if (and (state/enable-timetracking?)
            (not= (:block/content block) value))
-    (let [new-marker (first (util/safe-re-find marker/bare-marker-pattern (or value "")))
-          new-value (with-marker-time value block (:block/format block)
+    (let [format (:block/format block)
+          new-marker (last (util/safe-re-find (marker/marker-pattern format) (or value "")))
+          new-value (with-marker-time value block format
                       new-marker
                       (:block/marker block))]
       new-value)
@@ -436,7 +437,7 @@
        (another-block-with-same-id-exists? uuid block-id)
        (notification/show!
         [:p.content
-         (util/format "Block with the id % already exists!" block-id)]
+         (util/format "Block with the id %s already exists!" block-id)]
         :error)
 
        force?
@@ -789,8 +790,14 @@
       (save-block-if-changed! block new-content))))
 
 (defn set-marker
-  [{:block/keys [marker content] :as block} new-marker]
-  (let [new-content (->
+  [{:block/keys [marker content format] :as block} new-marker]
+  (let [old-header-marker (when (not= format :org)
+                            (re-find (marker/header-marker-pattern true marker) content))
+        new-header-marker (when old-header-marker
+                            (string/replace old-header-marker marker new-marker))
+        marker (or old-header-marker marker)
+        new-marker (or new-header-marker new-marker)
+        new-content (->
                      (if marker
                        (string/replace-first content (re-pattern (str "^" marker)) new-marker)
                        (str new-marker " " content))
@@ -1575,13 +1582,8 @@
       (util/electron?)
       (str "assets://" repo-dir path)
 
-      (mobile/native-android?)
-      (mobile/convert-file-src
-       (str "file://" repo-dir path))
-
-      (mobile/native-ios?)
-      (mobile/convert-file-src
-       (str repo-dir path))
+      (mobile-util/is-native-platform?)
+      (mobile-util/convert-file-src (str repo-dir path))
 
       :else
       (let [handle-path (str "handle" repo-dir path)
@@ -2856,10 +2858,10 @@
         (or ctrlKey metaKey)
         nil
 
-        ;; FIXME: On iOS, a backspace click to call keydown-backspace-handler
+        ;; FIXME: On mobile, a backspace click to call keydown-backspace-handler
         ;; does not work sometimes in an empty block, hence the empty block
         ;; can't be deleted. Need to figure out why and find a better solution.
-        (and (mobile/native-ios?)
+        (and (mobile-util/is-native-platform?)
              (= key "Backspace")
              (= value ""))
         (do
@@ -3130,12 +3132,12 @@
       (and (util/url? text)
            (or (string/includes? text "youtube.com")
                (string/includes? text "youtu.be"))
-           (mobile/is-native-platform?))
+           (mobile-util/is-native-platform?))
       (commands/simple-insert! (state/get-edit-input-id) (util/format "{{youtube %s}}" text) nil)
 
       (and (util/url? text)
            (string/includes? text "twitter.com")
-           (mobile/is-native-platform?))
+           (mobile-util/is-native-platform?))
       (commands/simple-insert! (state/get-edit-input-id) (util/format "{{twitter %s}}" text) nil)
 
       (and (text/block-ref? text)

+ 3 - 4
src/main/frontend/handler/file.cljs

@@ -314,10 +314,9 @@
 
 (defn watch-for-current-graph-dir!
   []
-  (when (util/electron?)
-    (when-let [repo (state/get-current-repo)]
-      (when-let [dir (config/get-repo-dir repo)]
-        (fs/watch-dir! dir)))))
+  (when-let [repo (state/get-current-repo)]
+    (when-let [dir (config/get-repo-dir repo)]
+      (fs/watch-dir! dir))))
 
 (defn create-metadata-file
   [repo-url encrypted?]

+ 40 - 25
src/main/frontend/handler/repo.cljs

@@ -145,29 +145,44 @@
        (state/pub-event! [:page/create-today-journal repo-url])))))
 
 (defn- load-pages-metadata!
-  [repo file-paths files]
-  (try
-    (let [file (config/get-pages-metadata-path)]
-      (when (contains? (set file-paths) file)
-        (when-let [content (some #(when (= (:file/path %) file) (:file/content %)) files)]
-          (let [metadata (common-handler/safe-read-string content "Parsing pages metadata file failed: ")
-                pages (db/get-all-pages repo)
-                pages (zipmap (map :block/name pages) pages)
-                metadata (->>
-                          (filter (fn [{:block/keys [name created-at updated-at]}]
-                                    (when-let [page (get pages name)]
-                                      (and
-                                       (or
-                                        (nil? (:block/created-at page))
-                                        (>= created-at (:block/created-at page)))
-                                       (or
-                                        (nil? (:block/updated-at page))
-                                        (>= updated-at (:block/created-at page)))))) metadata)
-                          (remove nil?))]
-            (when (seq metadata)
-              (db/transact! repo metadata))))))
-    (catch js/Error e
-      (log/error :exception e))))
+  "force?: if set true, skip the metadata timestamp range check"
+  ([repo file-paths files]
+   (load-pages-metadata! repo file-paths files false))
+  ([repo file-paths files force?]
+   (try
+     (let [file (config/get-pages-metadata-path)]
+       (when (contains? (set file-paths) file)
+         (when-let [content (some #(when (= (:file/path %) file) (:file/content %)) files)]
+           (let [metadata (common-handler/safe-read-string content "Parsing pages metadata file failed: ")
+                 pages (db/get-all-pages repo)
+                 pages (zipmap (map :block/name pages) pages)
+                 metadata (->>
+                           (filter (fn [{:block/keys [name created-at updated-at]}]
+                                     (when-let [page (get pages name)]
+                                       (and
+                                        (>= updated-at created-at) ;; metadata validation
+                                        (or force? ;; when force is true, shortcut timestamp range check
+                                            (and (or (nil? (:block/created-at page))
+                                                     (>= created-at (:block/created-at page)))
+                                                 (or (nil? (:block/updated-at page))
+                                                     (>= updated-at (:block/created-at page)))))
+                                        (or ;; persistent metadata is the gold standard
+                                         (not= created-at (:block/created-at page))
+                                         (not= updated-at (:block/created-at page)))))) metadata)
+                           (remove nil?))]
+             (when (seq metadata)
+               (db/transact! repo metadata))))))
+     (catch js/Error e
+       (log/error :exception e)))))
+
+(defn update-pages-metadata!
+  "update pages meta content -> db. Only accept non-encrypted content!"
+  [repo content force?]
+  (let [path (config/get-pages-metadata-path)
+        files [{:file/path path
+                :file/content content}]
+        file-paths [path]]
+    (util/profile "update-pages-metadata!" (load-pages-metadata! repo file-paths files force?))))
 
 (defn- parse-files-and-create-default-files-inner!
   [repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts metadata opts]
@@ -189,7 +204,7 @@
                                 :re-render-root? false
                                 :from-disk? true
                                 :metadata metadata}))
-    (load-pages-metadata! repo-url file-paths files)
+    (load-pages-metadata! repo-url file-paths files true)
     (when first-clone?
       (if (and (not db-encrypted?) (state/enable-encryption? repo-url))
         (state/pub-event! [:modal/encryption-setup-dialog repo-url
@@ -249,7 +264,7 @@
 
   (let [config (or (state/get-config repo-url)
                    (when-let [content (some-> (first (filter #(= (config/get-config-path repo-url) (:file/path %)) nfs-files))
-                                        :file/content)]
+                                              :file/content)]
                      (common-handler/read-config content)))
         relate-path-fn (fn [m k]
                          (some-> (get m k)

+ 1 - 2
src/main/frontend/handler/web/nfs.cljs

@@ -179,8 +179,7 @@
                              (state/add-repo! {:url repo :nfs? true})
                              (state/set-loading-files! repo false)
                              (when ok-handler (ok-handler))
-                             (when (util/electron?)
-                               (fs/watch-dir! dir-name))
+                             (fs/watch-dir! dir-name)
                              (db/persist-if-idle! repo)))))
                (p/catch (fn [error]
                           (log/error :nfs/load-files-error repo)

+ 4 - 3
src/main/frontend/mobile/util.cljs

@@ -22,9 +22,10 @@
 
 (defonce folder-picker (registerPlugin "FolderPicker"))
 (when (native-ios?)
- (defonce download-icloud-files (registerPlugin "DownloadiCloudFiles")))
-(when (native-ios?)
+  (defonce download-icloud-files (registerPlugin "DownloadiCloudFiles"))
   (defonce ios-file-container (registerPlugin "FileContainer")))
+(when (native-ios?)
+  (defonce fs-watcher (registerPlugin "FsWatcher")))
 
 (defn sync-icloud-repo [repo-dir]
   (let [repo-name (-> (string/split repo-dir "Documents/")
@@ -32,7 +33,7 @@
                       string/trim
                       js/decodeURI)]
     (.syncGraph download-icloud-files
-                       (clj->js {:graph repo-name}))))
+                (clj->js {:graph repo-name}))))
 
 (defn hide-splash []
   (.hide SplashScreen))

+ 2 - 0
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -190,7 +190,9 @@
 
 (defn shortcuts->commands [handler-id]
   (let [m (get @config/config handler-id)]
+    ;; NOTE: remove nil vals, since some commands are conditional
     (->> m
+         (filter (comp some? val))
          (map (fn [[id _]] (-> (shortcut-data-by-id id)
                                (assoc :id id :handler-id handler-id)
                                (rename-keys {:binding :shortcut

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

@@ -2,13 +2,14 @@
   (:require [clojure.string :as string]
             [frontend.util :as util]))
 
-(def marker-pattern
-  #"^(NOW|LATER|TODO|DOING|DONE|WAITING|WAIT|CANCELED|CANCELLED|STARTED|IN-PROGRESS)?\s?")
+(defn marker-pattern [format]
+  (re-pattern
+   (str "^" (when-not (= format :org) "(#*\\s*)?")
+        "(NOW|LATER|TODO|DOING|DONE|WAITING|WAIT|CANCELED|CANCELLED|STARTED|IN-PROGRESS)?\\s?")))
 
 (def bare-marker-pattern
   #"(NOW|LATER|TODO|DOING|DONE|WAITING|WAIT|CANCELED|CANCELLED|STARTED|IN-PROGRESS){1}\s+")
 
-
 (defn add-or-update-marker
   [content format marker]
   (let [[re-pattern new-line-re-pattern]
@@ -23,7 +24,7 @@
         new-content
         (str (subs content 0 pos)
              (string/replace-first (subs content pos)
-                                   marker-pattern
+                                   (marker-pattern format)
                                    (str marker " ")))]
     new-content))
 

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

@@ -1,6 +1,6 @@
 (ns frontend.handler.editor-test
   (:require [frontend.handler.editor :as editor]
-            [clojure.test :refer [deftest is testing]]))
+            [clojure.test :refer [deftest is testing are]]))
 
 (deftest extract-nearest-link-from-text-test
   (testing "Page, block and tag links"
@@ -41,3 +41,24 @@
          (editor/extract-nearest-link-from-text
           "[[https://github.com/logseq/logseq][logseq]] is #awesome :)" 0 editor/url-regex))
       "Finds url in org link correctly"))
+
+(defn- set-marker
+  [marker content format new-marker]
+  (let [actual-content (atom nil)]
+    (with-redefs [editor/save-block-if-changed! (fn [_ content]
+                                                  (reset! actual-content content))]
+      (editor/set-marker {:block/marker marker :block/content content :block/format format}
+                         new-marker)
+      @actual-content)))
+
+(deftest set-marker-org
+  (are [marker content new-marker expect] (= expect (set-marker marker content :org new-marker))
+    "TODO" "TODO content" "DOING" "DOING content"
+    "TODO" "## TODO content" "DOING" "## TODO content"
+    "DONE" "DONE content" "" "content"))
+
+(deftest set-marker-markdown
+  (are [marker content new-marker expect] (= expect (set-marker marker content :markdown new-marker))
+    "TODO" "TODO content" "DOING" "DOING content"
+    "TODO" "## TODO content" "DOING" "## DOING content"
+    "DONE" "DONE content" "" "content"))

+ 8 - 10
yarn.lock

@@ -5247,10 +5247,10 @@ mkdirp@^1.0.3:
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
[email protected].1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.3.1.tgz#50a8ad73d0489624883ad1414dd8a11ccff0ddf1"
-  integrity sha512-WlsK7fKQOV4t3YBVonHOOE+SOYkPn6/jb3Ob+Hn8dH/SMZ71OfhdKIxjqJrYn4M+10FfiQvKS9lXVHLAGqsl/A==
[email protected].2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/mldoc/-/mldoc-1.3.2.tgz#d08bb6bd7a6ea6ae27cb02f4d5b4c8f7c95d31ba"
+  integrity sha512-yK/IfRwLpNEyFyU61wISho3ik2en/VVtY4wxZFfyasOX5xBvxcI/FBAqXmO4hV7V6paH8Ko9APgAW3AcX+CiOQ==
   dependencies:
     yargs "^12.0.2"
 
@@ -6760,17 +6760,15 @@ [email protected]:
     react-draggable "3.x"
     react-resizable "1.x"
 
-react-icon-base@2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.0.tgz#a196e33fdf1e7aaa1fda3aefbb68bdad9e82a79d"
-  integrity sha1-oZbjP98eeqof2jrvu2i9rZ6Cp50=
+react-icon-base@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/react-icon-base/-/react-icon-base-2.1.2.tgz#a17101dad9c1192652356096860a9ab43a0766c7"
+  integrity sha512-NRlRo0RPxWRMQT7osj8UCBSSXsGOxhF1pre84ildhuft5S2U382NOs7tg29osWSjbO90L2a3VTCqadA/LnAzHQ==
 
 [email protected]:
   version "2.2.7"
   resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-2.2.7.tgz#d7860826b258557510dac10680abea5ca23cf650"
   integrity sha512-0n4lcGqzJFcIQLoQytLdJCE0DKSA9dkwEZRYoGrIDJZFvIT6Hbajx5mv9geqhqFiNjUgtxg8kPyDfjlhymbGFg==
-  dependencies:
-    react-icon-base "2.1.0"
 
 react-is@^16.13.1, react-is@^16.3.1:
   version "16.13.1"