Browse Source

Merge branch 'master' into feat/integrated-title-bar

Konstantinos 2 years ago
parent
commit
13d29a072f

+ 2 - 2
android/app/build.gradle

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

+ 16 - 3
android/app/src/main/java/com/logseq/app/FsWatcher.java

@@ -92,10 +92,23 @@ public class FsWatcher extends Plugin {
             shouldRead = true;
         }
 
-        URI dir = (new File(mPath)).toURI();
-        URI fpath = f.toURI();
+        Uri dir = Uri.fromFile(new File(mPath));
+        Uri fpath = Uri.fromFile(f);
+        String relpath = null;
+
+        if (fpath.getPath().startsWith(dir.getPath())) {
+            relpath = fpath.getPath().substring(dir.getPath().length());
+            if (relpath.startsWith("/")) {
+                relpath = relpath.substring(1);
+            }
+            relpath = Uri.decode(relpath);
+        } else {
+            Log.e("FsWatcher", "file path not under watch path");
+            return;
+        }
+
 
-        obj.put("path", Normalizer.normalize(dir.relativize(fpath).toString(), Normalizer.Form.NFC));
+        obj.put("path", Normalizer.normalize(relpath, Normalizer.Form.NFC));
         obj.put("dir", Uri.fromFile(new File(mPath))); // Uri is for Android. URI is for RFC compatible
         JSObject stat;
 

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

@@ -27,6 +27,7 @@
 		D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
 		FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
 		FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
+		FE96D6102A1B811A001ECE32 /* SharedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE96D60F2A1B811A001ECE32 /* SharedData.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -83,6 +84,7 @@
 		DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
 		FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
 		FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
+		FE96D60F2A1B811A001ECE32 /* SharedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedData.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -156,6 +158,7 @@
 			children = (
 				5FFF7D7927E4E70700B00DA8 /* ShareViewController.entitlements */,
 				5FFF7D6C27E343FA00B00DA8 /* ShareViewController.swift */,
+				FE96D60F2A1B811A001ECE32 /* SharedData.swift */,
 				5FFF7D6E27E343FA00B00DA8 /* MainInterface.storyboard */,
 				5FFF7D7127E343FA00B00DA8 /* Info.plist */,
 			);
