Browse Source

Merge remote-tracking branch 'upstream/master' into whiteboards

Peng Xiao 3 years ago
parent
commit
0075dac0c5
55 changed files with 903 additions and 709 deletions
  1. 2 2
      android/app/build.gradle
  2. 3 0
      android/app/src/main/assets/capacitor.config.json
  3. 33 25
      android/app/src/main/java/com/logseq/app/FsWatcher.java
  4. 4 0
      capacitor.config.ts
  5. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  6. 2 26
      ios/App/App/FileSync/Extensions.swift
  7. 53 10
      ios/App/App/FileSync/FileSync.swift
  8. 35 22
      ios/App/App/FsWatcher.swift
  9. 3 0
      ios/App/App/capacitor.config.json
  10. 1 1
      resources/package.json
  11. 6 3
      shadow-cljs.edn
  12. 11 9
      src/electron/electron/fs_watcher.cljs
  13. 13 13
      src/electron/electron/utils.cljs
  14. 4 9
      src/main/frontend/commands.cljs
  15. 133 86
      src/main/frontend/components/block.cljs
  16. 0 8
      src/main/frontend/components/block.css
  17. 2 95
      src/main/frontend/components/editor.cljs
  18. 0 30
      src/main/frontend/components/editor.css
  19. 3 5
      src/main/frontend/components/header.cljs
  20. 4 1
      src/main/frontend/components/header.css
  21. 2 2
      src/main/frontend/components/onboarding/setups.cljs
  22. 2 4
      src/main/frontend/components/page.cljs
  23. 2 2
      src/main/frontend/components/page_menu.cljs
  24. 2 2
      src/main/frontend/components/repo.cljs
  25. 6 6
      src/main/frontend/components/settings.cljs
  26. 25 22
      src/main/frontend/components/sidebar.cljs
  27. 2 2
      src/main/frontend/components/widgets.cljs
  28. 2 2
      src/main/frontend/config.cljs
  29. 2 2
      src/main/frontend/db/conn.cljs
  30. 74 0
      src/main/frontend/db/model.cljs
  31. 3 3
      src/main/frontend/extensions/video/youtube.cljs
  32. 5 5
      src/main/frontend/fs.cljs
  33. 2 2
      src/main/frontend/handler.cljs
  34. 37 43
      src/main/frontend/handler/editor.cljs
  35. 1 4
      src/main/frontend/handler/editor/lifecycle.cljs
  36. 23 10
      src/main/frontend/handler/events.cljs
  37. 3 3
      src/main/frontend/handler/page.cljs
  38. 1 1
      src/main/frontend/handler/ui.cljs
  39. 8 8
      src/main/frontend/handler/web/nfs.cljs
  40. 12 2
      src/main/frontend/mobile/core.cljs
  41. 8 9
      src/main/frontend/mobile/footer.cljs
  42. 52 1
      src/main/frontend/mobile/index.css
  43. 13 13
      src/main/frontend/mobile/intent.cljs
  44. 141 0
      src/main/frontend/mobile/mobile_bar.cljs
  45. 4 52
      src/main/frontend/mobile/util.cljs
  46. 1 1
      src/main/frontend/modules/instrumentation/sentry.cljs
  47. 59 33
      src/main/frontend/modules/outliner/core.cljs
  48. 1 2
      src/main/frontend/page.cljs
  49. 11 18
      src/main/frontend/state.cljs
  50. 3 37
      src/main/frontend/ui.cljs
  51. 55 61
      src/main/frontend/util.cljc
  52. 11 6
      src/main/frontend/util/fs.cljs
  53. 1 1
      src/main/frontend/util/thingatpt.cljs
  54. 1 1
      src/main/frontend/version.cljs
  55. 12 0
      src/main/logseq/graph_parser/text.cljs

+ 2 - 2
android/app/build.gradle

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

+ 3 - 0
android/app/src/main/assets/capacitor.config.json

@@ -10,6 +10,9 @@
 			"androidScaleType": "CENTER_CROP",
 			"splashImmersive": false,
 			"backgroundColor": "#002b36"