@@ -345,6 +348,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				FE96D6102A1B811A001ECE32 /* SharedData.swift in Sources */,
 				5FFF7D6D27E343FA00B00DA8 /* ShareViewController.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -438,7 +442,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
@@ -492,7 +496,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
@@ -515,7 +519,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.6;
+				MARKETING_VERSION = 0.9.7;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -542,7 +546,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.9.6;
+				MARKETING_VERSION = 0.9.7;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -565,9 +569,9 @@
 				INFOPLIST_FILE = ShareViewController/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.9.6;
+				MARKETING_VERSION = 0.9.7;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -592,9 +596,9 @@
 				INFOPLIST_FILE = ShareViewController/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
-				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.9.6;
+				MARKETING_VERSION = 0.9.7;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 4 - 2
ios/App/ShareViewController/Info.plist

@@ -8,6 +8,8 @@
 		<dict>
 			<key>NSExtensionActivationRule</key>
 			<dict>
+				<key>NSExtensionActivationDictionaryVersion</key>
+				<integer>2</integer>
 				<key>NSExtensionActivationSupportsFileWithMaxCount</key>
 				<integer>5</integer>
 				<key>NSExtensionActivationSupportsImageWithMaxCount</key>
@@ -17,9 +19,9 @@
 				<key>NSExtensionActivationSupportsText</key>
 				<true/>
 				<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
-				<integer>1</integer>
+				<integer>3</integer>
 				<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
-				<integer>1</integer>
+				<integer>3</integer>
 				<key>NSExtensionActivationUsesStrictMatching</key>
 				<false/>
 			</dict>

+ 117 - 111
ios/App/ShareViewController/ShareViewController.swift

@@ -9,182 +9,180 @@
 import MobileCoreServices
 import Social
 import UIKit
-
-class ShareItem {
-    public var title: String?
-    public var type: String?
-    public var url: String?
-}
+import UniformTypeIdentifiers
 
 class ShareViewController: UIViewController {
-    
-    private var shareItems: [ShareItem] = []
-    
+
+    private var sharedData: SharedData = SharedData.init(resources: [])
+
     var groupContainerUrl: URL? {
         return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.logseq.logseq")
     }
-    
+
     override public func viewDidAppear(_ animated: Bool) {
-       super.viewDidAppear(animated)
-       self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
+        super.viewDidAppear(animated)
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+            self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
+        }
     }
-    
+
     private func sendData() {
-        let queryItems = shareItems.map {
+        let encoder: JSONEncoder = JSONEncoder()
+        let data = try? encoder.encode(self.sharedData)
+        let queryPayload = String(decoding: data!, as: UTF8.self)
+
+        let queryItems =
             [
                 URLQueryItem(
-                    name: "title",
-                    value: $0.title?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
-                URLQueryItem(name: "description", value: ""),
-                URLQueryItem(
-                    name: "type",
-                    value: $0.type?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
-                URLQueryItem(
-                    name: "url",
-                    value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
+                    name: "payload",
+                    value: queryPayload.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
             ]
-        }.flatMap({ $0 })
         var urlComps = URLComponents(string: "logseq://shared?")!
         urlComps.queryItems = queryItems
         openURL(urlComps.url!)
     }
-    
-    fileprivate func createSharedFileUrl(_ url: URL?) -> String {
-        
-        let copyFileUrl = groupContainerUrl!.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + "/" + url!
+
+    fileprivate func createSharedFileUrl(_ url: URL?) -> URL? {
+        let tempFilename = url!
             .lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
-        try? Data(contentsOf: url!).write(to: URL(string: copyFileUrl)!)
-        
+        let copyFileUrl = groupContainerUrl!.appendingPathComponent(tempFilename)
+        try? Data(contentsOf: url!).write(to: copyFileUrl)
         return copyFileUrl
     }
-    
-    func saveScreenshot(_ image: UIImage) -> String {
-        
+
+    // Screenshots, shared images from some system App are passed as UIImage
+    func saveUIImage(_ image: UIImage) -> URL? {
         let dateFormatter = DateFormatter()
         dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss"
-        
-        let copyFileUrl = groupContainerUrl!.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
-        + dateFormatter.string(from: Date()) + ".png"
-        
+        let filename = dateFormatter.string(from: Date()) + ".png"
+
+        let copyFileUrl = groupContainerUrl!.appendingPathComponent(filename)
+
         do {
-            try image.pngData()?.write(to: URL(string: copyFileUrl)!)
+            try image.pngData()?.write(to: copyFileUrl)
             return copyFileUrl
         } catch {
             print(error.localizedDescription)
-            return ""
+            return nil
         }
     }
-    
+
+    // Can be a path or a web URL
     fileprivate func handleTypeUrl(_ attachment: NSItemProvider)
-    async throws -> ShareItem
+    async throws -> SharedResource
     {
         let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil)
         let url = results as! URL?
-        let shareItem: ShareItem = ShareItem()
-        
+
+        var res = SharedResource()
+
         if url!.isFileURL {
-            shareItem.title = url!.lastPathComponent
-            shareItem.type = "application/" + url!.pathExtension.lowercased()
-            shareItem.url = createSharedFileUrl(url)
+            res.name = url!.lastPathComponent
+            res.ext = url!.pathExtension
+            res.type = url!.pathExtensionAsMimeType()
+            res.url = createSharedFileUrl(url)
         } else {
-            shareItem.title = url!.absoluteString
-            shareItem.url = url!.absoluteString
-            shareItem.type = "text/plain"
+            res.name = url!.absoluteString
+            res.type = "text/plain"
         }
-        
-        return shareItem
+
+        return res
     }
-    
+
     fileprivate func handleTypeText(_ attachment: NSItemProvider)
-    async throws -> ShareItem
+    async throws -> SharedResource?
     {
-        let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
-        let shareItem: ShareItem = ShareItem()
-        let text = results as! String
-        shareItem.title = text
-        shareItem.type = "text/plain"
-        
-        return shareItem
+        let item = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
+        self.sharedData.text = item as? String
+        return nil
     }
-    
+
     fileprivate func handleTypeMovie(_ attachment: NSItemProvider)
-    async throws -> ShareItem
+    async throws -> SharedResource
     {
         let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil)
-        let shareItem: ShareItem = ShareItem()
-        
+
         let url = results as! URL?
-        shareItem.title = url!.lastPathComponent
-        shareItem.type = "video/" + url!.pathExtension.lowercased()
-        shareItem.url = createSharedFileUrl(url)
-        
-        return shareItem
+
+        let name = url!.lastPathComponent
+        let ext = url!.pathExtension.lowercased()
+        let type = url!.pathExtensionAsMimeType()
+        let sharedUrl = createSharedFileUrl(url)
+
+        let res = SharedResource(name: name, ext: ext, type: type, url: sharedUrl)
+
+        return res
     }
-    
+
     fileprivate func handleTypeImage(_ attachment: NSItemProvider)
-    async throws -> ShareItem
+    async throws -> SharedResource
     {
         let data = try await attachment.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil)
-        
-        let shareItem: ShareItem = ShareItem()
+
+        var res = SharedResource()
+
         switch data {
         case let image as UIImage:
-            shareItem.title = "screenshot"
-            shareItem.type = "image/png"
-            shareItem.url = self.saveScreenshot(image)
+            res.url = self.saveUIImage(image)
+            res.ext = "png"
+            res.name = res.url?.lastPathComponent
+            res.type = res.url?.pathExtensionAsMimeType()
         case let url as URL:
-            shareItem.title = url.lastPathComponent
-            shareItem.type = "image/" + url.pathExtension.lowercased()
-            shareItem.url = self.createSharedFileUrl(url)
+            res.name = url.lastPathComponent
+            res.ext = url.pathExtension.lowercased()
+            res.type = url.pathExtensionAsMimeType()
+            res.url = self.createSharedFileUrl(url)
         default:
             print("Unexpected image data:", type(of: data))
         }
-        
-        return shareItem
+
+        return res
     }
-    
-    
+
+
     override public func viewDidLoad() {
         super.viewDidLoad()
-        
-        shareItems.removeAll()
-        
-        let extensionItem = extensionContext?.inputItems.first as! NSExtensionItem
+
+        sharedData.empty()
+        let inputItems = extensionContext?.inputItems as! [NSExtensionItem]
         Task {
             try await withThrowingTaskGroup(
-                of: ShareItem.self,
+                of: SharedResource?.self,
                 body: { taskGroup in
-                    
-                    for attachment in extensionItem.attachments! {
-                        if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
-                            taskGroup.addTask {
-                                return try await self.handleTypeUrl(attachment)
-                            }
-                        } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
-                            taskGroup.addTask {
-                                return try await self.handleTypeText(attachment)
-                            }
-                        } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
-                            taskGroup.addTask {
-                                return try await self.handleTypeMovie(attachment)
-                            }
-                        } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
-                            taskGroup.addTask {
-                                return try await self.handleTypeImage(attachment)
+                    for extensionItem in inputItems {
+                        for attachment in extensionItem.attachments! {
+                            if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
+                                taskGroup.addTask {
+                                    return try await self.handleTypeUrl(attachment)
+                                }
+                            } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
+                                taskGroup.addTask {
+                                    return try await self.handleTypeText(attachment)
+                                }
+                            } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
+                                taskGroup.addTask {
+                                    return try await self.handleTypeMovie(attachment)
+                                }
+                            } else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
+                                taskGroup.addTask {
+                                    return try await self.handleTypeImage(attachment)
+                                }
                             }
                         }
                     }
-                    
+
                     for try await item in taskGroup {
-                        self.shareItems.append(item)
+                        if let item = item {
+                            self.sharedData.resources.append(item)
+                        }
                     }
                 })
-            
+
             self.sendData()
-            
+
         }
     }
-    
+
     @discardableResult
     @objc func openURL(_ url: URL) -> Bool {
         var responder: UIResponder? = self
@@ -196,6 +194,14 @@ class ShareViewController: UIViewController {
         }
         return false
     }
-    
+
+
+}
+
+extension URL {
+    func pathExtensionAsMimeType() -> String? {
+        let type = UTType(filenameExtension: self.pathExtension)
+        return type?.preferredMIMEType
+    }
 }
 

+ 25 - 0
ios/App/ShareViewController/SharedData.swift

@@ -0,0 +1,25 @@
+//
+//  SharedData.swift
+//  ShareViewController
+//
+//  Created by Mono Wang on 5/22/23.
+//
+
+import Foundation
+
+public struct SharedResource: Decodable, Encodable {
+    var name: String?
+    var ext: String?
+    var type: String?
+    var url: URL?
+}
+
+public struct SharedData: Decodable, Encodable {
+    var text: String?
+    var resources: [SharedResource]
+    
+    mutating func empty() {
+        text = nil
+        resources = []
+    }
+}

+ 1 - 1
package.json

@@ -122,7 +122,7 @@
         "path-complete-extname": "1.0.0",
         "pixi-graph-fork": "0.2.0",
         "pixi.js": "6.2.0",
-        "posthog-js": "1.10.2",
+        "posthog-js": "1.57.2",
         "react": "17.0.2",
         "react-dom": "17.0.2",
         "react-grid-layout": "0.16.6",

+ 1 - 1
resources/package.json

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

+ 1 - 1
src/main/frontend/fs/sync.cljs

@@ -1777,7 +1777,7 @@
         (when (sync-state--valid-to-accept-filewatcher-event? sync-state)
           (when (or (:mtime stat) (= type "unlink"))
             (go
-              (let [path (path-normalize (remove-dir-prefix dir path))
+              (let [path (path-normalize path)
                     files-meta (and (not= "unlink" type)
                                     (<! (<get-local-files-meta
                                          rsapi (:current-syncing-graph-uuid sync-state) dir [path])))

+ 14 - 15
src/main/frontend/handler/editor.cljs

@@ -1193,10 +1193,6 @@
       (common-handler/copy-to-clipboard-without-id-property! (:block/format block) md-content html sorted-blocks)
       (delete-block-aux! block true))))
 
-(defn clear-last-selected-block!
-  []
-  (state/drop-last-selection-block!))
-
 (defn highlight-selection-area!
   [end-block]
   (when-let [start-block (state/get-selection-start-block-or-first)]
@@ -1212,13 +1208,17 @@
   (cond
     ;; when editing, quit editing and select current block
     (state/editing?)
-    (state/exit-editing-and-set-selected-blocks! [(gdom/getElement (state/get-editing-block-dom-id))])
+    (let [element (gdom/getElement (state/get-editing-block-dom-id))]
+      (when element
+        (util/scroll-to-block element)
+        (state/exit-editing-and-set-selected-blocks! [element])))
 
     ;; when selection and one block selected, select next block
     (and (state/selection?) (== 1 (count (state/get-selection-blocks))))
     (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed-skip)
           element (f (first (state/get-selection-blocks)))]
       (when element
+        (util/scroll-to-block element)
         (state/conj-selection-block! element direction)))
 
     ;; if same direction, keep conj on same direction
@@ -1227,11 +1227,17 @@
           first-last (if (= :up direction) first last)
           element (f (first-last (state/get-selection-blocks)))]
       (when element
+        (util/scroll-to-block element)
         (state/conj-selection-block! element direction)))
 
     ;; if different direction, keep clear until one left
     (state/selection?)
-    (clear-last-selected-block!))
+    (let [f (if (= :up direction) util/get-prev-block-non-collapsed util/get-next-block-non-collapsed)
+          last-first (if (= :up direction) last first)
+          element (f (last-first (state/get-selection-blocks)))]
+      (when element
+        (util/scroll-to-block element)
+        (state/drop-last-selection-block!))))
   nil)
 
 (defn on-select-block
@@ -2482,13 +2488,6 @@
       (.preventDefault e)
       (keydown-new-line))))
 
-(defn- scroll-to-block
-  [block]
-  (when block
-    (when-not (util/element-visible? block)
-      (.scrollIntoView block #js {:behavior "smooth"
-                                  :block "center"}))))
-
 (defn- select-first-last
   "Select first or last block in viewpoint"
   [direction]
@@ -2496,7 +2495,7 @@
         block (->> (util/get-blocks-noncollapse)
                    (f))]
     (when block
-      (scroll-to-block block)
+      (util/scroll-to-block block)
       (state/exit-editing-and-set-selected-blocks! [block]))))
 
 (defn- select-up-down [direction]
@@ -2509,7 +2508,7 @@
             :down util/get-next-block-non-collapsed)
         sibling-block (f selected)]
     (when (and sibling-block (dom/attr sibling-block "blockid"))
-      (scroll-to-block sibling-block)
+      (util/scroll-to-block sibling-block)
       (state/exit-editing-and-set-selected-blocks! [sibling-block]))))
 
 (defn- move-cross-boundary-up-down

+ 11 - 3
src/main/frontend/handler/paste.cljs

@@ -233,12 +233,20 @@
     (state/set-state! :editor/on-paste? true)
     (let [clipboard-data (gobj/get e "clipboardData")
           html (.getData clipboard-data "text/html")
-          text (.getData clipboard-data "text")]
+          text (.getData clipboard-data "text")
+          has-files? (seq (.-files clipboard-data))]
       (cond
         (and (string/blank? text) (string/blank? html))
+        ;; When both text and html are blank, paste file if exists.
+        ;; NOTE: util/stop is not called here if no file is provided, 
+        ;; so the default paste behavior of the native platform will be used.
+        (when has-files?
+          (paste-file-if-exists id e))
+
+        ;; both file attachment and text/html exist
+        (and has-files? (state/preferred-pasting-file?))
         (paste-file-if-exists id e)
-        (and (seq (.-files clipboard-data)) (state/preferred-pasting-file?))
-        (paste-file-if-exists id e)
+
         :else
         (let [text' (or (when (gp-util/url? text)
                           (wrap-macro-url text))

+ 10 - 3
src/main/frontend/mobile/deeplink.cljs

@@ -9,7 +9,8 @@
    [frontend.handler.route :as route-handler]
    [frontend.mobile.intent :as intent]
    [frontend.state :as state]
-   [frontend.util.text :as text-util]))
+   [frontend.util.text :as text-util]
+   [logseq.graph-parser.util :as gp-util]))
 
 (def *link-to-another-graph (atom false))
 
@@ -70,8 +71,14 @@
       (= hostname "shared")
       (let [result (into {} (map (fn [key]
                                    [(keyword key) (.get search-params key)])
-                                 ["title" "url" "type"]))]
-        (intent/handle-result result))
+                                 ["title" "url" "type" "payload"]))]
+        (if (:payload result)
+          (let [raw (gp-util/safe-decode-uri-component (:payload result))
+                payload (-> raw
+                            js/JSON.parse
+                            (js->clj :keywordize-keys true))]
+            (intent/handle-payload payload))
+          (intent/handle-result result)))
 
       :else
       nil)))

+ 86 - 1
src/main/frontend/mobile/intent.cljs

@@ -152,7 +152,92 @@
                         (gp-util/safe-decode-uri-component v)
                         v))])))
 
-(defn handle-result [result]
+(defn- handle-asset-file [url format]
+  (p/let [basename (node-path/basename url)
+          label (-> basename util/node-path.name)
+          path (editor-handler/get-asset-path basename)
+          _file (p/catch
+                 (.copy Filesystem (clj->js {:from url :to path}))
+                 (fn [error]
+                   (log/error :copy-file-error {:error error})))
+          url (util/format "../assets/%s" basename)
+          url-link (editor-handler/get-asset-file-link format url label true)]
+    url-link))
+
+(defn- handle-payload-resource
+  [{:keys [type name ext url] :as resource} format]
+  (if url
+    (cond
+      (contains? (set/union config/doc-formats config/media-formats)
+                 (keyword ext))
+      (handle-asset-file url format)
+
+      :else
+      (notification/show!
+       [:div
+        "Parsing current shared content are not supported. Please report the following codes on "
+        [:a {:href "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"
+             :target "_blank"} "Github"]
+        ". We will look into it soon."
+        [:pre.code (with-out-str (pprint/pprint resource))]] :warning false))
+
+    (cond
+      (= type "text/plain")
+      name
+
+      :else
+      (notification/show!
+       [:div
+        "Parsing current shared content are not supported. Please report the following codes on "
+        [:a {:href "https://github.com/logseq/logseq/issues/new?labels=from:in-app&template=bug_report.yaml"
+             :target "_blank"} "Github"]
+        ". We will look into it soon."
+        [:pre.code (with-out-str (pprint/pprint resource))]] :warning false))))
+
+(defn handle-payload
+  "Mobile share intent handler v2, use complex payload to support more types of content."
+  [payload]
+  ;; use :text template, use {url} as rich text placeholder
+  (p/let [page (or (state/get-current-page) (string/lower-case (date/journal-name)))
+          format (db/get-page-format page)
+
+          template (get-in (state/get-config)
+                           [:quick-capture-templates :text]
+                           "**{time}** [[quick capture]]: {text} {url}")
+          {:keys [text resources]} payload
+          text (or text "")
+          rich-content (-> (p/all (map (fn [resource]
+                                         (handle-payload-resource resource format))
+                                       resources))
+                           (p/then (partial string/join "\n")))]
+    (when (or (not-empty text) (not-empty rich-content))
+      (let [time (date/get-current-time)
+            date-ref-name (date/today)
+            content (-> template
+                        (string/replace "{time}" time)
+                        (string/replace "{date}" date-ref-name)
+                        (string/replace "{text}" text)
+                        (string/replace "{url}" rich-content))
+            edit-content (state/get-edit-content)
+            edit-content-blank? (string/blank? edit-content)
+            edit-content-include-capture? (and (not-empty edit-content)
+                                               (string/includes? edit-content "[[quick capture]]"))]
+        (if (and (state/editing?) (not edit-content-include-capture?))
+          (if edit-content-blank?
+            (editor-handler/insert content)
+            (editor-handler/insert (str "\n" content)))
+
+          (do
+            (editor-handler/escape-editing)
+            (js/setTimeout #(editor-handler/api-insert-new-block! content {:page page
+                                                                           :edit-block? true
+                                                                           :replace-empty-target? true})
+                           100)))))))
+
+
+(defn handle-result
+  "Mobile share intent handler v1, legacy. Only for Android"
+  [result]
   (let [result (decode-received-result result)]
     (when-let [type (:type result)]
       (cond

+ 23 - 10
src/main/frontend/util.cljc

@@ -405,6 +405,16 @@
                 %))
            (take-while (complement nil?) (iterate #(.-parentElement %) element)))))
 
+#?(:cljs
+   (defn element-visible?
+     [element]
+     (when element
+       (when-let [r (.getBoundingClientRect element)]
+         (and (>= (.-top r) 0)
+              (<= (+ (.-bottom r) 64)
+                  (or (.-innerHeight js/window)
+                      (js/document.documentElement.clientHeight))))))))
+
 #?(:cljs
    (defn element-top [elem top]
      (when elem
@@ -455,6 +465,19 @@
      ([animate?]
       (scroll-to (app-scroll-container-node) 0 animate?))))
 
+#?(:cljs
+   (defn scroll-to-block
+     "Scroll into the view to vertically align a non-visible block to the centre
+     of the visible area"
+     ([block]
+      (scroll-to-block block true))
+     ([block animate?]
+      (when block
+        (when-not (element-visible? block)
+          (.scrollIntoView block
+                           #js {:behavior (if animate? "smooth" "auto")
+                                :block    "center"}))))))
+
 #?(:cljs
    (defn link?
      [node]
@@ -1404,16 +1427,6 @@
       (fn [resolve]
         (load url resolve)))))
 
-#?(:cljs
-   (defn element-visible?
-     [element]
-     (when element
-       (when-let [r (.getBoundingClientRect element)]
-         (and (>= (.-top r) 0)
-              (<= (+ (.-bottom r) 64)
-                  (or (.-innerHeight js/window)
-                      (js/document.documentElement.clientHeight))))))))
-
 #?(:cljs
    (defn copy-image-to-clipboard
      [src]

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

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

+ 10 - 4
yarn.lock

@@ -5807,12 +5807,13 @@ postcss@^8.2.1:
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
 
-posthog-js@1.10.2:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.10.2.tgz#74d6c84f9675b65dfd4ff6f4051ed8d3cb974076"
-  integrity sha512-JNjWstHEexhj5CEKldSeYNyPJbtOvZQ3ZPL55fxU7+f+gTBL8RlOb8eFohCPYIk0VhMf2UM1rXxwVBOeMQQQFw==
+posthog-js@1.57.2:
+  version "1.57.2"
+  resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.57.2.tgz#131fb93e2ad099baff4317f3d91a4d6c96a08e7f"
+  integrity sha512-ER4gkYZasrd2Zwmt/yLeZ5G/nZJ6tpaYBCpx3CvocDx+3F16WdawJlYMT0IyLKHXDniC5+AsjzFd6fi8uyYlJA==
   dependencies:
     fflate "^0.4.1"
+    rrweb-snapshot "^1.1.14"
 
 prepend-http@^2.0.0:
   version "2.0.0"
@@ -6384,6 +6385,11 @@ roarr@^2.15.3:
     semver-compare "^1.0.0"
     sprintf-js "^1.1.2"
 
+rrweb-snapshot@^1.1.14:
+  version "1.1.14"
+  resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.14.tgz#9d4d9be54a28a893373428ee4393ec7e5bd83fcc"
+  integrity sha512-eP5pirNjP5+GewQfcOQY4uBiDnpqxNRc65yKPW0eSoU1XamDfc4M8oqpXGMyUyvLyxFDB0q0+DChuxxiU2FXBQ==
+
 run-parallel@^1.1.9:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"