+		},
+		"Keyboard": {
+			"resize": "none"
 		}
 	},
 	"ios": {

+ 33 - 25
android/app/src/main/java/com/logseq/app/FsWatcher.java

@@ -1,5 +1,7 @@
 package com.logseq.app;
 
+import android.annotation.SuppressLint;
+import android.os.Build;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.StructStat;
@@ -26,7 +28,6 @@ public class FsWatcher extends Plugin {
 
     List<SingleFileObserver> observers;
     private String mPath;
-    private Uri mPathUri;
 
     @Override
     public void load() {
@@ -35,17 +36,23 @@ public class FsWatcher extends Plugin {
 
     @PluginMethod()
     public void watch(PluginCall call) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            call.reject("Android version not supported");
+            return;
+        }
         String pathParam = call.getString("path");
         // check file:// or no scheme uris
         Uri u = Uri.parse(pathParam);
         Log.i("FsWatcher", "watching " + u);
         if (u.getScheme() == null || u.getScheme().equals("file")) {
-            File pathObj = new File(u.getPath());
-            if (pathObj == null) {
+            File pathObj;
+            try {
+                pathObj = new File(u.getPath());
+            } catch (Exception e) {
                 call.reject("invalid watch path: " + pathParam);
                 return;
             }
-            mPathUri = Uri.fromFile(pathObj);
+
             mPath = pathObj.getAbsolutePath();
 
             int mask = FileObserver.CLOSE_WRITE |
@@ -56,15 +63,16 @@ public class FsWatcher extends Plugin {
                 call.reject("already watching");
                 return;
             }
-            observers = new ArrayList<SingleFileObserver>();
+            observers = new ArrayList<>();
             observers.add(new SingleFileObserver(pathObj, mask));
 
             // NOTE: only watch first level of directory
             File[] files = pathObj.listFiles();
             if (files != null) {
-                for (int i = 0; i < files.length; ++i) {
-                    if (files[i].isDirectory() && !files[i].getName().startsWith(".")) {
-                        observers.add(new SingleFileObserver(files[i], mask));
+                for (File file : files) {
+                    String filename = file.getName();
+                    if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("node_modules")) {
+                        observers.add(new SingleFileObserver(file, mask));
                     }
                 }
             }
@@ -103,13 +111,14 @@ public class FsWatcher extends Plugin {
         }
         File[] files = pathObj.listFiles();
         if (files != null) {
-            for (int i = 0; i < files.length; ++i) {
-                if (files[i].isDirectory() && !files[i].getName().startsWith(".") && !files[i].getName().equals("bak")) {
-                    this.initialNotify(files[i], maxDepth - 1);
-                } else if (files[i].isFile()
-                        && Pattern.matches("[^.].*?\\.(md|org|css|edn|text|markdown|yml|yaml|json|js)$",
-                                files[i].getName())) {
-                    this.onObserverEvent(FileObserver.CREATE, files[i].getAbsolutePath());
+            for (File file : files) {
+                String filename = file.getName();
+                if (file.isDirectory() && !filename.startsWith(".") && !filename.equals("bak") && !filename.equals("version-files") && !filename.equals("node_modules")) {
+                    this.initialNotify(file, maxDepth - 1);
+                } else if (file.isFile()
+                        && Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$",
+                        file.getName())) {
+                    this.onObserverEvent(FileObserver.CREATE, file.getAbsolutePath());
                 }
             }
         }
@@ -132,9 +141,7 @@ public class FsWatcher extends Plugin {
                 try {
                     obj.put("stat", getFileStat(path));
                     content = getFileContents(f);
-                } catch (IOException e) {
-                    e.printStackTrace();
-                } catch (ErrnoException e) {
+                } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
                 obj.put("content", content);
@@ -145,9 +152,7 @@ public class FsWatcher extends Plugin {
                 try {
                     obj.put("stat", getFileStat(path));
                     content = getFileContents(f);
-                } catch (IOException e) {
-                    e.printStackTrace();
-                } catch (ErrnoException e) {
+                } catch (IOException | ErrnoException e) {
                     e.printStackTrace();
                 }
                 obj.put("content", content);
@@ -172,7 +177,7 @@ public class FsWatcher extends Plugin {
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 
         byte[] buffer = new byte[1024];
-        int length = 0;
+        int length;
 
         while ((length = inputStream.read(buffer)) != -1) {
             outputStream.write(buffer, 0, length);
@@ -183,22 +188,25 @@ public class FsWatcher extends Plugin {
     }
 
     public static JSObject getFileStat(final String path) throws ErrnoException {
+        File file = new File(path);
         StructStat stat = Os.stat(path);
         JSObject obj = new JSObject();
         obj.put("atime", stat.st_atime);
         obj.put("mtime", stat.st_mtime);
         obj.put("ctime", stat.st_ctime);
+        obj.put("size", file.length());
         return obj;
     }
 
     private class SingleFileObserver extends FileObserver {
-        private String mPath;
+        private final String mPath;
 
         public SingleFileObserver(String path, int mask) {
             super(path, mask);
             mPath = path;
         }
 
+        @SuppressLint("NewApi")
         public SingleFileObserver(File path, int mask) {
             super(path, mask);
             mPath = path.getAbsolutePath();
@@ -206,9 +214,9 @@ public class FsWatcher extends Plugin {
 
         @Override
         public void onEvent(int event, String path) {
-            if (path != null) {
+            if (path != null && !path.equals("graphs-txid.edn") && !path.equals("broken-config.edn")) {
                 Log.d("FsWatcher", "got path=" + path + " event=" + event);
-                if (Pattern.matches("[^.].*?\\.(md|org|css|edn|text|markdown|yml|yaml|json|js)$", path)) {
+                if (Pattern.matches("(?i)[^.].*?\\.(md|org|css|edn|js|markdown)$", path)) {
                     String fullPath = mPath + "/" + path;
                     FsWatcher.this.onObserverEvent(event, fullPath);
                 }

+ 4 - 0
capacitor.config.ts

@@ -13,6 +13,10 @@ const config: CapacitorConfig = {
             splashImmersive: false,
             backgroundColor: "#002b36"
         },
+
+        Keyboard: {
+            resize: "none"
+        }
     },
     ios: {
         scheme: "Logseq"

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

@@ -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.6.6;
+				MARKETING_VERSION = 0.6.10;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -568,7 +568,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.6.10;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -593,7 +593,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.6.10;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -620,7 +620,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.6.6;
+				MARKETING_VERSION = 0.6.10;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 2 - 26
ios/App/App/FileSync/Extensions.swift

@@ -8,11 +8,6 @@
 import Foundation
 import CryptoKit
 
-
-import var CommonCrypto.CC_MD5_DIGEST_LENGTH
-import func CommonCrypto.CC_MD5
-import typealias CommonCrypto.CC_LONG
-
 // via https://github.com/krzyzanowskim/CryptoSwift
 extension Array where Element == UInt8 {
   public init(hex: String) {
@@ -120,27 +115,8 @@ extension Data {
 
 extension String {
     var MD5: String {
-        // TODO: incremental hash
-        if #available(iOS 13.0, *) {
-            let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
-            return computed.map { String(format: "%02hhx", $0) }.joined()
-        } else {
-            // Fallback on earlier versions, no CryptoKit
-            let length = Int(CC_MD5_DIGEST_LENGTH)
-            let messageData = self.data(using:.utf8)!
-            var digestData = Data(count: length)
-            
-            _ = digestData.withUnsafeMutableBytes { digestBytes -> UInt8 in
-                messageData.withUnsafeBytes { messageBytes -> UInt8 in
-                    if let messageBytesBaseAddress = messageBytes.baseAddress, let digestBytesBlindMemory = digestBytes.bindMemory(to: UInt8.self).baseAddress {
-                        let messageLength = CC_LONG(messageData.count)
-                        CC_MD5(messageBytesBaseAddress, messageLength, digestBytesBlindMemory)
-                    }
-                    return 0
-                }
-            }
-            return digestData.map { String(format: "%02hhx", $0) }.joined()
-        }
+        let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
+        return computed.map { String(format: "%02hhx", $0) }.joined()
     }
     
     func encodeAsFname() -> String {

+ 53 - 10
ios/App/App/FileSync/FileSync.swift

@@ -17,6 +17,49 @@ var URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
 var BUCKET: String = "logseq-file-sync-bucket"
 var REGION: String = "us-east-2"
 
+
+public struct SyncMetadata: CustomStringConvertible, Equatable {
+    var md5: String
+    var size: Int
+
+    public init?(of fileURL: URL) {
+        do {
+            let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey])
+            guard fileAttributes.isRegularFile! else {
+                return nil
+            }
+            size = fileAttributes.fileSize ?? 0
+            
+            // incremental MD5sum
+            let bufferSize = 1024 * 1024
+            let file = try FileHandle(forReadingFrom: fileURL)
+            defer {
+                file.closeFile()
+            }
+            var ctx = Insecure.MD5.init()
+            while autoreleasepool(invoking: {
+                let data = file.readData(ofLength: bufferSize)
+                if data.count > 0 {
+                    ctx.update(data: data)
+                    return true // continue
+                } else {
+                    return false // eof
+                }
+            }) {}
+            
+            let computed = ctx.finalize()
+            md5 = computed.map { String(format: "%02hhx", $0) }.joined()
+        } catch {
+            return nil
+        }
+    }
+
+    public var description: String {
+        return "SyncMetadata(md5=\(md5), size=\(size))"
+    }
+}
+
+
 // MARK: FileSync Plugin
 
 @objc(FileSync)
@@ -69,16 +112,16 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
             return
         }
         
-        var fileMd5Digests: [String: [String: Any]] = [:]
+        var fileMetadataDict: [String: [String: Any]] = [:]
         for filePath in filePaths {
             let url = baseURL.appendingPathComponent(filePath)
-            if let content = try? String(contentsOf: url, encoding: .utf8) {
-                fileMd5Digests[filePath] = ["md5": content.MD5,
-                                            "size": content.lengthOfBytes(using: .utf8)]
+            if let meta = SyncMetadata(of: url) {
+                fileMetadataDict[filePath] = ["md5": meta.md5,
+                                              "size": meta.size]
             }
         }
         
-        call.resolve(["result": fileMd5Digests])
+        call.resolve(["result": fileMetadataDict])
     }
     
     @objc func getLocalAllFilesMeta(_ call: CAPPluginCall) {
@@ -88,21 +131,21 @@ public class FileSync: CAPPlugin, SyncDebugDelegate {
                   return
               }
         
-        var fileMd5Digests: [String: [String: Any]] = [:]
+        var fileMetadataDict: [String: [String: Any]] = [:]
         if let enumerator = FileManager.default.enumerator(at: baseURL, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsPackageDescendants, .skipsHiddenFiles]) {
             
             for case let fileURL as URL in enumerator {
                 if !fileURL.isSkipped() {
-                    if let content = try? String(contentsOf: fileURL, encoding: .utf8) {
-                        fileMd5Digests[fileURL.relativePath(from: baseURL)!] = ["md5": content.MD5,
-                                                                                "size": content.lengthOfBytes(using: .utf8)]
+                    if let meta = SyncMetadata(of: fileURL) {
+                        fileMetadataDict[fileURL.relativePath(from: baseURL)!] = ["md5": meta.md5,
+                                                                                  "size": meta.size]
                     }
                 } else if fileURL.isICloudPlaceholder() {
                     try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
                 }
             }
         }
-        call.resolve(["result": fileMd5Digests])
+        call.resolve(["result": fileMetadataDict])
     }
     
     

+ 35 - 22
ios/App/App/FsWatcher.swift

@@ -52,23 +52,20 @@ public class FsWatcher: CAPPlugin, PollingWatcherDelegate {
                                                    "dir": baseUrl?.description as Any,
                                                    "path": url.description,
                                                   ])
-        case .Add:
-            let content = try? String(contentsOf: url, encoding: .utf8)
-            self.notifyListeners("watcher", data: ["event": "add",
+        case .Add, .Change:
+            var content: String? = nil
+            if url.shouldNotifyWithContent() {
+                content = try? String(contentsOf: url, encoding: .utf8)
+            }
+            self.notifyListeners("watcher", data: ["event": event.description,
                                                    "dir": baseUrl?.description as Any,
                                                    "path": url.description,
                                                    "content": content as Any,
-                                                   "stat": ["mtime": metadata?.contentModificationTimestamp,
-                                                            "ctime": metadata?.creationTimestamp]
+                                                   "stat": ["mtime": metadata?.contentModificationTimestamp ?? 0,
+                                                            "ctime": metadata?.creationTimestamp ?? 0,
+                                                            "size": metadata?.fileSize as Any]
                                                   ])
-        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
@@ -84,16 +81,18 @@ extension URL {
         if self.lastPathComponent.starts(with: ".") {
             return true
         }
-        // NOTE: used by file-sync
-        if self.lastPathComponent == "graphs-txid.edn" {
+        if self.lastPathComponent == "graphs-txid.edn" || self.lastPathComponent == "broken-config.edn" {
             return true
         }
-        let allowedPathExtensions: Set = ["md", "markdown", "org", "css", "edn", "excalidraw"]
+        return false
+    }
+    
+    func shouldNotifyWithContent() -> Bool {
+        let allowedPathExtensions: Set = ["md", "markdown", "org", "js", "edn", "css", "excalidraw"]
         if allowedPathExtensions.contains(self.pathExtension.lowercased()) {
-            return false
+            return true
         }
-        // skip for other file types
-        return true
+        return false
     }
     
     func isICloudPlaceholder() -> Bool {
@@ -110,13 +109,27 @@ public protocol PollingWatcherDelegate {
     func recevedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?)
 }
 
-public enum PollingWatcherEvent: String {
+public enum PollingWatcherEvent {
     case Add
     case Change
     case Unlink
     case Error
+    
+    var description: String {
+        switch self {
+        case .Add:
+            return "add"
+        case .Change:
+            return "change"
+        case .Unlink:
+            return "unlink"
+        case .Error:
+            return "error"
+        }
+    }
 }
 
+
 public struct SimpleFileMetadata: CustomStringConvertible, Equatable {
     var contentModificationTimestamp: Double
     var creationTimestamp: Double
@@ -192,11 +205,11 @@ public class PollingWatcher {
                 
                 if isDirectory {
                     // NOTE: URL.path won't end with a `/`
-                    if fileURL.path.hasSuffix("/logseq/bak") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" {
+                    if fileURL.path.hasSuffix("/logseq/bak") || fileURL.path.hasSuffix("/logseq/version-files") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" {
                         enumerator.skipDescendants()
                     }
                 }
-            
+                
                 if isRegularFile && !fileURL.isSkipped() {
                     if let meta = SimpleFileMetadata(of: fileURL) {
                         newMetaDb[fileURL] = meta

+ 3 - 0
ios/App/App/capacitor.config.json

@@ -10,6 +10,9 @@
 			"androidScaleType": "CENTER_CROP",
 			"splashImmersive": false,
 			"backgroundColor": "#002b36"
+		},
+		"Keyboard": {
+			"resize": "none"
 		}
 	},
 	"ios": {

+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.6.9",
+  "version": "0.6.10",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",

+ 6 - 3
shadow-cljs.edn

@@ -35,7 +35,8 @@
                            :source-map         true
                            :externs            ["datascript/externs.js"
                                                 "externs.js"]
-                           :warnings           {:fn-deprecated false}}
+                           :warnings           {:fn-deprecated false
+                                                :redef false}}
         :closure-defines  {goog.debug.LOGGING_ENABLED      true}
 
         ;; NOTE: electron, browser/mobile-app use different asset-paths.
@@ -60,7 +61,8 @@
 
                                 :externs  ["datascript/externs.js"
                                            "externs.js"]
-                                :warnings {:fn-deprecated false}}}
+                                :warnings {:fn-deprecated false
+                                           :redef false}}}
 
   :test {:target          :node-test
          :output-to       "static/tests.js"
@@ -96,7 +98,8 @@
                                   :output-feature-set :es-next
                                   :externs            ["datascript/externs.js"
                                                        "externs.js"]
-                                  :warnings           {:fn-deprecated false}}
+                                  :warnings           {:fn-deprecated false
+                                                       :redef false}}
                :devtools         {:before-load frontend.core/stop
                                   :after-load  frontend.core/start
                                   :preloads    [devtools.preload]}}

+ 11 - 9
src/electron/electron/fs_watcher.cljs

@@ -4,7 +4,6 @@
             ["chokidar" :as watcher]
             [electron.utils :as utils]
             ["electron" :refer [app]]
-            [frontend.util.fs :as util-fs]
             [electron.window :as window]))
 
 ;; TODO: explore different solutions for different platforms
@@ -30,10 +29,15 @@
 
 (defn- publish-file-event!
   [dir path event]
-  (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
-                                 :path (utils/fix-win-path! path)
-                                 :content (utils/read-file path)
-                                 :stat (fs/statSync path)}))
+  (let [content (when (and (not= event "unlink")
+                           (utils/should-read-content? path))
+                  (utils/read-file path))
+        stat (when (not= event "unlink")
+               (fs/statSync path))]
+    (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
+                                   :path (utils/fix-win-path! path)
+                                   :content content
+                                   :stat stat})))
 
 (defn watch-dir!
   "Watch a directory if no such file watcher exists"
@@ -43,7 +47,7 @@
     (let [watcher (.watch watcher dir
                           (clj->js
                            {:ignored (fn [path]
-                                       (util-fs/ignored-path? dir path))
+                                       (utils/ignored-path? dir path))
                             :ignoreInitial false
                             :ignorePermissionErrors true
                             :interval polling-interval
@@ -63,9 +67,7 @@
              (publish-file-event! dir path "change")))
       (.on watcher "unlink"
            (fn [path]
-             (send-file-watcher! dir "unlink"
-                                 {:dir (utils/fix-win-path! dir)
-                                  :path (utils/fix-win-path! path)})))
+             (publish-file-event! dir path "unlink")))
       (.on watcher "error"
            (fn [path]
              (println "Watch error happened: "

+ 13 - 13
src/electron/electron/utils.cljs

@@ -62,27 +62,27 @@
   (when-let [agent (cfgs/get-item :settings/agent)]
     (set-fetch-agent agent)))
 
-;; keep same as ignored-path? in src/main/frontend/util/fs.cljs
-;; TODO: merge them
 (defn ignored-path?
+  "Ignore given path from file-watcher notification"
   [dir path]
   (when (string? path)
     (or
      (some #(string/starts-with? path (str dir "/" %))
-           ["." ".recycle" "assets" "node_modules" "logseq/bak"])
+           ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
      (some #(string/includes? path (str "/" % "/"))
-           ["." ".recycle" "assets" "node_modules" "logseq/bak"])
-     (string/ends-with? path ".DS_Store")
+           ["." ".recycle" "node_modules" "logseq/bak" "version-files"])
+     (some #(string/ends-with? path %)
+           [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"])
      ;; hidden directory or file
      (let [relpath (path/relative dir path)]
        (or (re-find #"/\.[^.]+" relpath)
-           (re-find #"^\.[^.]+" relpath)))
-     (let [path (string/lower-case path)]
-       (and
-        (not (string/blank? (path/extname path)))
-        (not
-         (some #(string/ends-with? path %)
-               [".md" ".markdown" ".org" ".js" ".edn" ".css"])))))))
+           (re-find #"^\.[^.]+" relpath))))))
+
+(defn should-read-content?
+  "Skip reading content of file while using file-watcher"
+  [path]
+  (let [ext (string/lower-case (path/extname path))]
+    (contains? #{".md" ".markdown" ".org" ".js" ".edn" ".css"} ext)))
 
 (defn fix-win-path!
   [path]
@@ -131,4 +131,4 @@
 
 (defn normalize-lc
   [s]
-  (normalize (string/lower-case s)))
+  (normalize (string/lower-case s)))

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

@@ -273,21 +273,16 @@
      ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
      ["Calculator" [[:editor/input "```calc\n\n```" {:backward-pos 4}]
                     [:codemirror/focus]] "Insert a calculator"]
+     
      ["draw" (draw-handler/initialize-excalidarw-file) "Draw a graph with Excalidraw"]
-
-     (when (util/zh-CN-supported?)
-       ["Embed Bilibili video" [[:editor/input "{{bilibili }}" {:last-pattern (state/get-editor-command-trigger)
-                                                                :backward-pos 2}]]])
+     
      ["Embed HTML " (->inline "html")]
 
-     ["Embed Youtube video" [[:editor/input "{{youtube }}" {:last-pattern (state/get-editor-command-trigger)
-                                                            :backward-pos 2}]]]
+     ["Embed Video URL" [[:editor/input "{{video }}" {:last-pattern (state/get-editor-command-trigger)
+                                                    :backward-pos 2}]]]
 
      ["Embed Youtube timestamp" [[:youtube/insert-timestamp]]]
 
-     ["Embed Vimeo video" [[:editor/input "{{vimeo }}" {:last-pattern (state/get-editor-command-trigger)
-                                                        :backward-pos 2}]]]
-
      ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern (state/get-editor-command-trigger)
                                                           :backward-pos 2}]]]]
 

+ 133 - 86
src/main/frontend/components/block.cljs

@@ -264,14 +264,14 @@
   (let [src (::src state)
         granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
         href (config/get-local-asset-absolute-path href)]
-    (when (or granted? (util/electron?) (mobile-util/is-native-platform?))
+    (when (or granted? (util/electron?) (mobile-util/native-platform?))
       (p/then (editor-handler/make-asset-url href) #(reset! src %)))
 
     (when @src
       (let [ext (keyword (util/get-file-ext @src))
             share-fn (fn [event]
                        (util/stop event)
-                       (when (mobile-util/is-native-platform?)
+                       (when (mobile-util/native-platform?)
                          (p/let [url (str (config/get-repo-dir (state/get-current-repo)) href)]
                            (.share Share #js {:url url
                                               :title "Open file with your favorite app"}))))]
@@ -884,7 +884,7 @@
                              (state/set-state! :pdf/current current)))}
          label-text]
 
-        (mobile-util/is-native-platform?)
+        (mobile-util/native-platform?)
         (asset-link config label-text s metadata full_text))
 
       (contains? (config/doc-formats) ext)
@@ -1113,42 +1113,80 @@
 (defn- macro-vimeo-cp
   [_config arguments]
   (when-let [url (first arguments)]
-    (let [Vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com)?)((?:/video/)?)([\w-]+)(\S+)?$"]
-      (when-let [vimeo-id (nth (util/safe-re-find Vimeo-regex url) 5)]
-        (when-not (string/blank? vimeo-id)
-          (let [width (min (- (util/get-width) 96)
-                           560)
-                height (int (* width (/ 315 560)))]
-            [:iframe
-             {:allow-full-screen "allowfullscreen"
-              :allow
-              "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
-              :frame-border "0"
-              :src (str "https://player.vimeo.com/video/" vimeo-id)
-              :height height
-              :width width}]))))))
+    (when-let [vimeo-id (nth (util/safe-re-find text/vimeo-regex url) 5)]
+      (when-not (string/blank? vimeo-id)
+        (let [width (min (- (util/get-width) 96)
+                         560)
+              height (int (* width (/ 315 560)))]
+          [:iframe
+           {:allow-full-screen "allowfullscreen"
+            :allow
+            "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+            :frame-border "0"
+            :src (str "https://player.vimeo.com/video/" vimeo-id)
+            :height height
+            :width width}])))))
 
 (defn- macro-bilibili-cp
   [_config arguments]
   (when-let [url (first arguments)]
-    (let [id-regex #"https?://www\.bilibili\.com/video/([^? ]+)"]
-      (when-let [id (cond
-                      (<= (count url) 15) url
-                      :else
-                      (last (util/safe-re-find id-regex url)))]
-        (when-not (string/blank? id)
-          (let [width (min (- (util/get-width) 96)
-                           560)
-                height (int (* width (/ 315 560)))]
-            [:iframe
-             {:allowfullscreen true
-              :framespacing "0"
-              :frameborder "no"
-              :border "0"
-              :scrolling "no"
-              :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
-              :width width
-              :height (max 500 height)}]))))))
+    (when-let [id (cond
+                    (<= (count url) 15) url
+                    :else
+                    (nth (util/safe-re-find text/bilibili-regex url) 5))]
+      (when-not (string/blank? id)
+        (let [width (min (- (util/get-width) 96)
+                         560)
+              height (int (* width (/ 315 560)))]
+          [:iframe
+           {:allowfullscreen true
+            :framespacing "0"
+            :frameborder "no"
+            :border "0"
+            :scrolling "no"
+            :src (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
+            :width width
+            :height (max 500 height)}])))))
+
+(defn- macro-video-cp
+  [_config arguments]
+  (when-let [url (first arguments)]
+    (let [width (min (- (util/get-width) 96)
+                     560)
+          height (int (* width (/ 315 560)))
+          results (text/get-matched-video url)
+          src (match results
+                     [_ _ _ (:or "youtube.com" "youtu.be" "y2u.be") _ id _]
+                     (if (= (count id) 11) ["youtube-player" id] url)
+
+                     [_ _ _ "youtube-nocookie.com" _ id _]
+                     (str "https://www.youtube-nocookie.com/embed/" id)
+
+                     [_ _ _ "loom.com" _ id _]
+                     (str "https://www.loom.com/embed/" id)
+
+                     [_ _ _ (_ :guard #(string/ends-with? % "vimeo.com")) _ id _]
+                     (str "https://player.vimeo.com/video/" id)
+
+                     [_ _ _ "bilibili.com" _ id _]
+                     (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1")
+
+                     :else
+                     url)]
+      (if (and (coll? src)
+               (= (first src) "youtube-player"))
+        (youtube/youtube-video (last src))
+        (when src
+          [:iframe
+           {:allowfullscreen true
+            :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+            :framespacing "0"
+            :frameborder "no"
+            :border "0"
+            :scrolling "no"
+            :src src
+            :width width
+            :height height}])))))
 
 (defn- macro-else-cp
   [name config arguments]
@@ -1261,13 +1299,12 @@
 
       (= name "youtube")
       (when-let [url (first arguments)]
-        (let [YouTube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)(\S+)?$"]
-          (when-let [youtube-id (cond
-                                  (== 11 (count url)) url
-                                  :else
-                                  (nth (util/safe-re-find YouTube-regex url) 5))]
-            (when-not (string/blank? youtube-id)
-              (youtube/youtube-video youtube-id)))))
+        (when-let [youtube-id (cond
+                                (== 11 (count url)) url
+                                :else
+                                (nth (util/safe-re-find text/youtube-regex url) 5))]
+          (when-not (string/blank? youtube-id)
+            (youtube/youtube-video youtube-id))))
 
       (= name "youtube-timestamp")
       (when-let [timestamp (first arguments)]
@@ -1290,6 +1327,9 @@
       (= name "bilibili")
       (macro-bilibili-cp config arguments)
 
+      (= name "video")
+      (macro-video-cp config arguments)
+      
       (contains? #{"tweet" "twitter"} name)
       (when-let [url (first arguments)]
         (let [id-regex #"/status/(\d+)"]
@@ -1535,7 +1575,7 @@
                                    "hide-inner-bullet"))}
                     [:span.bullet {:blockid (str uuid)}]]]]
        (cond
-         (and (or (mobile-util/is-native-platform?)
+         (and (or (mobile-util/native-platform?)
                   (:ui/show-empty-bullets? (state/get-config)))
               (not doc-mode?))
          bullet
@@ -1869,35 +1909,40 @@
   (.stopPropagation e)
   (let [target (gobj/get e "target")
         button (gobj/get e "buttons")
-        shift? (gobj/get e "shiftKey")]
-    (when (contains? #{1 0} button)
-      (when-not (target-forbidden-edit? target)
-        (if (and shift? (state/get-selection-start-block))
-          (editor-handler/highlight-selection-area! block-id)
-          (do
-            (editor-handler/clear-selection!)
-            (editor-handler/unhighlight-blocks!)
-            (let [f #(let [block (or (db/pull [:block/uuid (:block/uuid block)]) block)
-                           cursor-range (util/caret-range (gdom/getElement block-id))
-                           {:block/keys [content format]} block
-                           content (->> content
-                                        (property/remove-built-in-properties format)
-                                        (drawer/remove-logbook))]
-                       ;; save current editing block
-                       (let [{:keys [value] :as state} (editor-handler/get-state)]
-                         (editor-handler/save-block! state value))
-                       (state/set-editing!
-                        edit-input-id
-                        content
-                        block
-                        cursor-range
-                        false))]
-              ;; wait a while for the value of the caret range
-              (if (util/ios?)
-                (f)
-                (js/setTimeout f 5))
-
-              (when block-id (state/set-selection-start-block! block-id)))))))))
+        shift? (gobj/get e "shiftKey")
+        meta? (util/meta-key? e)]
+    (if (and meta? (not (state/get-edit-input-id)))
+      (do
+        (util/stop e)
+        (state/conj-selection-block! (gdom/getElement block-id) :down))
+      (when (contains? #{1 0} button)
+        (when-not (target-forbidden-edit? target)
+          (if (and shift? (state/get-selection-start-block))
+            (editor-handler/highlight-selection-area! block-id)
+            (do
+              (editor-handler/clear-selection!)
+              (editor-handler/unhighlight-blocks!)
+              (let [f #(let [block (or (db/pull [:block/uuid (:block/uuid block)]) block)
+                             cursor-range (util/caret-range (gdom/getElement block-id))
+                             {:block/keys [content format]} block
+                             content (->> content
+                                          (property/remove-built-in-properties format)
+                                          (drawer/remove-logbook))]
+                         ;; save current editing block
+                         (let [{:keys [value] :as state} (editor-handler/get-state)]
+                           (editor-handler/save-block! state value))
+                         (state/set-editing!
+                          edit-input-id
+                          content
+                          block
+                          cursor-range
+                          false))]
+                ;; wait a while for the value of the caret range
+                (if (util/ios?)
+                  (f)
+                  (js/setTimeout f 5))
+
+                (when block-id (state/set-selection-start-block! block-id))))))))))
 
 (rum/defc dnd-separator-wrapper < rum/reactive
   [block block-id slide? top? block-content?]
@@ -1970,7 +2015,8 @@
                              (when (and
                                     (state/in-selection-mode?)
                                     (not (string/includes? content "```"))
-                                    (not (gobj/get e "shiftKey")))
+                                    (not (gobj/get e "shiftKey"))
+                                    (not (util/meta-key? e)))
                                ;; clear highlighted text
                                (util/clear-selection!)))}
        (not slide?)
@@ -2227,20 +2273,21 @@
 
 (defn- block-mouse-over
   [uuid e *control-show? block-id doc-mode?]
-  (util/stop e)
-  (when (or
-         (model/block-collapsed? uuid)
-         (editor-handler/collapsable? uuid {:semantic? true}))
-    (reset! *control-show? true))
-  (when-let [parent (gdom/getElement block-id)]
-    (let [node (.querySelector parent ".bullet-container")]
-      (when doc-mode?
-        (dom/remove-class! node "hide-inner-bullet"))))
-  (when (and
-         (state/in-selection-mode?)
-         (non-dragging? e))
+  (when-not @*dragging?
     (util/stop e)
-    (editor-handler/highlight-selection-area! block-id)))
+    (when (or
+           (model/block-collapsed? uuid)
+           (editor-handler/collapsable? uuid {:semantic? true}))
+      (reset! *control-show? true))
+    (when-let [parent (gdom/getElement block-id)]
+      (let [node (.querySelector parent ".bullet-container")]
+        (when doc-mode?
+          (dom/remove-class! node "hide-inner-bullet"))))
+    (when (and
+           (state/in-selection-mode?)
+           (non-dragging? e))
+      (util/stop e)
+      (editor-handler/highlight-selection-area! block-id))))
 
 (defn- block-mouse-leave
   [e *control-show? block-id doc-mode?]

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

@@ -185,14 +185,6 @@
   }
 }
 
-html.is-mobile,
-html.is-native-iphone,
-html.is-native-android {
-  .references .block-control {
-    margin-left: -20px;
-  }
-}
-
 .block-ref {
   border-bottom: 0.5px solid;
   border-bottom-color: var(--ls-block-ref-link-text-color);

+ 2 - 95
src/main/frontend/components/editor.cljs

@@ -7,9 +7,6 @@
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.search :as search]
             [frontend.components.svg :as svg]
-            [frontend.mobile.camera :as mobile-camera]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.extensions.zotero :as zotero]
@@ -27,9 +24,7 @@
             [goog.dom :as gdom]
             [promesa.core :as p]
             [rum.core :as rum]
-            [frontend.handler.history :as history]
-            [frontend.mobile.footer :as footer]
-            [frontend.handler.config :as config-handler]))
+            [frontend.mobile.footer :as footer]))
 
 (rum/defc commands < rum/reactive
   [id format]
@@ -232,88 +227,6 @@
                            template)
             :class       "black"}))))))
 
-(rum/defc mobile-bar-indent-outdent [indent? icon]
-  [:div
-   [:button.bottom-action
-    {:on-mouse-down (fn [e]
-                      (util/stop e)
-                      (editor-handler/indent-outdent indent?))}
-    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
-
-(def ^:private mobile-bar-icons-keywords
-  [:checkbox :brackets :parentheses :command :tag :a-b :list :camera
-   :brand-youtube :link :rotate :rotate-clockwise :code :bold :italic :strikethrough :paint])
-
-(def ^:private mobile-bar-commands-stats
-  (atom (into {}
-              (mapv (fn [name] [name {:counts 0}])
-                    mobile-bar-icons-keywords))))
-
-(defn set-command-stats [icon]
-  (let [key (keyword icon)
-        counts (get-in @mobile-bar-commands-stats [key :counts])]
-    (swap! mobile-bar-commands-stats
-           assoc-in [key :counts] (inc counts))
-    (config-handler/set-config!
-     :mobile/toolbar-stats @mobile-bar-commands-stats)))
-
-(rum/defc mobile-bar-command
-  [command-handler icon & [count? event?]]
-  [:div
-   [:button.bottom-action
-    {:on-mouse-down (fn [e]
-                      (util/stop e)
-                      (when count?
-                        (set-command-stats icon))
-                      (if event?
-                        (command-handler e)
-                        (command-handler)))}
-    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
-
-(defn mobile-bar-commands
-  [_parent-state parent-id]
-  (let [viewport-fn (fn [] (when-let [input (gdom/getElement parent-id)]
-                             (util/make-el-cursor-position-into-center-viewport input)
-                             (.focus input)))]
-    (zipmap mobile-bar-icons-keywords
-     [(mobile-bar-command editor-handler/cycle-todo! "checkbox" true)
-      (mobile-bar-command #(editor-handler/toggle-page-reference-embed parent-id) "brackets" true)
-      (mobile-bar-command #(editor-handler/toggle-block-reference-embed parent-id) "parentheses" true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "/" {})) "command" true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "#" {})) "tag" true)
-      (mobile-bar-command editor-handler/cycle-priority! "a-b" true)
-      (mobile-bar-command editor-handler/toggle-list! "list" true)
-      (mobile-bar-command #(mobile-camera/embed-photo parent-id) "camera" true)
-      (mobile-bar-command commands/insert-youtube-timestamp "brand-youtube" true)
-      (mobile-bar-command editor-handler/html-link-format! "link" true)
-      (mobile-bar-command history/undo! "rotate" true true)
-      (mobile-bar-command history/redo! "rotate-clockwise" true true)
-      (mobile-bar-command #(do (viewport-fn) (commands/simple-insert! parent-id "<" {})) "code" true)
-      (mobile-bar-command editor-handler/bold-format! "bold" true)
-      (mobile-bar-command editor-handler/italics-format! "italic" true)
-      (mobile-bar-command editor-handler/strike-through-format! "strikethrough" true)
-      (mobile-bar-command editor-handler/highlight-format! "paint" true)])))
-
-(rum/defc mobile-bar < rum/reactive
-  [parent-state parent-id]
-  (when-let [config-toolbar-stats (:mobile/toolbar-stats (state/get-config))]
-   (reset! mobile-bar-commands-stats config-toolbar-stats))
-  (let [commands (mobile-bar-commands parent-state parent-id)
-        sorted-commands (sort-by (comp :counts second) > @mobile-bar-commands-stats)]
-    [:div#mobile-editor-toolbar.bg-base-2
-     [:div.toolbar-commands
-      (mobile-bar-indent-outdent false "arrow-bar-left")
-      (mobile-bar-indent-outdent true "arrow-bar-right")
-      (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 #(if (state/sub :document/mode?)
-                             (editor-handler/insert-new-block! nil)
-                             (commands/simple-insert! parent-id "\n" {})) "arrow-back")
-      (for [command sorted-commands]
-        ((first command) commands))]
-     [:div.toolbar-hide-keyboard
-      (mobile-bar-command #(state/clear-edit!) "keyboard-show")]]))
-
 (rum/defcs input < rum/reactive
   (rum/local {} ::input-value)
   (mixins/event-mixin
@@ -607,7 +520,7 @@
   (mixins/event-mixin setup-key-listener!)
   (shortcut/mixin :shortcut.handler/block-editing-only)
   lifecycle/lifecycle
-  [state {:keys [format block]} id config]
+  [state {:keys [format block]} id _config]
   (let [content (state/sub-edit-content)
         heading-class (get-editor-style-class content format)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
@@ -616,11 +529,6 @@
        [:div#audio-record-toolbar
         (footer/audio-record-cp)])
 
-     (when (and (or (mobile-util/is-native-platform?)
-                    config/mobile?)
-                (not (:review-cards? config)))
-       (mobile-bar state id))
-
      (ui/ls-textarea
       {:id                id
        :cacheMeasurements (editor-row-height-unchanged?) ;; check when content updated (as the content variable is binded)
@@ -629,7 +537,6 @@
        :on-click          (editor-handler/editor-on-click! id)
        :on-change         (editor-handler/editor-on-change! block id search-timeout)
        :on-paste          (editor-handler/editor-on-paste! id)
-       :on-height-change  (editor-handler/editor-on-height-change! id)
        :auto-focus        false
        :class             heading-class})
 

+ 0 - 30
src/main/frontend/components/editor.css

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

+ 3 - 5
src/main/frontend/components/header.cljs

@@ -224,11 +224,10 @@
   (let [repos (->> (state/sub [:me :repos])
                    (remove #(= (:url %) config/local-repo)))
         electron-mac? (and util/mac? (util/electron?))
-        vw-state (state/sub :ui/visual-viewport-state)
         show-open-folder? (and (nfs/supported?)
                                (or (empty? repos)
                                    (nil? (state/sub :git/current-repo)))
-                               (not (mobile-util/is-native-platform?))
+                               (not (mobile-util/native-platform?))
                                (not config/publishing?))]
     [:div.cp__header#head
      {:class           (util/classnames [{:electron-mac   electron-mac?
@@ -239,8 +238,7 @@
                            (when (and (util/electron?)
                                       (.. target -classList (contains "cp__header")))
                              (js/window.apis.toggleMaxOrMinActiveWindow))))
-      :style           {:fontSize  50
-                        :transform (str "translateY(" (or (:offset-top vw-state) 0) "px)")}}
+      :style           {:fontSize  50}}
      [:div.l.flex
       (left-menu-button {:on-click (fn []
                                      (open-fn)
@@ -271,7 +269,7 @@
                 (mobile-util/native-ios?))
         (back-and-forward))
 
-      (when-not (mobile-util/is-native-platform?)
+      (when-not (mobile-util/native-platform?)
         (new-block-mode))
 
       (when show-open-folder?

+ 4 - 1
src/main/frontend/components/header.css

@@ -16,6 +16,7 @@
   user-select: none;
   line-height: 1;
   white-space: nowrap;
+  background-color: var(--ls-primary-background-color);
 
   > .l {
     width: var(--ls-left-sidebar-width);
@@ -221,7 +222,9 @@ html.is-native-iphone-without-notch,
 html.is-native-ipad {
 
      #main-container {
-        padding-top: 0px;
+         padding-top: 0px;
+         display: flex;
+         flex-direction: column;
     }
 
      #main-content-container {

+ 2 - 2
src/main/frontend/components/onboarding/setups.cljs

@@ -71,10 +71,10 @@
       [:section.a
        [:strong "Let’s get you set up."]
        [:small (str "Where on your " DEVICE " do you want to save your work?")
-        (when (mobile-util/is-native-platform?)
+        (when (mobile-util/native-platform?)
           (mobile-intro))]
 
-       (if (or (nfs/supported?) (mobile-util/is-native-platform?))
+       (if (or (nfs/supported?) (mobile-util/native-platform?))
          [:div.choose.flex.flex-col.items-center
           {:on-click #(page-handler/ls-dir-files!
                        (fn []

+ 2 - 4
src/main/frontend/components/page.cljs

@@ -183,7 +183,7 @@
         (ui/foldable
          [:h2.font-bold.opacity-50 (util/format "Pages tagged with \"%s\"" tag)]
          [:ul.mt-2
-          (for [[original-name name] (sort pages)]
+          (for [[original-name name] (sort-by last pages)]
             [:li {:key (str "tagged-page-" name)}
              [:a {:href (rfe/href :page {:name name})}
               original-name]])]
@@ -231,7 +231,6 @@
                     (when (gp-util/wrapped-by-quotes? @*title-value)
                       (swap! *title-value gp-util/unquote-string)
                       (gobj/set (rum/deref input-ref) "value" @*title-value))
-                    (state/set-state! :editor/editing-page-title? false)
                     (cond
                       (= old-name @*title-value)
                       (reset! *edit? false)
@@ -266,7 +265,6 @@
                               (reset! *title-value old-name)
                               (reset! *edit? false)))}]]
         [:a.page-title {:on-mouse-down (fn [e]
-                                         (state/set-state! :editor/editing-page-title? true)
                                          (when (util/right-click? e)
                                            (state/set-state! :page-title/context {:page page-name})))
                         :on-click (fn [e]
@@ -364,7 +362,7 @@
        [:div.relative
         (when (and (not sidebar?) (not block?))
           [:div.flex.flex-row.space-between
-           (when (or (mobile-util/is-native-platform?) (util/mobile?))
+           (when (or (mobile-util/native-platform?) (util/mobile?))
              [:div.flex.flex-row.pr-2
               {:style {:margin-left -15}
                :on-mouse-over (fn [e]

+ 2 - 2
src/main/frontend/components/page_menu.cljs

@@ -84,7 +84,7 @@
                          (page-handler/unfavorite-page! page-original-name)
                          (page-handler/favorite-page! page-original-name)))}}
 
-          (when-not (mobile-util/is-native-platform?)
+          (when-not (mobile-util/native-platform?)
             {:title (t :page/presentation-mode)
              :options {:on-click (fn []
                                    (state/sidebar-add-block!
@@ -103,7 +103,7 @@
               :options {:on-click #(js/window.apis.openPath file-path)}}])
 
           (when (or (util/electron?)
-                    (mobile-util/is-native-platform?))
+                    (mobile-util/native-platform?))
             {:title   (t :page/copy-page-url)
              :options {:on-click #(util/copy-to-clipboard!
                                    (url-util/get-logseq-graph-page-url nil repo page-original-name))}})

+ 2 - 2
src/main/frontend/components/repo.cljs

@@ -44,7 +44,7 @@
        [:div.pl-1.content.mt-3
         [:div.flex.flex-row.my-4
          (when (or (nfs-handler/supported?)
-                   (mobile-util/is-native-platform?))
+                   (mobile-util/native-platform?))
            [:div.mr-8
             (ui/button
               (t :open-a-directory)
@@ -102,7 +102,7 @@
                        (when (and nfs-repo?
                                   (not= current-repo config/local-repo)
                                   (or (nfs-handler/supported?)
-                                      (mobile-util/is-native-platform?)))
+                                      (mobile-util/native-platform?)))
                          {:title (t :sync-from-local-files)
                           :hover-detail (t :sync-from-local-files-detail)
                           :options {:on-click

+ 6 - 6
src/main/frontend/components/settings.cljs

@@ -132,7 +132,7 @@
                               :href     href
                               :on-click on-click))]
     (when-not (or (util/mobile?)
-                  (mobile-util/is-native-platform?))
+                  (mobile-util/native-platform?))
       [:div.text-sm desc])]])
 
 (defn edit-config-edn []
@@ -161,7 +161,7 @@
      (ui/toggle show-brackets?
                 config-handler/toggle-ui-show-brackets!
                 true)]]
-   (when (not (or (util/mobile?) (mobile-util/is-native-platform?)))
+   (when (not (or (util/mobile?) (mobile-util/native-platform?)))
      [:div {:style {:text-align "right"}}
       (ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-brackets))])])
 
@@ -543,9 +543,9 @@
      (show-brackets-row t show-brackets?)
      (when (util/electron?) (switch-spell-check-row t))
      (outdenting-row t logical-outdenting?)
-     (when-not (or (util/mobile?) (mobile-util/is-native-platform?))
+     (when-not (or (util/mobile?) (mobile-util/native-platform?))
        (shortcut-tooltip-row t enable-shortcut-tooltip?))
-     (when-not (or (util/mobile?) (mobile-util/is-native-platform?))
+     (when-not (or (util/mobile?) (mobile-util/native-platform?))
        (tooltip-row t enable-tooltip?))
      (timetracking-row t enable-timetracking?)
      (journal-row t enable-journals?)
@@ -595,7 +595,7 @@
     [:div.panel-wrap.is-advanced
      (when (and util/mac? (util/electron?)) (app-auto-update-row t))
      (usage-diagnostics-row t instrument-disabled?)
-     (when-not (mobile-util/is-native-platform?) (developer-mode-row t developer-mode?))
+     (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
      (when (util/electron?) (plugin-system-switcher-row))
      (when (util/electron?) (https-user-agent-row https-agent-opts))
      (clear-cache-row t)
@@ -633,7 +633,7 @@
         (for [[label text icon]
               [[:general (t :settings-page/tab-general) (ui/icon "adjustments" {:style {:font-size 20}})]
                [:editor (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})]
-               (when-not (mobile-util/is-native-platform?)
+               (when-not (mobile-util/native-platform?)
                  [:git (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
                [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
                (when plugins-of-settings

+ 25 - 22
src/main/frontend/components/sidebar.cljs

@@ -4,37 +4,38 @@
             [frontend.components.command-palette :as command-palette]
             [frontend.components.header :as header]
             [frontend.components.journal :as journal]
+            [frontend.components.onboarding :as onboarding]
+            [frontend.components.plugins :as plugins]
             [frontend.components.repo :as repo]
             [frontend.components.right-sidebar :as right-sidebar]
+            [frontend.components.select :as select]
+            [frontend.components.svg :as svg]
             [frontend.components.theme :as theme]
             [frontend.components.widgets :as widgets]
-            [frontend.components.plugins :as plugins]
-            [frontend.components.select :as select]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
-            [frontend.db.model :as db-model]
-            [frontend.components.svg :as svg]
             [frontend.db-mixins :as db-mixins]
+            [frontend.db.model :as db-model]
+            [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.extensions.srs :as srs]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.route :as route-handler]
+            [frontend.handler.mobile.swipe :as swipe]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
             [frontend.mixins :as mixins]
+            [frontend.mobile.footer :as footer]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.mobile.mobile-bar :refer [mobile-bar]]
             [frontend.modules.shortcut.data-helper :as shortcut-dh]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [reitit.frontend.easy :as rfe]
             [goog.dom :as gdom]
             [goog.object :as gobj]
-            [rum.core :as rum]
-            [frontend.extensions.srs :as srs]
-            [frontend.extensions.pdf.assets :as pdf-assets]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.handler.mobile.swipe :as swipe]
-            [frontend.components.onboarding :as onboarding]
-            [frontend.mobile.footer :as footer]))
+            [reitit.frontend.easy :as rfe]
+            [rum.core :as rum]))
 
 (rum/defc nav-content-item
   [name {:keys [class]} child]
@@ -305,7 +306,10 @@
         :data-is-full-width        (or margin-less-pages?
                                         (contains? #{:all-files :all-pages :my-publishing} route-name))}
 
-       (when (and (not (mobile-util/is-native-platform?))
+       (mobile-bar)
+       (footer/footer)
+
+       (when (and (not (mobile-util/native-platform?))
                   (contains? #{:page :home} route-name))
          (widgets/demo-graph-alert))
 
@@ -378,7 +382,10 @@
                                               [page :page])]
                      (state/sidebar-add-block! current-repo db-id block-type)))
                  (reset! sidebar-inited? true))))
-           state)}
+           state)
+   :did-mount (fn [state]
+                (state/set-state! :mobile/show-tabbar? true)
+                state)}
   []
   (let [default-home (get-default-home-if-valid)
         current-repo (state/sub :git/current-repo)
@@ -465,7 +472,8 @@
 (defn- hide-context-menu-and-clear-selection
   [e]
   (state/hide-custom-context-menu!)
-  (when-not (gobj/get e "shiftKey")
+  (when-not (or (gobj/get e "shiftKey")
+                (util/meta-key? e))
     (editor-handler/clear-selection!)))
 
 (rum/defcs ^:large-vars/cleanup-todo sidebar <
@@ -550,12 +558,7 @@
                :indexeddb-support?  indexeddb-support?
                :light?              light?
                :db-restoring?       db-restoring?
-               :main-content        main-content})
-
-        (when (and (mobile-util/is-native-platform?)
-                   current-repo
-                   (not (state/sub :modal/show?)))
-          (footer/footer))]
+               :main-content        main-content})]
 
        (right-sidebar/sidebar)
 

+ 2 - 2
src/main/frontend/components/widgets.cljs

@@ -12,8 +12,8 @@
   []
   [:div.flex.flex-col
    [:h1.title (t :on-boarding/add-graph)]
-   (let [nfs-supported? (or (nfs/supported?) (mobile-util/is-native-platform?))]
-     (if (mobile-util/is-native-platform?)
+   (let [nfs-supported? (or (nfs/supported?) (mobile-util/native-platform?))]
+     (if (mobile-util/native-platform?)
        [:div.text-sm
         (ui/button "Open a local directory"
           :on-click #(page-handler/ls-dir-files! shortcut/refresh!))

+ 2 - 2
src/main/frontend/config.cljs

@@ -315,7 +315,7 @@
     (and (util/electron?) (local-db? repo-url))
     (get-local-dir repo-url)
 
-    (and (mobile-util/is-native-platform?) (local-db? repo-url))
+    (and (mobile-util/native-platform?) (local-db? repo-url))
     (let [dir (get-local-dir repo-url)]
       (if (string/starts-with? dir "file:")
         dir
@@ -328,7 +328,7 @@
 
 (defn get-repo-path
   [repo-url path]
-  (if (and (or (util/electron?) (mobile-util/is-native-platform?))
+  (if (and (or (util/electron?) (mobile-util/native-platform?))
            (local-db? repo-url))
     path
     (util/node-path.join (get-repo-dir repo-url) path)))

+ 2 - 2
src/main/frontend/db/conn.cljs

@@ -23,7 +23,7 @@
 (defn get-repo-name
   [repo]
   (cond
-    (mobile-util/is-native-platform?)
+    (mobile-util/native-platform?)
     (text/get-graph-name-from-path repo)
 
     (config/local-db? repo)
@@ -36,7 +36,7 @@
   "repo-path: output of `get-repo-name`"
   [repo-path]
   (if (or (util/electron?)
-          (mobile-util/is-native-platform?))
+          (mobile-util/native-platform?))
     (text/get-file-basename repo-path)
     repo-path))
 

+ 74 - 0
src/main/frontend/db/model.cljs

@@ -409,6 +409,29 @@
                      f))
                  form))
 
+(defn get-sorted-page-block-ids
+  [page-id]
+  (let [root (db-utils/entity page-id)]
+    (loop [result []
+           children (sort-by-left (:block/_parent root) root)]
+      (if (seq children)
+        (let [child (first children)]
+          (recur (conj result (:db/id child))
+                 (concat
+                  (sort-by-left (:block/_parent child) child)
+                  (rest children))))
+        result))))
+
+(defn sort-page-random-blocks
+  "Blocks could be non consecutive."
+  [blocks]
+  (assert (every? #(= (:block/page %) (:block/page (first blocks))) blocks) "Blocks must to be in a same page.")
+  (let [page-id (:db/id (:block/page (first blocks)))
+        ;; TODO: there's no need to sort all the blocks
+        sorted-ids (get-sorted-page-block-ids page-id)
+        blocks-map (zipmap (map :db/id blocks) blocks)]
+    (keep blocks-map sorted-ids)))
+
 (defn has-children?
   ([block-id]
    (has-children? (conn/get-db) block-id))
@@ -584,6 +607,57 @@
               (recur parent)))
           false)))))
 
+(defn get-prev-sibling
+  [db id]
+  (when-let [e (d/entity db id)]
+    (let [left (:block/left e)]
+      (when (not= (:db/id left) (:db/id (:block/parent e)))
+        left))))
+
+(defn get-right-sibling
+  [db db-id]
+  (when-let [block (d/entity db db-id)]
+    (get-by-parent-&-left db
+                          (:db/id (:block/parent block))
+                          db-id)))
+
+(defn last-child-block?
+  "The child block could be collapsed."
+  [db parent-id child-id]
+  (when-let [child (d/entity db child-id)]
+    (cond
+      (= parent-id child-id)
+      true
+
+      (get-right-sibling db child-id)
+      false
+
+      :else
+      (last-child-block? db parent-id (:db/id (:block/parent child))))))
+
+(defn- consecutive-block?
+  [block-1 block-2]
+  (let [db (conn/get-db)
+        aux-fn (fn [block-1 block-2]
+                 (and (= (:block/page block-1) (:block/page block-2))
+                      (or
+                       ;; sibling or child
+                       (= (:db/id (:block/left block-2)) (:db/id block-1))
+                       (when-let [prev-sibling (get-prev-sibling db (:db/id block-2))]
+                         (last-child-block? db (:db/id prev-sibling) (:db/id block-1))))))]
+    (or (aux-fn block-1 block-2) (aux-fn block-2 block-1))))
+
+(defn get-non-consecutive-blocks
+  [blocks]
+  (vec
+   (keep-indexed
+    (fn [i _block]
+      (when (< (inc i) (count blocks))
+        (when-not (consecutive-block? (nth blocks i)
+                                      (nth blocks (inc i)))
+          (nth blocks i))))
+    blocks)))
+
 (defn- get-start-id-for-pagination-query
   [repo-url current-db {:keys [db-before tx-meta] :as tx-report}
    result outliner-op page-id block-id tx-block-ids]

+ 3 - 3
src/main/frontend/extensions/video/youtube.cljs

@@ -109,7 +109,7 @@
 (defn gen-youtube-ts-macro []
   (if-let [player (get-player (state/get-input))]
     (util/format "{{youtube-timestamp %s}}" (Math/floor (.getCurrentTime ^js player)))
-    (when (mobile-util/is-native-platform?)
+    (when (mobile-util/native-platform?)
       (notification/show!
        "Please embed a YouTube video at first, then use this icon.
 Remember: You can paste a raw YouTube url as embedded video on mobile."
@@ -123,9 +123,9 @@ Remember: You can paste a raw YouTube url as embedded video on mobile."
         reg-number #"^\d+$"
         timestamp (str timestamp)
         total-seconds (-> (re-matches reg-number timestamp)
-                          parse-long)
+                          util/safe-parse-int)
         [_ hours minutes seconds] (re-matches reg timestamp)
-        [hours minutes seconds] (map parse-long [hours minutes seconds])]
+        [hours minutes seconds] (map util/safe-parse-int [hours minutes seconds])]
     (cond
       total-seconds
       total-seconds

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

@@ -32,7 +32,7 @@
       (and (util/electron?) (not bfs-local?))
       node-record
 
-      (mobile-util/is-native-platform?)
+      (mobile-util/native-platform?)
       mobile-record
 
       (local-db? dir)
@@ -109,7 +109,7 @@
 
     :else
     (let [[old-path new-path]
-          (map #(if (or (util/electron?) (mobile-util/is-native-platform?))
+          (map #(if (or (util/electron?) (mobile-util/native-platform?))
                   %
                   (str (config/get-repo-dir repo) "/" %))
                [old-path new-path])]
@@ -125,7 +125,7 @@
     (util/electron?)
     node-record
 
-    (mobile-util/is-native-platform?)
+    (mobile-util/native-platform?)
     mobile-record
 
     :else
@@ -136,7 +136,7 @@
   (let [record (get-record)]
     (p/let [result (protocol/open-dir record ok-handler)]
       (if (or (util/electron?)
-              (mobile-util/is-native-platform?))
+              (mobile-util/native-platform?))
         (let [[dir & paths] (bean/->clj result)]
           [(:path dir) paths])
         result))))
@@ -145,7 +145,7 @@
   [path-or-handle ok-handler]
   (let [record (get-record)
         electron? (util/electron?)
-        mobile? (mobile-util/is-native-platform?)]
+        mobile? (mobile-util/native-platform?)]
     (p/let [result (protocol/get-files record path-or-handle ok-handler)]
       (if (or electron? mobile?)
         (let [result (bean/->clj result)]

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

@@ -93,7 +93,7 @@
            (and (not (seq (db/get-files config/local-repo)))
                 ;; Not native local directory
                 (not (some config/local-db? (map :url repos)))
-                (not (mobile-util/is-native-platform?)))
+                (not (mobile-util/native-platform?)))
            ;; will execute `(state/set-db-restoring! false)` inside
            (repo-handler/setup-local-repo-if-not-exists!)
 
@@ -193,7 +193,7 @@
     (p/let [repos (get-repos)]
       (state/set-repos! repos)
       (restore-and-setup! repos db-schema)
-      (when (mobile-util/is-native-platform?)
+      (when (mobile-util/native-platform?)
         (p/do! (mobile-util/hide-splash))))
 
     (reset! db/*sync-search-indice-f search/sync-search-indice!)

+ 37 - 43
src/main/frontend/handler/editor.cljs

@@ -1,13 +1,15 @@
 (ns frontend.handler.editor
   (:require ["/frontend/utils" :as utils]
+            ["path" :as path]
             [cljs.core.match :refer [match]]
             [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as w]
             [dommy.core :as dom]
             [frontend.commands :as commands
-             :refer [*angle-bracket-caret-pos *show-block-commands
-                     *show-commands *slash-caret-pos]]
+             :refer [*angle-bracket-caret-pos
+                     *show-block-commands *show-commands
+                     *slash-caret-pos]]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
@@ -25,12 +27,12 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
-            [frontend.image :as image]
             [frontend.idb :as idb]
+            [frontend.image :as image]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.core :as outliner-core]
-            [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.transaction :as outliner-tx]
+            [frontend.modules.outliner.tree :as tree]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.template :as template]
@@ -40,21 +42,20 @@
             [frontend.util.clock :as clock]
             [frontend.util.cursor :as cursor]
             [frontend.util.drawer :as drawer]
+            [frontend.util.keycode :as keycode]
+            [frontend.util.list :as list]
             [frontend.util.marker :as marker]
-            [frontend.util.property :as property]
             [frontend.util.priority :as priority]
+            [frontend.util.property :as property]
             [frontend.util.thingatpt :as thingatpt]
-            [frontend.util.list :as list]
             [goog.dom :as gdom]
             [goog.dom.classes :as gdom-classes]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]
-            [frontend.util.keycode :as keycode]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.block :as gp-block]
-            ["path" :as path]))
+            [logseq.graph-parser.block :as gp-block]))
 
 ;; FIXME: should support multiple images concurrently uploading
 
@@ -1411,7 +1412,7 @@
       (util/electron?)
       (str "assets://" repo-dir path)
 
-      (mobile-util/is-native-platform?)
+      (mobile-util/native-platform?)
       (mobile-util/convert-file-src (str repo-dir path))
 
       :else
@@ -1677,7 +1678,8 @@
             (move-nodes blocks))
           (when-let [input-id (state/get-edit-input-id)]
             (when-let [input (gdom/getElement input-id)]
-              (.focus input))))
+              (.focus input)
+              (js/setTimeout #(util/scroll-editor-cursor input) 100))))
         (let [ids (state/get-selection-block-ids)]
           (when (seq ids)
             (let [lookup-refs (map (fn [id] [:block/uuid id]) ids)
@@ -2630,7 +2632,7 @@
         ;; 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-util/is-native-platform?)
+        (and (mobile-util/native-platform?)
              (= key "Backspace")
              (= value ""))
         (do
@@ -2813,24 +2815,9 @@
   [id]
   (fn [_e]
     (let [input (gdom/getElement id)]
+      (util/scroll-editor-cursor input)
       (close-autocomplete-if-outside input))))
 
-(defonce mobile-toolbar-height 40)
-(defn editor-on-height-change!
-  [id]
-  (fn [box-height ^js row-height]
-    (let [row-height (:rowHeight (js->clj row-height :keywordize-keys true))
-          input (gdom/getElement id)
-          caret (cursor/get-caret-pos input)
-          cursor-bottom (if caret (+ row-height (:top caret)) box-height)
-          box-top (gobj/get (.getBoundingClientRect input) "top")
-          cursor-y (+ cursor-bottom box-top)
-          vw-height (.-height js/window.visualViewport)]
-      (when (<  vw-height (+ cursor-y mobile-toolbar-height))
-        (let [main-node (gdom/getElement "main-content-container")
-              scroll-top (.-scrollTop main-node)]
-          (set! (.-scrollTop main-node) (+ scroll-top row-height)))))))
-
 (defn editor-on-change!
   [block id search-timeout]
   (fn [e]
@@ -2842,7 +2829,9 @@
                 (js/setTimeout
                  #(edit-box-on-change! e block id)
                  timeout)))
-      (edit-box-on-change! e block id))))
+      (let [input (gdom/getElement id)]
+        (edit-box-on-change! e block id)
+        (util/scroll-editor-cursor input)))))
 
 (defn- paste-text-parseable
   [format text]
@@ -2878,6 +2867,18 @@
         (recur (remove (set (map :block/uuid result)) (rest ids)) result))
       result)))
 
+(defn wrap-macro-url
+  [url]
+  (cond
+    (boolean (text/get-matched-video url))
+    (util/format "{{video %s}}" url)
+
+    (string/includes? url "twitter.com")
+    (util/format "{{twitter %s}}" url)
+
+    :else
+    (notification/show! (util/format "No macro is available for %s" url) :warning)))
+
 (defn- paste-copied-blocks-or-text
   [text e]
   (let [copied-blocks (state/get-copied-blocks)
@@ -2905,18 +2906,7 @@
       (and (gp-util/url? text)
            (not (string/blank? (util/get-selected-text))))
       (html-link-format! text)
-
-      (and (gp-util/url? text)
-           (or (string/includes? text "youtube.com")
-               (string/includes? text "youtu.be"))
-           (mobile-util/is-native-platform?))
-      (commands/simple-insert! (state/get-edit-input-id) (util/format "{{youtube %s}}" text) nil)
-
-      (and (gp-util/url? text)
-           (string/includes? text "twitter.com")
-           (mobile-util/is-native-platform?))
-      (commands/simple-insert! (state/get-edit-input-id) (util/format "{{twitter %s}}" text) nil)
-
+      
       (and (text/block-ref? text)
            (wrapped-by? input "((" "))"))
       (commands/simple-insert! (state/get-edit-input-id) (text/get-block-ref text) nil)
@@ -2953,7 +2943,10 @@
   (utils/getClipText
    (fn [clipboard-data]
      (when-let [_ (state/get-input)]
-       (state/append-current-edit-content! clipboard-data)))
+       (let [data (if (gp-util/url? clipboard-data)
+                        (wrap-macro-url clipboard-data)
+                        clipboard-data)]
+             (state/append-current-edit-content! data))))
    (fn [error]
      (js/console.error error))))
 
@@ -2964,7 +2957,8 @@
     (let [text (.getData (gobj/get e "clipboardData") "text")
           input (state/get-input)]
       (if-not (string/blank? text)
-        (if (thingatpt/org-admonition&src-at-point input)
+        (if (or (thingatpt/markdown-src-at-point input)
+                (thingatpt/org-admonition&src-at-point input))
           (when-not (mobile-util/native-ios?)
             (util/stop e)
             (paste-text-in-one-block-at-point))

+ 1 - 4
src/main/frontend/handler/editor/lifecycle.cljs

@@ -3,7 +3,6 @@
             [frontend.handler.editor.keyboards :as keyboards-handler]
             [frontend.state :as state]
             [frontend.util :as util]
-            [frontend.mobile.util :as mobile-util]
             [goog.dom :as gdom]))
 
 (defn did-mount!
@@ -21,9 +20,7 @@
 
     (when-let [element (gdom/getElement id)]
       (.focus element)
-      (when (or (mobile-util/is-native-platform?)
-                (util/mobile?))
-        (util/make-el-cursor-position-into-center-viewport element))))
+      (js/setTimeout #(util/scroll-editor-cursor element) 50)))
   state)
 
 (defn did-remount!

+ 23 - 10
src/main/frontend/handler/events.cljs

@@ -137,7 +137,7 @@
   [repo]
   (when
    (and (not (util/electron?))
-        (not (mobile-util/is-native-platform?)))
+        (not (mobile-util/native-platform?)))
     (fn [close-fn]
       [:div
        [:p
@@ -288,15 +288,28 @@
     (reset! st/*inited? true)
     (st/consume-pending-shortcuts!)))
 
-
-(defmethod handle :mobile/keyboard-will-show [[_]]
-  (when (and (state/get-left-sidebar-open?)
-             (state/editing?))
-    (state/set-left-sidebar-open! false)))
-
-(defmethod handle :mobile/keyboard-did-show [[_]]
-  (when-let [input (state/get-input)]
-    (util/make-el-cursor-position-into-center-viewport input)))
+(defmethod handle :mobile/keyboard-will-show [[_ keyboard-height]]
+  (let [main-node (util/app-scroll-container-node)]
+    (state/set-state! :mobile/show-tabbar? false)
+    (state/set-state! :mobile/show-toolbar? true)
+    (when (mobile-util/native-ios?)
+      (reset! util/keyboard-height keyboard-height)
+      (set! (.. main-node -style -marginBottom) (str keyboard-height "px"))
+      (when-let [card-preview-el (js/document.querySelector ".cards-review")]
+        (set! (.. card-preview-el -style -marginBottom) (str keyboard-height "px")))
+      (js/setTimeout (fn []
+                       (let [toolbar (.querySelector main-node "#mobile-editor-toolbar")]
+                         (set! (.. toolbar -style -bottom) (str keyboard-height "px"))))
+                     100))))
+
+(defmethod handle :mobile/keyboard-will-hide [[_]]
+  (let [main-node (util/app-scroll-container-node)]
+    (state/set-state! :mobile/show-toolbar? false)
+    (state/set-state! :mobile/show-tabbar? true)
+    (when (mobile-util/native-ios?)
+      (when-let [card-preview-el (js/document.querySelector ".cards-review")]
+        (set! (.. card-preview-el -style -marginBottom) "0px"))
+      (set! (.. main-node -style -marginBottom) "0px"))))
 
 (defmethod handle :plugin/consume-updates [[_ id pending? updated?]]
   (let [downloading? (:plugin/updates-downloading? @state/state)]

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

@@ -164,7 +164,7 @@
       (db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
       (->
        (p/let [_ (and (config/local-db? repo)
-                      (mobile-util/is-native-platform?)
+                      (mobile-util/native-platform?)
                       (fs/delete-file! repo file-path file-path {}))
                _ (fs/unlink! repo (config/get-repo-path repo file-path) nil)])
        (p/catch (fn [err]
@@ -594,7 +594,7 @@
                      page)
         (let [journal? (date/valid-journal-title? page)
               ref-file-path (str
-                             (if (or (util/electron?) (mobile-util/is-native-platform?))
+                             (if (or (util/electron?) (mobile-util/native-platform?))
                                (-> (config/get-repo-dir (state/get-current-repo))
                                    js/decodeURI
                                    (string/replace #"/+$" "")
@@ -728,7 +728,7 @@
                (not (state/loading-files? repo)))
       (state/set-today! (date/today))
       (when (or (config/local-db? repo)
-                (and (= "local" repo) (not (mobile-util/is-native-platform?))))
+                (and (= "local" repo) (not (mobile-util/native-platform?))))
         (let [title (date/today)
               today-page (util/page-name-sanity-lc title)
               format (state/get-preferred-format repo)

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

@@ -135,7 +135,7 @@
 
 (defn exec-js-if-exists-&-allowed!
   [t]
-  (when-not (mobile/is-native-platform?)
+  (when-not (mobile/native-platform?)
     (when-let [href (or
                      (state/get-custom-js-link)
                      (config/get-custom-js-path))]

+ 8 - 8
src/main/frontend/handler/web/nfs.cljs

@@ -26,12 +26,12 @@
             [frontend.encrypt :as encrypt]))
 
 (defn remove-ignore-files
-  [files]
+  [files dir-name nfs?]
   (let [files (remove (fn [f]
                         (let [path (:file/path f)]
                           (or (string/starts-with? path ".git/")
                               (string/includes? path ".git/")
-                              (and (util-fs/ignored-path? "" path)
+                              (and (util-fs/ignored-path? (if nfs? "" dir-name) path)
                                    (not= (:file/name f) ".gitignore")))))
                       files)]
     (if-let [ignore-file (some #(when (= (:file/name %) ".gitignore")
@@ -55,7 +55,7 @@
              :file/last-modified-at mtime
              :file/size             size
              :file/content content})
-       result)
+          result)
 
      electron?
      (map (fn [{:keys [path stat content]}]
@@ -64,7 +64,7 @@
                :file/last-modified-at mtime
                :file/size             size
                :file/content content}))
-       result)
+          result)
 
      :else
      (let [result (flatten (bean/->clj result))]
@@ -122,7 +122,7 @@
   [ok-handler]
   (let [path-handles (atom {})
         electron? (util/electron?)
-        mobile-native? (mobile-util/is-native-platform?)
+        mobile-native? (mobile-util/native-platform?)
         nfs? (and (not electron?)
                   (not mobile-native?))
         *repo (atom nil)]
@@ -147,7 +147,7 @@
                      (nfs/add-nfs-file-handle! root-handle-path root-handle))
                  result (nth result 1)
                  files (-> (->db-files mobile-native? electron? dir-name result)
-                           remove-ignore-files)
+                           (remove-ignore-files dir-name nfs?))
                  _ (when nfs?
                      (let [file-paths (set (map :file/path files))]
                        (swap! path-handles (fn [handles]
@@ -282,7 +282,7 @@
            handle-path (str config/local-handle-prefix dir-name)
            path-handles (atom {})
            electron? (util/electron?)
-           mobile-native? (mobile-util/is-native-platform?)
+           mobile-native? (mobile-util/native-platform?)
            nfs? (and (not electron?)
                      (not mobile-native?))]
        (when re-index?
@@ -297,7 +297,7 @@
                                                  (when nfs?
                                                    (swap! path-handles assoc path handle))))
                     new-files (-> (->db-files mobile-native? electron? dir-name files-result)
-                                  remove-ignore-files)
+                                  (remove-ignore-files dir-name nfs?))
                     _ (when nfs?
                         (let [file-paths (set (map :file/path new-files))]
                           (swap! path-handles (fn [handles]

+ 12 - 2
src/main/frontend/mobile/core.cljs

@@ -1,5 +1,6 @@
 (ns frontend.mobile.core
   (:require ["@capacitor/app" :refer [^js App]]
+            ["@capacitor/keyboard" :refer [^js Keyboard]]
             [clojure.string :as string]
             [frontend.fs.capacitor-fs :as fs]
             [frontend.handler.editor :as editor-handler]
@@ -80,6 +81,15 @@
                 (fn [event]
                   (state/pub-event! [:file-watcher/changed event])))
 
+  (.addListener Keyboard "keyboardWillShow"
+                  (fn [^js info]
+                    (let [keyboard-height (.-keyboardHeight info)]
+                      (state/pub-event! [:mobile/keyboard-will-show keyboard-height]))))
+
+  (.addListener Keyboard "keyboardWillHide"
+                  (fn []
+                    (state/pub-event! [:mobile/keyboard-will-hide])))
+
   (.addEventListener js/window "statusTap"
                      #(util/scroll-to-top true))
 
@@ -96,6 +106,6 @@
 
   (when (mobile-util/native-ios?)
     (ios-init))
-
-  (when (mobile-util/is-native-platform?)
+  
+  (when (mobile-util/native-platform?)
     (general-init)))

+ 8 - 9
src/main/frontend/mobile/footer.cljs

@@ -1,12 +1,12 @@
 (ns frontend.mobile.footer
-  (:require [frontend.ui :as ui]
-            [rum.core :as rum]
-            [frontend.state :as state]
+  (:require [clojure.string :as string]
+            [frontend.date :as date]
+            [frontend.handler.editor :as editor-handler]
             [frontend.mobile.record :as record]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
             [frontend.util :as util]
-            [frontend.handler.editor :as editor-handler]
-            [clojure.string :as string]
-            [frontend.date :as date]))
+            [rum.core :as rum]))
 
 (rum/defc mobile-bar-command [command-handler icon]
   [:div
@@ -46,9 +46,8 @@
 
 (rum/defc footer < rum/reactive
   []
-  (when-not (or (state/sub :editor/editing?)
-                (state/sub :block/component-editing-mode?)
-                (state/sub :editor/editing-page-title?))
+  (when (and (state/sub :mobile/show-tabbar?)
+             (state/get-current-repo))
     [:div.cp__footer.w-full.bottom-0.justify-between
      (audio-record-cp)
      (mobile-bar-command #(state/toggle-document-mode!) "notes")

+ 52 - 1
src/main/frontend/mobile/index.css

@@ -1,9 +1,10 @@
 .cp__footer {
     position: absolute;
     bottom: 0px;
+    left: 0px;
     padding: 10px 20px;
     background-color: var(--ls-primary-background-color);
-    z-index: 1000;
+    z-index: 10;
     display: flex;
     flex: 0 0 auto;
     white-space: nowrap;
@@ -22,6 +23,56 @@
     }
 }
 
+#mobile-editor-toolbar {
+  position: fixed;
+  bottom: 0;
+  transition: bottom 260ms;
+  /* transition-timing-function: cubic-bezier(.29, 1.01, 1, -0.68); */
+  /* transition-timing-function: steps(10, jump-end); */
+  /* transition-timing-function: steps(5, end); */
+  transition-timing-function: ease-out;
+  left: 0;
+  width: 100%;
+  z-index: 9999;
+  display: flex;
+  justify-content: space-between;
+
+  button {
+      padding: 7px 10px;
+
+      .submenu {
+          background-color: red;
+          z-index: 100;
+          background-color: var(--ls-secondary-background-color);
+          border-radius: 5px;
+          box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.02);
+          overflow-x: overlay;
+          overflow-y: hidden;
+          left: 0px;
+          height: 40px;
+      }
+      
+      .show-submenu {
+          display: block;
+      }
+  }
+  
+  .toolbar-commands {
+    justify-content: space-between;
+    display: flex;
+    align-items: center;
+    overflow-x: overlay;
+    overflow-y: hidden;
+    width: 95%;
+  }
+
+  .toolbar-hide-keyboard {
+    border-left: 1px solid;
+    border-color: var(--ls-quaternary-background-color);
+  }
+}
+
+
 html.is-native-ipad {
     .cp__footer {
         height: 55px;

+ 13 - 13
src/main/frontend/mobile/intent.cljs

@@ -1,22 +1,23 @@
 (ns frontend.mobile.intent
   (:require ["@capacitor/filesystem" :refer [Filesystem]]
+            ["path" :as path]
             ["send-intent" :refer [^js SendIntent]]
-            [lambdaisland.glogi :as log]
-            [promesa.core :as p]
+            [clojure.pprint :as pprint]
+            [clojure.set :as set]
             [clojure.string :as string]
+            [frontend.config :as config]
+            [frontend.date :as date]
             [frontend.db :as db]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
-            [frontend.date :as date]
             [frontend.util :as util]
-            [frontend.config :as config]
-            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [lambdaisland.glogi :as log]
             [logseq.graph-parser.config :as gp-config]
-            ["path" :as path]
-            [frontend.mobile.util :as mobile-util]
-            [frontend.handler.notification :as notification]
-            [clojure.pprint :as pprint]
-            [clojure.set :as set]))
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.text :as text]
+            [promesa.core :as p]))
 
 (defn- handle-received-text [result]
   (let [{:keys [title url]} result
@@ -33,9 +34,8 @@
                      (string/split url "\"\n"))
         text (some-> text (string/replace #"^\"" ""))
         url (and url
-                 (cond (or (string/includes? url "youtube.com")
-                           (string/includes? url "youtu.be"))
-                       (util/format "{{youtube %s}}" url)
+                 (cond (boolean (text/get-matched-video url))
+                       (util/format "{{video %s}}" url)
 
                        (and (string/includes? url "twitter.com")
                             (string/includes? url "status"))

+ 141 - 0
src/main/frontend/mobile/mobile_bar.cljs

@@ -0,0 +1,141 @@
+(ns frontend.mobile.mobile-bar
+  (:require [dommy.core :as dom]
+            [frontend.commands :as commands]
+            [frontend.date :as date]
+            [frontend.handler.config :as config-handler]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.handler.history :as history]
+            [frontend.handler.page :as page-handler]
+            [frontend.mobile.camera :as mobile-camera]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [goog.dom :as gdom]
+            [rum.core :as rum]))
+
+(def ^:private icons-keywords
+  [:checkbox :brackets :parentheses :command :tag :a-b :list :camera
+   :brand-youtube :link :rotate :rotate-clockwise :code :calendar :bold :italic :strikethrough :paint])
+
+(def ^:private commands-stats
+  (atom (into {}
+              (mapv (fn [name] [name {:counts 0}])
+                    icons-keywords))))
+
+(defn set-command-stats [icon]
+  (let [key (keyword icon)
+        counts (get-in @commands-stats [key :counts])]
+    (swap! commands-stats
+           assoc-in [key :counts] (inc counts))
+    (config-handler/set-config!
+     :mobile/toolbar-stats @commands-stats)))
+
+(rum/defc command
+  [command-handler icon & [count? event?]]
+  [:div
+   [:button.bottom-action
+    {:on-mouse-down (fn [e]
+                      (util/stop e)
+                      (when count?
+                        (set-command-stats icon))
+                      (if event?
+                        (command-handler e)
+                        (command-handler))
+                      (state/set-state! :mobile/toolbar-update-observer (rand-int 1000000)))}
+    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
+
+(rum/defc indent-outdent [indent? icon]
+  [:div
+   [:button.bottom-action
+    {:on-mouse-down (fn [e]
+                      (util/stop e)
+                      (editor-handler/indent-outdent indent?))}
+    (ui/icon icon {:style {:fontSize ui/icon-size}})]])
+
+(rum/defc timestamp-submenu
+  [parent-id]
+  (let [callback (fn [event]
+                   (util/stop event)
+                   (let [target (.-parentNode (.-target event))]
+                     (dom/remove-class! target "show-submenu")))
+        command-cp (fn [action description]
+                     [:button
+                      {:on-mouse-down (fn [e]
+                                        (action)
+                                        (callback e))}
+                      description])]
+    [:div
+     [:button.bottom-action
+      {:on-mouse-down (fn [event]
+                        (util/stop event)
+                        (set-command-stats :calendar)
+                        (state/set-state! :mobile/toolbar-update-observer (rand-int 1000000))
+                        (let [target (gdom/getNextElementSibling (.-target event))]
+                          (dom/add-class! target "show-submenu")))}
+      (ui/icon "calendar" {:style {:fontSize ui/icon-size}})
+      [:div.submenu.fixed.hidden.flex.flex-col.w-full.justify-evenly
+       {:style {:bottom @util/keyboard-height}}
+       (command-cp #(let [today (page-handler/get-page-ref-text (date/today))]
+                      (commands/simple-insert! parent-id today {}))
+                   "Today")
+       (command-cp #(let [tomorrow (page-handler/get-page-ref-text (date/tomorrow))]
+                      (commands/simple-insert! parent-id tomorrow {}))
+                   "Tomorrow")
+       (command-cp #(let [yesterday (page-handler/get-page-ref-text (date/yesterday))]
+                      (commands/simple-insert! parent-id yesterday {}))
+                   "Yesterday")
+       (command-cp #(let [timestamp (date/get-current-time)]
+                      (commands/simple-insert! parent-id timestamp {}))
+                   "Time")]]]))
+
+(defn commands
+  [parent-id]
+  (let [viewport-fn (fn [] (when-let [input (gdom/getElement parent-id)]
+                             (util/scroll-editor-cursor input :to-vw-one-quarter? true)
+                             (.focus input)))]
+    (zipmap icons-keywords
+            [(command editor-handler/cycle-todo! "checkbox" true)
+             (command #(do (viewport-fn) (editor-handler/toggle-page-reference-embed parent-id)) "brackets" true)
+             (command #(do (viewport-fn) (editor-handler/toggle-block-reference-embed parent-id)) "parentheses" true)
+             (command #(do (viewport-fn) (commands/simple-insert! parent-id "/" {})) "command" true)
+             (command #(do (viewport-fn) (commands/simple-insert! parent-id "#" {})) "tag" true)
+             (command editor-handler/cycle-priority! "a-b" true)
+             (command editor-handler/toggle-list! "list" true)
+             (command #(mobile-camera/embed-photo parent-id) "camera" true)
+             (command commands/insert-youtube-timestamp "brand-youtube" true)
+             (command editor-handler/html-link-format! "link" true)
+             (command history/undo! "rotate" true true)
+             (command history/redo! "rotate-clockwise" true true)
+             (timestamp-submenu parent-id)
+             (command #(commands/simple-insert! parent-id "<" {}) "code" true)
+             (command editor-handler/bold-format! "bold" true)
+             (command editor-handler/italics-format! "italic" true)
+             (command editor-handler/strike-through-format! "strikethrough" true)
+             (command editor-handler/highlight-format! "paint" true)])))
+
+(rum/defc mobile-bar < rum/reactive
+  []
+  (when (and (state/sub :mobile/toolbar-update-observer)
+             (state/sub :mobile/show-toolbar?))
+    (when-let [config-toolbar-stats (:mobile/toolbar-stats (state/get-config))]
+      (prn :config-toolbar-stats config-toolbar-stats)
+      (reset! commands-stats config-toolbar-stats))
+    (let [parent-id (state/get-edit-input-id)
+          commands (commands parent-id)
+          sorted-commands (sort-by (comp :counts second) > @commands-stats)]
+      (when (and (state/sub :mobile/show-toolbar?)
+                 (state/sub :editor/editing?))
+        [:div#mobile-editor-toolbar.bg-base-2
+         [:div.toolbar-commands
+          (indent-outdent false "arrow-bar-left")
+          (indent-outdent true "arrow-bar-right")
+          (command (editor-handler/move-up-down true) "arrow-bar-to-up")
+          (command (editor-handler/move-up-down false) "arrow-bar-to-down")
+          (command #(if (state/sub :document/mode?)
+                      (editor-handler/insert-new-block! nil)
+                      (commands/simple-insert! parent-id "\n" {})) "arrow-back")
+          (for [command sorted-commands]
+            ((first command) commands))]
+         [:div.toolbar-hide-keyboard
+          (command #(state/clear-edit!) "keyboard-show")]]))))
+

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

@@ -6,15 +6,15 @@
 (defn platform []
   (.getPlatform Capacitor))
 
-(defn is-native-platform? []
+(defn native-platform? []
   (.isNativePlatform Capacitor))
 
 (defn native-ios? []
-  (and (is-native-platform?)
+  (and (native-platform?)
        (= (platform) "ios")))
 
 (defn native-android? []
-  (and (is-native-platform?)
+  (and (native-platform?)
        (= (platform) "android")))
 
 (defn convert-file-src [path-str]
@@ -27,7 +27,7 @@
   (defonce file-sync (registerPlugin "FileSync")))
 
 ;; NOTE: both iOS and android share the same FsWatcher API
-(when (is-native-platform?)
+(when (native-platform?)
   (defonce fs-watcher (registerPlugin "FsWatcher")))
 
 (defn sync-icloud-repo [repo-dir]
@@ -41,45 +41,6 @@
 (defn hide-splash []
   (.hide SplashScreen))
 
-(def idevice-info
-  (atom
-   {:iPadPro12.9    {:width 1024 :height 1366 :statusbar 40}
-    :iPadPro11      {:width 834  :height 1194 :statusbar 40}
-    :iPadPro10.5    {:width 834  :height 1112 :statusbar 40}
-    :iPadAir10.5    {:width 834  :height 1112 :statusbar 40}
-    :iPadAir10.9    {:width 820  :height 1180 :statusbar 40}
-    :iPad10.2       {:width 810  :height 1080 :statusbar 40}
-    :iPadPro9.7     {:width 768  :height 1024 :statusbar 40}
-    :iPadmini9.7    {:width 768  :height 1024 :statusbar 40}
-    :iPadAir9.7     {:width 768  :height 1024 :statusbar 40}
-    :iPad9.7        {:width 768  :height 1024 :statusbar 40}
-    :iPadmini8.3        {:width 744  :height 1133 :statusbar 40}
-    :iPhone7Plus        {:width 476  :height 847  :statusbar 20}
-    :iPhone6sPlus   {:width 476  :height 847  :statusbar 20}
-    :iPhone6Plus        {:width 476  :height 847  :statusbar 20}
-    :iPhone13ProMax {:width 428  :height 926  :statusbar 47}
-    :iPhone12ProMax {:width 428  :height 926  :statusbar 47}
-    :iPhone11ProMax {:width 414  :height 896  :statusbar 44}
-    :iPhone11       {:width 414  :height 896  :statusbar 48}
-    :iPhoneXSMax        {:width 414  :height 896  :statusbar 48}
-    :iPhoneXR       {:width 414  :height 896  :statusbar 48}
-    :iPhone8Plus        {:width 414  :height 736  :statusbar 20}
-    :iPhone13Pro        {:width 390  :height 844  :statusbar 47}
-    :iPhone13       {:width 390  :height 844  :statusbar 47}
-    :iPhone12       {:width 390  :height 844  :statusbar 47}
-    :iPhone12Pro        {:width 390  :height 844  :statusbar 47}
-    :iPhone11Pro        {:width 375  :height 812  :statusbar 44}
-    :iPhoneXS       {:width 375  :height 812  :statusbar 44}
-    :iPhoneX        {:width 375  :height 812  :statusbar 44}
-    :iPhone8        {:width 375  :height 667  :statusbar 20}
-    :iPhone7        {:width 375  :height 667  :statusbar 20}
-    :iPhone6s       {:width 375  :height 667  :statusbar 20}
-    :iPhone6        {:width 375  :height 667  :statusbar 20}
-    :iPhone13mini   {:width 375  :height 812  :statusbar 44}
-    :iPhone12mini   {:width 375  :height 812  :statusbar 44}
-    :iPhoneSE4      {:width 320  :height 568  :statusbar 20}
-    :iPodtouch5     {:width 320  :height 568  :statusbar 20}}))
-
 (defn get-idevice-model
   []
   (when (native-ios?)
@@ -121,12 +82,3 @@
   []
   (when-let [model (get-idevice-model)]
     (string/starts-with? (first model) "iPad")))
-
-(defn get-idevice-statusbar-height
-  []
-  (let [[model landscape?] (get-idevice-model)
-        model (when-not (= model "Not a known Apple device!")
-                (keyword model))]
-    (if (and model landscape?)
-      20
-      (:statusbar (model @idevice-info)))))

+ 1 - 1
src/main/frontend/modules/instrumentation/sentry.cljs

@@ -18,7 +18,7 @@
    :initialScope {:tags
                   {:platform (cond
                                (util/electron?) "electron"
-                               (mobile-util/is-native-platform?) "mobile"
+                               (mobile-util/native-platform?) "mobile"
                                :else "web")
                    :publishing config/publishing?}}
    :integrations [(new posthog/SentryIntegration posthog "logseq" 5311485)

+ 59 - 33
src/main/frontend/modules/outliner/core.cljs

@@ -99,7 +99,7 @@
 
   (-get-parent-id [this]
     (-> (get-in this [:data :block/parent])
-      (outliner-u/->block-id)))
+        (outliner-u/->block-id)))
 
   (-set-parent-id [this parent-id]
     (outliner-u/check-block-id parent-id)
@@ -107,7 +107,7 @@
 
   (-get-left-id [this]
     (-> (get-in this [:data :block/left])
-      (outliner-u/->block-id)))
+        (outliner-u/->block-id)))
 
   (-set-left-id [this left-id]
     (outliner-u/check-block-id left-id)
@@ -170,7 +170,7 @@
 
   (-del [this txs-state children?]
     (assert (ds/outliner-txs-state? txs-state)
-      "db should be satisfied outliner-tx-state?")
+            "db should be satisfied outliner-tx-state?")
     (let [block-id (tree/-get-id this)
           ids (set (if children?
                      (let [children (db/get-block-children (state/get-current-repo) block-id)
@@ -192,7 +192,7 @@
                                                  (assoc :block/left parent))))
                                            immediate-children)))
                     txs))
-                  txs)]
+                txs)]
       (swap! txs-state concat txs)
       block-id))
 
@@ -209,12 +209,7 @@
 (defn get-right-sibling
   [db-id]
   (when db-id
-    (when-let [block (db/entity db-id)]
-      (db-model/get-by-parent-&-left (conn/get-db)
-                                     (:db/id (:block/parent block))
-                                     db-id))))
-
-
+    (db-model/get-right-sibling (conn/get-db) db-id)))
 
 (defn- assoc-level-aux
   [tree-vec children-key init-level]
@@ -285,13 +280,13 @@
     (loop [node node
            limit limit
            result []]
-     (if (zero? limit)
-       result
-       (if-let [left (tree/-get-left node)]
-         (if-not (= left parent)
-           (recur left (dec limit) (conj result (tree/-get-id left)))
-           result)
-         result)))))
+      (if (zero? limit)
+        result
+        (if-let [left (tree/-get-left node)]
+          (if-not (= left parent)
+            (recur left (dec limit) (conj result (tree/-get-id left)))
+            result)
+          result)))))
 
 (defn- page-first-child?
   [block]
@@ -494,6 +489,48 @@
         {:tx-data full-tx
          :blocks tx}))))
 
+(defn- build-move-blocks-next-tx
+  [blocks]
+  (let [id->blocks (zipmap (map :db/id blocks) blocks)
+        top-level-blocks (get-top-level-blocks blocks)
+        top-level-blocks-ids (set (map :db/id top-level-blocks))
+        right-block (get-right-sibling (:db/id (last top-level-blocks)))]
+    (when (and right-block
+               (not (contains? top-level-blocks-ids (:db/id right-block))))
+      {:db/id (:db/id right-block)
+       :block/left (loop [block (:block/left right-block)]
+                     (if (contains? top-level-blocks-ids (:db/id block))
+                       (recur (:block/left (get id->blocks (:db/id block))))
+                       (:db/id block)))})))
+
+(defn- find-new-left
+  [block moved-ids target-block current-block sibling?]
+  (if (= (:db/id target-block) (:db/id (:block/left current-block)))
+    (if sibling?
+      (db/entity (last moved-ids))
+      target-block)
+    (let [left (db/entity (:db/id (:block/left block)))]
+      (if (contains? (set moved-ids) (:db/id left))
+        (find-new-left left moved-ids target-block current-block sibling?)
+        left))))
+
+(defn- fix-non-consecutive-blocks
+  [blocks target-block sibling?]
+  (let [page-blocks (group-by :block/page blocks)]
+    (->>
+     (mapcat (fn [[_page blocks]]
+               (let [blocks (db-model/sort-page-random-blocks blocks)
+                     non-consecutive-blocks (->> (conj (db-model/get-non-consecutive-blocks blocks) (last blocks))
+                                                 (util/distinct-by :db/id))]
+                 (when (seq non-consecutive-blocks)
+                   (mapv (fn [block]
+                           (when-let [right (get-right-sibling (:db/id block))]
+                             (when-let [new-left (find-new-left right (distinct (map :db/id blocks)) target-block block sibling?)]
+                               {:db/id      (:db/id right)
+                                :block/left (:db/id new-left)})))
+                         non-consecutive-blocks)))) page-blocks)
+     (remove nil?))))
+
 (defn- delete-block
   "Delete block from the tree."
   [txs-state block' children?]
@@ -556,23 +593,11 @@
               (tree/-save new-right-node txs-state))))
         (doseq [id block-ids]
           (let [node (block (db/pull id))]
-            (tree/-del node txs-state true)))))
+            (tree/-del node txs-state true)))
+        (let [fix-non-consecutive-tx (fix-non-consecutive-blocks blocks nil false)]
+          (swap! txs-state concat fix-non-consecutive-tx))))
     {:tx-data @txs-state}))
 
-(defn- build-move-blocks-next-tx
-  [blocks]
-  (let [id->blocks (zipmap (map :db/id blocks) blocks)
-        top-level-blocks (get-top-level-blocks blocks)
-        top-level-blocks-ids (set (map :db/id top-level-blocks))
-        right-block (get-right-sibling (:db/id (last top-level-blocks)))]
-    (when (and right-block
-               (not (contains? top-level-blocks-ids (:db/id right-block))))
-      {:db/id (:db/id right-block)
-       :block/left (loop [block (:block/left right-block)]
-                     (if (contains? top-level-blocks-ids (:db/id block))
-                       (recur (:block/left (get id->blocks (:db/id block))))
-                       (:db/id block)))})))
-
 (defn move-blocks
   "Move `blocks` to `target-block` as siblings or children."
   [blocks target-block {:keys [sibling? outliner-op]}]
@@ -598,7 +623,8 @@
                                      (let [children-ids (mapcat #(db/get-block-children-ids (state/get-current-repo) (:block/uuid %)) blocks)]
                                        (map (fn [uuid] {:block/uuid uuid
                                                         :block/page target-page}) children-ids)))
-                  full-tx (util/concat-without-nil tx-data move-blocks-next-tx children-page-tx)
+                  fix-non-consecutive-tx (fix-non-consecutive-blocks blocks target-block sibling?)
+                  full-tx (util/concat-without-nil tx-data move-blocks-next-tx children-page-tx fix-non-consecutive-tx)
                   tx-meta (cond-> {:move-blocks (mapv :db/id blocks)
                                    :target (:db/id target-block)}
                             not-same-page?

+ 1 - 2
src/main/frontend/page.cljs

@@ -15,8 +15,7 @@
   []
   (try
     (comp
-      (ui/setup-active-keystroke!)
-      (ui/setup-patch-ios-visual-viewport-state!))
+     (ui/setup-active-keystroke!))
     (catch js/Error _e
       nil)))
 

+ 11 - 18
src/main/frontend/state.cljs

@@ -58,6 +58,7 @@
      :modal/close-btn?                      nil
      :modal/subsets                         []
 
+     
      ;; right sidebar
      :ui/fullscreen?                        false
      :ui/settings-open?                     false
@@ -88,9 +89,6 @@
      :ui/shortcut-tooltip?                  (if (false? (storage/get :ui/shortcut-tooltip?))
                                               false
                                               true)
-     :ui/visual-viewport-pending?           false
-     :ui/visual-viewport-state              nil
-
      :document/mode?                        document-mode?
 
      :config                                {}
@@ -114,7 +112,6 @@
      :editor/args                           nil
      :editor/on-paste?                      false
      :editor/last-key-code                  nil
-     :editor/editing-page-title?            false
 
      ;; for audio record
      :editor/record-status                  "NONE"
@@ -146,6 +143,13 @@
      :electron/updater                      {}
      :electron/user-cfgs                    nil
 
+     ;; mobile
+     :mobile/show-toolbar?                  false
+     ;;; toolbar icon doesn't update correctly when clicking after separate it from box,
+     ;;; add a random in (<= 1000000) to observer its update
+     :mobile/toolbar-update-observer        0 
+     :mobile/show-tabbar?                   false
+
      ;; plugin
      :plugin/enabled                        (and (util/electron?)
                                                  ;; true false :theme-only
@@ -289,7 +293,7 @@
 (defn get-current-repo
   []
   (or (:git/current-repo @state)
-      (when-not (mobile-util/is-native-platform?)
+      (when-not (mobile-util/native-platform?)
         "local")))
 
 (defn get-config
@@ -853,10 +857,7 @@
              (util/set-change-value input content))
 
            (when move-cursor?
-             (cursor/move-cursor-to input pos))
-
-           (when (or (util/mobile?) (mobile-util/is-native-platform?))
-             (util/make-el-center-if-near-top input))))))))
+             (cursor/move-cursor-to input pos))))))))
 
 (defn clear-edit!
   []
@@ -1193,7 +1194,7 @@
 
 (defn enable-tooltip?
   []
-  (if (or (util/mobile?) (mobile-util/is-native-platform?))
+  (if (or (util/mobile?) (mobile-util/native-platform?))
     false
     (get (get (sub-config) (get-current-repo))
          :ui/enable-tooltip?
@@ -1547,14 +1548,6 @@
   []
   (:editor/last-key-code @state))
 
-(defn set-visual-viewport-state
-  [input]
-  (set-state! :ui/visual-viewport-state input))
-
-(defn get-visual-viewport-state
-  []
-  (:ui/visual-viewport-state @state))
-
 (defn get-plugin-by-id
   [id]
   (when-let [id (and id (keyword id))]

+ 3 - 37
src/main/frontend/ui.cljs

@@ -46,7 +46,7 @@
        (util/safari?)
        (js/window.scrollTo 0 0)))
 
-(defonce icon-size (if (mobile-util/is-native-platform?) 23 20))
+(defonce icon-size (if (mobile-util/native-platform?) 23 20))
 
 (rum/defc ls-textarea
   < rum/reactive
@@ -290,40 +290,6 @@
         (.appendChild js/document.head node))
       style)))
 
-(defn setup-patch-ios-visual-viewport-state!
-  []
-  (when-let [^js vp (and (or (and (util/mobile?) (util/safari?))
-                             (mobile-util/native-ios?))
-                         js/window.visualViewport)]
-    (let [raf-pending? (atom false)
-          set-raf-pending! #(reset! raf-pending? %)
-          on-viewport-changed
-          (fn []
-            (let [update-vw-state
-                  (debounce
-                   (fn []
-                     (state/set-visual-viewport-state {:height     (.-height vp)
-                                                       :page-top   (.-pageTop vp)
-                                                       :offset-top (.-offsetTop vp)})
-                     (state/set-state! :ui/visual-viewport-pending? false))
-                   20)]
-              (when-not @raf-pending?
-                (let [f (fn []
-                          (set-raf-pending! false)
-                          (update-vw-state))]
-                  (set-raf-pending! true)
-                  (state/set-state! :ui/visual-viewport-pending? true)
-                  (js/window.requestAnimationFrame f)))))]
-
-      (.addEventListener vp "resize" on-viewport-changed)
-      (.addEventListener vp "scroll" on-viewport-changed)
-
-      (fn []
-        (.removeEventListener vp "resize" on-viewport-changed)
-        (.removeEventListener vp "scroll" on-viewport-changed)
-        (state/set-visual-viewport-state nil))))
-  #())
-
 (defn apply-custom-theme-effect! [theme]
   (when plugin-handler/lsp-enabled?
     (when-let [custom-theme (state/sub [:ui/custom-theme (keyword theme)])]
@@ -684,7 +650,7 @@
                                              (assoc :on-mouse-down on-mouse-down
                                                     :class "cursor"))
        [:div.flex.flex-row.items-center
-        (when-not (mobile-util/is-native-platform?)
+        (when-not (mobile-util/native-platform?)
           [:a.block-control.opacity-50.hover:opacity-100.mr-2
            (cond->
             {:style    {:width       14
@@ -953,7 +919,7 @@
   (rum/local true ::active?)
   [state content-fn sensor-opts {:keys [reset-height? once?]}]
   (let [*active? (::active? state)]
-    (if (or (util/mobile?) (mobile-util/is-native-platform?))
+    (if (or (util/mobile?) (mobile-util/native-platform?))
       (content-fn)
       (let [*visible? (::visible? state)]
         (visibility-sensor

+ 55 - 61
src/main/frontend/util.cljc

@@ -11,7 +11,7 @@
             [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
             [dommy.core :as d]
-            [frontend.mobile.util :refer [is-native-platform?]]
+            [frontend.mobile.util :refer [native-platform?]]
             [logseq.graph-parser.util :as gp-util]
             [goog.dom :as gdom]
             [goog.object :as gobj]
@@ -89,7 +89,7 @@
 
 #?(:cljs
    (def nfs? (and (not (electron?))
-                  (not (is-native-platform?)))))
+                  (not (native-platform?)))))
 
 #?(:cljs
    (defn file-protocol?
@@ -1040,29 +1040,6 @@
       (string/replace "&quot;" "\"")
       (string/replace "&apos;" "'")))
 
-#?(:cljs
-   (defn system-locales
-     []
-     (when-not node-test?
-       (when-let [navigator (and js/window (.-navigator js/window))]
-         ;; https://zzz.buzz/2016/01/13/detect-browser-language-in-javascript/
-         (when navigator
-           (let [v (js->clj
-                    (or
-                     (.-languages navigator)
-                     (.-language navigator)
-                     (.-userLanguage navigator)
-                     (.-browserLanguage navigator)
-                     (.-systemLanguage navigator)))]
-             (if (string? v) [v] v)))))))
-
-#?(:cljs
-   (defn zh-CN-supported?
-     []
-     (let [system-locales (set (system-locales))]
-       (or (contains? system-locales "zh-CN")
-           (contains? system-locales "zh-Hans-CN")))))
-
 (comment
   (= (get-relative-path "journals/2020_11_18.org" "pages/grant_ideas.org")
      "../pages/grant_ideas.org")
@@ -1216,6 +1193,12 @@
    (defn meta-key-name []
      (if mac? "Cmd" "Ctrl")))
 
+#?(:cljs
+   (defn meta-key? [e]
+     (if mac?
+       (gobj/get e "metaKey")
+       (gobj/get e "ctrlKey"))))
+
 #?(:cljs
    (defn right-click?
      [e]
@@ -1224,42 +1207,53 @@
        (or (= which 3)
            (= button 2)))))
 
-#?(:cljs
-   (defn make-el-into-center-viewport
-     [^js/HTMLElement el]
-     (when el
-       (.scrollIntoView el #js {:block "center" :behavior "smooth"}))))
-
-#?(:cljs
-   (defn make-el-cursor-position-into-center-viewport
-     [^js/HTMLElement el]
-     (when el
-       (let [main-node (gdom/getElement "main-content-container")
-             pos (get-selection-start el)
-             cursor-top (some-> (gdom/getElement "mock-text")
-                                gdom/getChildren
-                                array-seq
-                                (nth-safe pos)
-                                .-offsetTop)
-             box-caret (.getBoundingClientRect el)
-             box-top (.-top box-caret)
-             box-bottom (.-bottom box-caret)
-             vw-height (or (.-height js/window.visualViewport)
-                           (.-clientHeight js/document.documentElement))
-             scroll-top (.-scrollTop main-node)
-             cursor-y (if cursor-top (+ cursor-top box-top) box-bottom)
-             scroll (- cursor-y (/ vw-height 2))]
-         (when (> scroll 0)
-           (set! (.-scrollTop main-node) (+ scroll-top scroll)))))))
-
-#?(:cljs
-   (defn make-el-center-if-near-top
-     ([^js/HTMLElement el]
-      (make-el-center-if-near-top el 80))
-     ([^js/HTMLElement el offset]
-      (let [target-top (.-top (.getBoundingClientRect el))]
-        (when (<= target-top (or (safe-parse-int offset) 0))
-          (make-el-into-center-viewport el))))))
+(def keyboard-height (atom nil))
+#?(:cljs
+   (defn scroll-editor-cursor
+     [^js/HTMLElement el & {:keys [to-vw-one-quarter?]}]
+     (when (and el (or (native-platform?) mobile?))
+       (let [box-rect    (.getBoundingClientRect el)
+             box-top     (.-top box-rect)
+             box-bottom  (.-bottom box-rect)
+
+             header-height (-> (gdom/getElementByClass "cp__header")
+                               .-clientHeight)
+
+             main-node   (app-scroll-container-node)
+             scroll-top  (.-scrollTop main-node)
+
+             current-pos (get-selection-start el)
+             mock-text   (some-> (gdom/getElement "mock-text")
+                                 gdom/getChildren
+                                 array-seq
+                                 (nth-safe current-pos))
+             offset-top   (and mock-text (.-offsetTop mock-text))
+             offset-height (and mock-text (.-offsetHeight mock-text))
+
+             cursor-y    (if offset-top (+ offset-top box-top offset-height 2) box-bottom)
+             vw-height   (or (.-height js/window.visualViewport)
+                             (.-clientHeight js/document.documentElement))
+             ;; mobile toolbar height: 40px
+             scroll      (- cursor-y (- vw-height (+ @keyboard-height 40)))]
+         (cond
+           (and to-vw-one-quarter? (> cursor-y (* vw-height 0.4)))
+           (set! (.-scrollTop main-node) (+ scroll-top (- cursor-y (/ vw-height 4))))
+
+           (and (< cursor-y (+ header-height offset-height 4)) ;; 4 is top+bottom padding for per line
+                (>= cursor-y header-height))
+           (.scrollBy main-node (bean/->js {:top (- (+ offset-height 4))}))
+
+           (< cursor-y header-height)
+           (let [_ (.scrollIntoView el true)
+                 main-node (app-scroll-container-node)
+                 scroll-top (.-scrollTop main-node)]
+             (set! (.-scrollTop main-node) (- scroll-top (/ vw-height 4))))
+
+           (> scroll 0)
+           (set! (.-scrollTop main-node) (+ scroll-top scroll))
+
+           :else
+           nil)))))
 
 #?(:cljs
    (defn sm-breakpoint?

+ 11 - 6
src/main/frontend/util/fs.cljs

@@ -4,17 +4,22 @@
 
 ;; TODO: move all file path related util functions to here
 
-;; keep same as ignored-path? in src/electron/electron/utils.cljs
-;; TODO: merge them
+;; NOTE: This is not the same ignored-path? as src/electron/electron/utils.cljs.
+;;       The assets directory is ignored.
+;;
+;; When in nfs-mode, dir is "", path is relative path to graph dir.
+;; When in native-mode, dir and path are absolute paths.
 (defn ignored-path?
+  "Ignore path for ls-dir-files-with-handler! and reload-dir!"
   [dir path]
   (when (string? path)
     (or
      (some #(string/starts-with? path (str dir "/" %))
-           ["." ".recycle" "assets" "node_modules" "logseq/bak"])
+           ["." ".recycle" "assets" "node_modules" "logseq/bak" "version-files"])
      (some #(string/includes? path (str "/" % "/"))
-           ["." ".recycle" "assets" "node_modules" "logseq/bak"])
-     (string/ends-with? path ".DS_Store")
+           ["." ".recycle" "assets" "node_modules" "logseq/bak" "version-files"])
+     (some #(string/ends-with? path %)
+           [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"])
      ;; hidden directory or file
      (let [relpath (path/relative dir path)]
        (or (re-find #"/\.[^.]+" relpath)
@@ -24,4 +29,4 @@
         (not (string/blank? (path/extname path)))
         (not
          (some #(string/ends-with? path %)
-               [".md" ".markdown" ".org" ".js" ".edn" ".css"])))))))
+               [".md" ".markdown" ".org" ".js" ".edn" ".css"])))))))

+ 1 - 1
src/main/frontend/util/thingatpt.cljs

@@ -147,7 +147,7 @@
                      :name name
                      :end (+ (:end admonition&src) (count name))))))))
 
-(defn- markdown-src-at-point [& [input]]
+(defn markdown-src-at-point [& [input]]
   (when-let [markdown-src (thing-at-point ["```" "```"] input)]
     (let [language (-> (:full-content markdown-src)
                        string/split-lines

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.6.9")
+(defonce version "0.6.10")

+ 12 - 0
src/main/logseq/graph_parser/text.cljs

@@ -122,6 +122,18 @@
   [s]
   (string/split s #"(\"[^\"]*\")"))
 
+(def bilibili-regex #"^((?:https?:)?//)?((?:www).)?((?:bilibili.com))(/(?:video/)?)([\w-]+)(\S+)?$")
+(def loom-regex #"^((?:https?:)?//)?((?:www).)?((?:loom.com))(/(?:share/|embed/))([\w-]+)(\S+)?$")
+(def vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$")
+(def youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$")
+
+(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)))
+
 (def markdown-link #"\[([^\[]+)\](\(.*\))")
 
 (defn split-page-refs-without-brackets