Procházet zdrojové kódy

Merge branch 'master' into refactor/refs-from-property-values

Gabriel Horner před 3 roky
rodič
revize
9d9e976b8c
100 změnil soubory, kde provedl 767 přidání a 2511 odebrání
  1. 4 2
      .github/workflows/build-android.yml
  2. 3 0
      .github/workflows/db.yml
  3. 3 0
      .github/workflows/graph-parser.yml
  4. 1 0
      .gitignore
  5. 4 3
      android/app/build.gradle
  6. 3 2
      android/app/capacitor.build.gradle
  7. 1 0
      android/app/src/main/AndroidManifest.xml
  8. 0 21
      android/app/src/main/assets/capacitor.config.json
  9. 4 0
      android/app/src/main/assets/capacitor.plugins.json
  10. 0 351
      android/app/src/main/java/com/logseq/app/FileSync.java
  11. 1 2
      android/app/src/main/java/com/logseq/app/MainActivity.java
  12. 6 0
      android/app/src/main/res/drawable/splash_centered.xml
  13. 4 0
      android/app/src/main/res/values/colors.xml
  14. 9 4
      android/app/src/main/res/values/styles.xml
  15. 5 2
      android/build.gradle
  16. 3 0
      android/capacitor.settings.gradle
  17. 0 1
      android/file-sync/.gitignore
  18. 0 36
      android/file-sync/build.gradle
  19. 0 0
      android/file-sync/consumer-rules.pro
  20. 0 21
      android/file-sync/proguard-rules.pro
  21. 0 5
      android/file-sync/src/main/AndroidManifest.xml
  22. 0 33
      android/file-sync/src/main/java/com/logseq/sync/FileMeta.java
  23. 0 34
      android/file-sync/src/main/java/com/logseq/sync/RSFileSync.java
  24. binární
      android/file-sync/src/main/jniLibs/arm64-v8a/librsapi.so
  25. binární
      android/file-sync/src/main/jniLibs/armeabi-v7a/librsapi.so
  26. binární
      android/file-sync/src/main/jniLibs/x86/librsapi.so
  27. binární
      android/file-sync/src/main/jniLibs/x86_64/librsapi.so
  28. 1 1
      android/gradle/wrapper/gradle-wrapper.properties
  29. 1 1
      android/settings.gradle
  30. 14 12
      android/variables.gradle
  31. 5 0
      capacitor.config.ts
  32. 4 1
      deps/db/bb.edn
  33. 1 0
      deps/db/src/logseq/db.cljs
  34. 1 0
      deps/db/src/logseq/db/default.cljs
  35. 2 1
      deps/db/src/logseq/db/schema.cljs
  36. 9 3
      deps/graph-parser/bb.edn
  37. 2 1
      deps/graph-parser/src/logseq/graph_parser.cljs
  38. 1 1
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  39. 1 1
      deps/graph-parser/src/logseq/graph_parser/config.cljs
  40. 2 0
      deps/graph-parser/src/logseq/graph_parser/extract.cljc
  41. 2 2
      deps/graph-parser/src/logseq/graph_parser/log.cljs
  42. 2 0
      deps/graph-parser/src/logseq/graph_parser/mldoc.cljc
  43. 1 1
      deps/graph-parser/src/logseq/graph_parser/property.cljs
  44. 1 0
      deps/graph-parser/src/logseq/graph_parser/text.cljs
  45. 2 1
      deps/graph-parser/src/logseq/graph_parser/utf8.cljs
  46. 1 1
      deps/graph-parser/src/logseq/graph_parser/util/block_ref.cljs
  47. 2 2
      deps/graph-parser/src/logseq/graph_parser/util/page_ref.cljs
  48. 7 0
      docs/dev-practices.md
  49. 4 36
      ios/App/App.xcodeproj/project.pbxproj
  50. 4 19
      ios/App/App/AppDelegate.swift
  51. 0 110
      ios/App/App/FileSync/AgeEncryption.swift
  52. 0 211
      ios/App/App/FileSync/Extensions.swift
  53. 0 25
      ios/App/App/FileSync/FileSync.m
  54. 0 596
      ios/App/App/FileSync/FileSync.swift
  55. 0 36
      ios/App/App/FileSync/Payload.swift
  56. 0 381
      ios/App/App/FileSync/SyncClient.swift
  57. 0 5
      ios/App/App/Info.plist
  58. 12 4
      ios/App/Podfile
  59. 1 1
      libs/src/LSPlugin.ts
  60. 14 13
      package.json
  61. 1 5
      resources/css/common.css
  62. 1 0
      resources/electron.html
  63. 1 0
      resources/index.html
  64. 0 0
      resources/js/tabler.min.js
  65. 2 2
      resources/package.json
  66. 1 1
      shadow-cljs.edn
  67. 1 0
      src/electron/electron/core.cljs
  68. 6 9
      src/electron/electron/file_sync_rsapi.cljs
  69. 0 3
      src/electron/electron/handler.cljs
  70. 1 1
      src/electron/electron/window.cljs
  71. 54 54
      src/main/frontend/components/block.css
  72. 24 15
      src/main/frontend/components/command_palette.css
  73. 9 9
      src/main/frontend/components/encryption.cljs
  74. 44 48
      src/main/frontend/components/file_sync.cljs
  75. 8 8
      src/main/frontend/components/header.cljs
  76. 10 10
      src/main/frontend/components/header.css
  77. 12 6
      src/main/frontend/components/page.cljs
  78. 5 5
      src/main/frontend/components/page.css
  79. 15 12
      src/main/frontend/components/plugins.cljs
  80. 5 0
      src/main/frontend/components/plugins.css
  81. 82 76
      src/main/frontend/components/query_table.cljs
  82. 6 3
      src/main/frontend/components/repo.cljs
  83. 13 9
      src/main/frontend/components/right_sidebar.cljs
  84. 4 4
      src/main/frontend/components/search.cljs
  85. 2 1
      src/main/frontend/components/settings.cljs
  86. 12 19
      src/main/frontend/components/settings.css
  87. 21 19
      src/main/frontend/components/sidebar.cljs
  88. 26 24
      src/main/frontend/components/sidebar.css
  89. 31 37
      src/main/frontend/db/query_dsl.cljs
  90. 53 33
      src/main/frontend/dicts.cljc
  91. 3 3
      src/main/frontend/encrypt.cljs
  92. 1 1
      src/main/frontend/fs.cljs
  93. 28 41
      src/main/frontend/fs/capacitor_fs.cljs
  94. 115 48
      src/main/frontend/fs/sync.cljs
  95. 9 4
      src/main/frontend/handler/events.cljs
  96. 3 10
      src/main/frontend/handler/file_sync.cljs
  97. 6 3
      src/main/frontend/handler/repo.cljs
  98. 2 3
      src/main/frontend/handler/user.cljs
  99. 1 1
      src/main/frontend/handler/web/nfs.cljs
  100. 1 1
      src/main/frontend/mobile/footer.cljs

+ 4 - 2
.github/workflows/build-android.yml

@@ -43,6 +43,7 @@ on:
 env:
   CLOJURE_VERSION: '1.10.1.763'
   NODE_VERSION: '16'
+  JAVA_VERSION: '11'
 
 jobs:
   build-apk:
@@ -72,9 +73,10 @@ jobs:
             ${{ runner.os }}-yarn-
 
       - name: Setup Java JDK
-        uses: actions/setup-java@v1.4.3
+        uses: actions/setup-java@v2
         with:
-          java-version: 1.8
+          distribution: 'zulu'
+          java-version: ${{ env.JAVA_VERSION }}
 
       - name: Cache clojure deps
         uses: actions/cache@v2

+ 3 - 0
.github/workflows/db.yml

@@ -91,3 +91,6 @@ jobs:
 
       - name: Lint datalog rules
         run: bb lint:rules
+
+      - name: Lint for namespaces that aren't documented
+        run: bb lint:ns-docstrings

+ 3 - 0
.github/workflows/graph-parser.yml

@@ -120,3 +120,6 @@ jobs:
 
       - name: Lint for vars that are too large
         run: bb lint:large-vars
+
+      - name: Lint for namespaces that aren't documented
+        run: bb lint:ns-docstrings

+ 1 - 0
.gitignore

@@ -47,3 +47,4 @@ startup.png
 ~*~
 
 ios/App/App/capacitor.config.json
+android/app/src/main/assets/capacitor.config.json

+ 4 - 3
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 40
-        versionName "0.8.6"
+        versionCode 41
+        versionName "0.8.7"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -30,6 +30,8 @@ repositories {
 }
 
 dependencies {
+    implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
+    implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
     implementation fileTree(include: ['*.jar'], dir: 'libs')
     implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
     implementation project(':capacitor-android')
@@ -38,7 +40,6 @@ dependencies {
     androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
     androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
     implementation project(':capacitor-cordova-android-plugins')
-    implementation project(':file-sync')
 }
 
 apply from: 'capacitor.build.gradle'

+ 3 - 2
android/app/capacitor.build.gradle

@@ -2,8 +2,8 @@
 
 android {
   compileOptions {
-      sourceCompatibility JavaVersion.VERSION_1_8
-      targetCompatibility JavaVersion.VERSION_1_8
+      sourceCompatibility JavaVersion.VERSION_11
+      targetCompatibility JavaVersion.VERSION_11
   }
 }
 
@@ -18,6 +18,7 @@ dependencies {
     implementation project(':capacitor-share')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-status-bar')
+    implementation project(':logseq-capacitor-file-sync')
     implementation project(':capacitor-voice-recorder')
     implementation project(':send-intent')
 

+ 1 - 0
android/app/src/main/AndroidManifest.xml

@@ -12,6 +12,7 @@
         android:theme="@style/AppTheme">
 
         <activity
+            android:exported="true"
             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
             android:name="com.logseq.app.MainActivity"
             android:label="@string/title_activity_main"

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

@@ -1,21 +0,0 @@
-{
-	"appId": "com.logseq.app",
-	"appName": "Logseq",
-	"bundledWebRuntime": false,
-	"webDir": "public",
-	"plugins": {
-		"SplashScreen": {
-			"launchShowDuration": 500,
-			"launchAutoHide": false,
-			"androidScaleType": "CENTER_CROP",
-			"splashImmersive": false,
-			"backgroundColor": "#002b36"
-		},
-		"Keyboard": {
-			"resize": "none"
-		}
-	},
-	"ios": {
-		"scheme": "Logseq"
-	}
-}

+ 4 - 0
android/app/src/main/assets/capacitor.plugins.json

@@ -35,6 +35,10 @@
 		"pkg": "@capacitor/status-bar",
 		"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
 	},
+	{
+		"pkg": "@logseq/capacitor-file-sync",
+		"classpath": "com.logseq.app.FileSyncPlugin"
+	},
 	{
 		"pkg": "capacitor-voice-recorder",
 		"classpath": "com.tchvu3.capacitorvoicerecorder.VoiceRecorder"

+ 0 - 351
android/app/src/main/java/com/logseq/app/FileSync.java

@@ -1,351 +0,0 @@
-package com.logseq.app;
-
-import android.net.Uri;
-import android.util.Log;
-
-import com.getcapacitor.JSArray;
-import com.getcapacitor.JSObject;
-import com.getcapacitor.Plugin;
-import com.getcapacitor.annotation.CapacitorPlugin;
-import com.getcapacitor.PluginCall;
-import com.getcapacitor.PluginMethod;
-import com.logseq.sync.FileMeta;
-import com.logseq.sync.RSFileSync;
-
-import org.json.JSONException;
-
-import java.util.List;
-
-@CapacitorPlugin(name = "FileSync")
-public class FileSync extends Plugin {
-
-    @Override
-    public void load() {
-        super.load();
-
-        Log.i("FileSync", "Android plugin loaded");
-    }
-
-    @PluginMethod()
-    public void keygen(PluginCall call) {
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                String[] keyPairs = RSFileSync.keygen();
-                JSObject data = new JSObject();
-                data.put("secretKey", keyPairs[0]);
-                data.put("publicKey", keyPairs[1]);
-                call.resolve(data);
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void setKey(PluginCall call) {
-        String secretKey = call.getString("secretKey");
-        String publicKey = call.getString("publicKey");
-        long code = RSFileSync.setKeys(secretKey, publicKey);
-        if (code != -1) {
-            JSObject ret = new JSObject();
-            ret.put("ok", true);
-            call.resolve(ret);
-        } else {
-            call.reject("invalid setKey call");
-        }
-    }
-
-    @PluginMethod()
-    public void setEnv(PluginCall call) {
-        String env = call.getString("env");
-        if (env == null) {
-            call.reject("required parameter: env");
-            return;
-        }
-        this.setKey(call);
-        long code = RSFileSync.setEnvironment(env);
-        if (code != -1) {
-            JSObject ret = new JSObject();
-            ret.put("ok", true);
-            call.resolve(ret);
-        } else {
-            call.reject("invalid setEnv call");
-        }
-    }
-
-    @PluginMethod()
-    public void encryptFnames(PluginCall call) {
-        call.setKeepAlive(true);
-
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                List<String> filePaths = null;
-                try {
-                    filePaths = call.getArray("filePaths").toList();
-                } catch (JSONException e) {
-                    e.printStackTrace();
-                    return;
-                }
-
-                for (int i = 0; i < filePaths.size(); i++) {
-                    String filePath = filePaths.get(i);
-                    filePaths.set(i, Uri.decode(filePath));
-                }
-
-                String[] raw;
-                raw = RSFileSync.encryptFilenames(filePaths);
-                if (raw != null) {
-                    JSObject ret = new JSObject();
-                    ret.put("value", JSArray.from(raw));
-                    call.resolve(ret);
-                }
-
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void decryptFnames(PluginCall call) {
-        call.setKeepAlive(true);
-
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                JSArray filePaths = call.getArray("filePaths");
-                String[] raw;
-                try {
-                    raw = RSFileSync.decryptFilenames(filePaths.toList());
-                    for (int i = 0; i < raw.length; i++) {
-                        raw[i] = Uri.encode(raw[i], "/");
-                    }
-                    if (raw != null) {
-                        JSObject ret = new JSObject();
-                        ret.put("value", JSArray.from(raw));
-                        call.resolve(ret);
-                    }
-                } catch (JSONException e) {
-                    e.printStackTrace();
-                    call.reject("cannot decrypt fnames: " + e.toString());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    //@PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
-    @PluginMethod()
-    public void getLocalFilesMeta(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-
-
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                for (int i = 0; i < filePaths.size(); i++) {
-                    String filePath = filePaths.get(i);
-                    filePaths.set(i, Uri.decode(filePath));
-                }
-
-                FileMeta[] metas = RSFileSync.getLocalFilesMeta(basePath, filePaths);
-                if (metas == null) {
-                    call.reject(RSFileSync.getLastError());
-                    return;
-                }
-                JSObject dict = new JSObject();
-                for (FileMeta meta : metas) {
-                    if (meta == null) {
-                        continue;
-                    }
-                    Log.i("FileSync", "got meta " + meta.toString());
-                    JSObject item = new JSObject();
-                    item.put("md5", meta.md5);
-                    item.put("size", meta.size);
-                    item.put("encryptedFname", meta.encryptedFilename);
-
-                    item.put("mtime", meta.mtime); // not used for now
-                    dict.put(Uri.encode(meta.filePath, "/"), item);
-                }
-                JSObject ret = new JSObject();
-                ret.put("result", dict);
-                call.resolve(ret);
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void getLocalAllFilesMeta(PluginCall call) {
-        call.setKeepAlive(true);
-
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                String basePath = call.getString("basePath");
-                FileMeta[] metas = RSFileSync.getLocalAllFilesMeta(basePath);
-                if (metas == null) {
-                    call.reject(RSFileSync.getLastError());
-                    return;
-                }
-                JSObject dict = new JSObject();
-                for (FileMeta meta : metas) {
-                    JSObject item = new JSObject();
-                    item.put("md5", meta.md5);
-                    item.put("size", meta.size);
-                    item.put("encryptedFname", meta.encryptedFilename);
-
-                    item.put("mtime", meta.mtime); // not used for now
-                    dict.put(Uri.encode(meta.filePath, "/"), item);
-                }
-                JSObject ret = new JSObject();
-                ret.put("result", dict);
-                call.resolve(ret);
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void deleteLocalFiles(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        RSFileSync.deleteLocalFiles(basePath, filePaths);
-
-        JSObject ret = new JSObject();
-        ret.put("ok", true);
-        call.resolve(ret);
-    }
-
-    @PluginMethod()
-    public void updateLocalFiles(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-        String graphUUID = call.getString("graphUUID");
-        String token = call.getString("token");
-
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                long code = RSFileSync.updateLocalFiles(basePath, filePaths, graphUUID, token);
-                if (code != -1) {
-                    JSObject ret = new JSObject();
-                    ret.put("ok", true);
-                    call.resolve(ret);
-                } else {
-                    call.reject(RSFileSync.getLastError());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void updateLocalVersionFiles(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-        String graphUUID = call.getString("graphUUID");
-        String token = call.getString("token");
-
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                long code = RSFileSync.updateLocalVersionFiles(basePath, filePaths, graphUUID, token);
-                if (code != -1) {
-                    JSObject ret = new JSObject();
-                    ret.put("ok", true);
-                    call.resolve(ret);
-                } else {
-                    call.reject(RSFileSync.getLastError());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void deleteRemoteFiles(PluginCall call) throws JSONException {
-        List<String> filePaths = call.getArray("filePaths").toList();
-        String graphUUID = call.getString("graphUUID");
-        String token = call.getString("token");
-        long txid = call.getInt("txid").longValue();
-
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        call.setKeepAlive(true);
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                long code = RSFileSync.deleteRemoteFiles(filePaths, graphUUID, token, txid);
-                if (code != -1) {
-                    JSObject ret = new JSObject();
-                    ret.put("ok", true);
-                    ret.put("txid", code);
-                    call.resolve(ret);
-                } else {
-                    call.reject(RSFileSync.getLastError());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void updateRemoteFiles(PluginCall call) throws JSONException {
-        String basePath = call.getString("basePath");
-        List<String> filePaths = call.getArray("filePaths").toList();
-        String graphUUID = call.getString("graphUUID");
-        String token = call.getString("token");
-        long txid = call.getInt("txid").longValue();
-        // NOTE: fnameEncryption is ignored. since it's always on.
-
-        for (int i = 0; i < filePaths.size(); i++) {
-            filePaths.set(i, Uri.decode(filePaths.get(i)));
-        }
-
-        Thread runner = new Thread() {
-            @Override
-            public void run() {
-                long code = RSFileSync.updateRemoteFiles(basePath, filePaths, graphUUID, token, txid);
-                if (code != -1) {
-                    JSObject ret = new JSObject();
-                    ret.put("ok", true);
-                    ret.put("txid", code);
-                    call.resolve(ret);
-                } else {
-                    call.reject(RSFileSync.getLastError());
-                }
-            }
-        };
-        runner.start();
-    }
-
-    @PluginMethod()
-    public void ageEncryptWithPassphrase(PluginCall call) {
-        call.reject("unimplemented");
-    }
-
-    @PluginMethod()
-    public void ageDecryptWithPassphrase(PluginCall call) {
-        call.reject("unimplemented");
-    }
-}

+ 1 - 2
android/app/src/main/java/com/logseq/app/MainActivity.java

@@ -12,10 +12,9 @@ import java.util.TimerTask;
 public class MainActivity extends BridgeActivity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
         registerPlugin(FolderPicker.class);
         registerPlugin(FsWatcher.class);
-        registerPlugin(FileSync.class);
+        super.onCreate(savedInstanceState);
 
         new Timer().schedule(new TimerTask() {
             @Override

+ 6 - 0
android/app/src/main/res/drawable/splash_centered.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list  xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@color/logoPrimary" />
+    <item android:drawable="@drawable/splash" android:gravity="center" />
+</layer-list>
+

+ 4 - 0
android/app/src/main/res/values/colors.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="logoPrimary">#002b36</color>
+</resources>

+ 9 - 4
android/app/src/main/res/values/styles.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
 
     <!-- Base application theme. -->
     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
@@ -9,14 +9,19 @@
         <item name="colorAccent">@color/colorAccent</item>
     </style>
 
-    <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
+    <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
         <item name="windowActionBar">false</item>
         <item name="windowNoTitle">true</item>
         <item name="android:background">@null</item>
         <item name="android:windowIsTranslucent">true</item>
     </style>
 
-    <style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
-        <item name="android:background">@drawable/splash</item>
+    <!-- App Starting -->
+    <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
+        <item name="android:windowBackground">@drawable/splash_centered</item>
+
+        <item name="windowSplashScreenAnimationDuration">200</item>
+        <item name="windowSplashScreenIconBackgroundColor">@color/logoPrimary</item>
+        <item name="postSplashScreenTheme">@style/AppTheme</item>
     </style>
 </resources>

+ 5 - 2
android/build.gradle

@@ -5,10 +5,11 @@ buildscript {
     repositories {
         google()
         jcenter()
+        mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:4.2.1'
-        classpath 'com.google.gms:google-services:4.3.5'
+        classpath 'com.android.tools.build:gradle:7.2.1'
+        classpath 'com.google.gms:google-services:4.3.13'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
@@ -21,9 +22,11 @@ allprojects {
     repositories {
         google()
         jcenter()
+        mavenCentral()
     }
 }
 
 task clean(type: Delete) {
     delete rootProject.buildDir
 }
+

+ 3 - 0
android/capacitor.settings.gradle

@@ -29,6 +29,9 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa
 include ':capacitor-status-bar'
 project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
 
+include ':logseq-capacitor-file-sync'
+project(':logseq-capacitor-file-sync').projectDir = new File('../node_modules/@logseq/capacitor-file-sync/android')
+
 include ':capacitor-voice-recorder'
 project(':capacitor-voice-recorder').projectDir = new File('../node_modules/capacitor-voice-recorder/android')
 

+ 0 - 1
android/file-sync/.gitignore

@@ -1 +0,0 @@
-/build

+ 0 - 36
android/file-sync/build.gradle

@@ -1,36 +0,0 @@
-plugins {
-    id 'com.android.library'
-}
-
-android {
-    compileSdkVersion 30
-
-    defaultConfig {
-        minSdkVersion 21
-        targetSdkVersion 30
-        versionCode 1
-        versionName "1.0"
-
-        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
-        consumerProguardFiles "consumer-rules.pro"
-    }
-
-    buildTypes {
-        release {
-            minifyEnabled false
-            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
-        }
-    }
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-}
-
-dependencies {
-
-    implementation 'com.android.support:appcompat-v7:28.0.0'
-    testImplementation 'junit:junit:4.+'
-    androidTestImplementation 'com.android.support.test:runner:1.0.2'
-    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
-}

+ 0 - 0
android/file-sync/consumer-rules.pro


+ 0 - 21
android/file-sync/proguard-rules.pro

@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-#   http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-#   public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile

+ 0 - 5
android/file-sync/src/main/AndroidManifest.xml

@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.logseq.sync">
-
-</manifest>

+ 0 - 33
android/file-sync/src/main/java/com/logseq/sync/FileMeta.java

@@ -1,33 +0,0 @@
-package com.logseq.sync;
-
-public class FileMeta {
-    public String filePath;
-    public long size;
-    public long mtime;
-    public String md5;
-    public String encryptedFilename;
-
-    public FileMeta(String filePath, long size, long mtime, String md5) {
-        this.filePath = filePath;
-        this.size = size;
-        this.mtime = mtime;
-        this.md5 = md5;
-        this.encryptedFilename = encryptedFilename;
-    }
-
-    public FileMeta(long size, long mtime, String md5) {
-        this.size = size;
-        this.mtime = mtime;
-        this.md5 = md5;
-        this.encryptedFilename = null;
-    }
-
-    public String toString() {
-        return "FileMeta{" +
-                "size=" + size +
-                ", mtime=" + mtime +
-                ", md5='" + md5 + '\'' +
-                ", encryptedFilename='" + encryptedFilename + '\'' +
-                '}';
-    }
-}

+ 0 - 34
android/file-sync/src/main/java/com/logseq/sync/RSFileSync.java

@@ -1,34 +0,0 @@
-package com.logseq.sync;
-
-import java.util.List;
-
-public class RSFileSync {
-    static {
-        System.loadLibrary("rsapi");
-    }
-
-    public static native String getLastError();
-
-    public static native String[] keygen();
-
-    public static native long setEnvironment(String env);
-    public static native long setKeys(String secretKey, String publicKey);
-
-    public static native String[] encryptFilenames(List<String> filenames);
-    public static native String[] decryptFilenames(List<String> encryptedFilenames);
-
-    public static native FileMeta[] getLocalFilesMeta(String basePath, List<String> filePaths);
-    public static native FileMeta[] getLocalAllFilesMeta(String basePath);
-
-    public static native long renameLocalFile(String basePath, String oldPath, String newPath);
-
-    public static native void deleteLocalFiles(String basePath, List<String> filePaths);
-
-    public static native long updateLocalFiles(String basePath, List<String> filePaths, String graphUUID, String token);
-
-    public static native long updateLocalVersionFiles(String basePath, List<String> filePaths, String graphUUID, String token);
-
-    public static native long deleteRemoteFiles(List<String> filePaths, String graphUUID, String token, long txid);
-
-    public static native long updateRemoteFiles(String basePath, List<String> filePaths, String graphUUID, String token, long txid);
-}

binární
android/file-sync/src/main/jniLibs/arm64-v8a/librsapi.so


binární
android/file-sync/src/main/jniLibs/armeabi-v7a/librsapi.so


binární
android/file-sync/src/main/jniLibs/x86/librsapi.so


binární
android/file-sync/src/main/jniLibs/x86_64/librsapi.so


+ 1 - 1
android/gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 1 - 1
android/settings.gradle

@@ -3,4 +3,4 @@ include ':capacitor-cordova-android-plugins'
 project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
 
 apply from: 'capacitor.settings.gradle'
-include ':file-sync'
+

+ 14 - 12
android/variables.gradle

@@ -1,14 +1,16 @@
 ext {
-    minSdkVersion = 21
-    compileSdkVersion = 30
-    targetSdkVersion = 30
-    androidxActivityVersion = '1.2.0'
-    androidxAppCompatVersion = '1.2.0'
-    androidxCoordinatorLayoutVersion = '1.1.0'
-    androidxCoreVersion = '1.3.2'
-    androidxFragmentVersion = '1.3.0'
-    junitVersion = '4.13.1'
-    androidxJunitVersion = '1.1.2'
-    androidxEspressoCoreVersion = '3.3.0'
-    cordovaAndroidVersion = '7.0.0'
+    minSdkVersion = 22
+    compileSdkVersion = 32
+    targetSdkVersion = 32
+    androidxActivityVersion = '1.4.0'
+    androidxAppCompatVersion = '1.4.2'
+    androidxCoordinatorLayoutVersion = '1.2.0'
+    androidxCoreVersion = '1.8.0'
+    androidxFragmentVersion = '1.4.1'
+    junitVersion = '4.13.2'
+    androidxJunitVersion = '1.1.3'
+    androidxEspressoCoreVersion = '3.4.0'
+    cordovaAndroidVersion = '10.1.1'
+    coreSplashScreenVersion = '1.0.0-rc01'
+    androidxWebkitVersion = '1.4.0'
 }

+ 5 - 0
capacitor.config.ts

@@ -20,6 +20,11 @@ const config: CapacitorConfig = {
   },
   ios: {
     scheme: 'Logseq'
+  },
+  cordova: {
+    staticPlugins: [
+      '@logseq/capacitor-file-sync', // AgeEncryption requires static link
+    ]
   }
 }
 

+ 4 - 1
deps/db/bb.edn

@@ -4,7 +4,7 @@
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "abb32ccd26405d56fd28a29d56f3cb902b8c4334"}}
+   :git/sha "1815db538241082a01e95601e23e4290dd64d0c0"}}
 
  :pods
  {clj-kondo/clj-kondo {:version "2022.02.09"}}
@@ -19,6 +19,9 @@
   lint:carve
   logseq.bb-tasks.lint.carve/-main
 
+  lint:ns-docstrings
+  logseq.bb-tasks.lint.ns-docstrings/-main
+
   lint:rules
   {:requires ([logseq.bb-tasks.lint.datalog :as datalog]
               [logseq.db.rules :as rules])

+ 1 - 0
deps/db/src/logseq/db.cljs

@@ -1,4 +1,5 @@
 (ns logseq.db
+  "Main namespace for public db fns"
   (:require [logseq.db.default :as default-db]
             [logseq.db.schema :as db-schema]
             [datascript.core :as d]))

+ 1 - 0
deps/db/src/logseq/db/default.cljs

@@ -1,4 +1,5 @@
 (ns logseq.db.default
+  "Provides fns for seeding default data in a logseq db"
   (:require [clojure.string :as string]))
 
 (defonce built-in-pages-names

+ 2 - 1
deps/db/src/logseq/db/schema.cljs

@@ -1,4 +1,5 @@
-(ns logseq.db.schema)
+(ns logseq.db.schema
+  "Main db schema for the Logseq app")
 
 (defonce version 1)
 (defonce ast-version 1)

+ 9 - 3
deps/graph-parser/bb.edn

@@ -3,8 +3,8 @@
  {logseq/bb-tasks
   #_{:local/root "../../../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
-   :git/sha "abb32ccd26405d56fd28a29d56f3cb902b8c4334"}}
-
+   :git/sha "1815db538241082a01e95601e23e4290dd64d0c0"}}
+ 
  :pods
  {clj-kondo/clj-kondo {:version "2022.02.09"}}
 
@@ -16,7 +16,13 @@
   logseq.bb-tasks.lint.large-vars/-main
 
   lint:carve
-  logseq.bb-tasks.lint.carve/-main}
+  logseq.bb-tasks.lint.carve/-main
+
+  lint:ns-docstrings
+  logseq.bb-tasks.lint.ns-docstrings/-main
+
+  lint:minimize-public-vars
+  logseq.bb-tasks.lint.minimize-public-vars/-main}
 
  :tasks/config
  {:large-vars

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

@@ -1,5 +1,6 @@
 (ns logseq.graph-parser
-  "Main ns used by logseq app to parse graph from source files"
+  "Main ns used by logseq app to parse graph from source files and then save to
+  the given database connection"
   (:require [datascript.core :as d]
             [logseq.graph-parser.extract :as extract]
             [logseq.graph-parser.util :as gp-util]

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

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.block
-  "Block related code needed for graph-parser"
+  "Given mldoc ast, prepares block data in preparation for db transaction"
   (:require [clojure.set :as set]
             [clojure.string :as string]
             [clojure.walk :as walk]

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

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.config
-  "Config that is shared between graph-parser and rest of app"
+  "App config that is shared between graph-parser and rest of app"
   (:require [clojure.set :as set]
             [clojure.string :as string]))
 

+ 2 - 0
deps/graph-parser/src/logseq/graph_parser/extract.cljc

@@ -1,4 +1,6 @@
 (ns logseq.graph-parser.extract
+  "Handles extraction of blocks, pages and mldoc ast in preparation for db
+  transaction"
   ;; Disable clj linters since we don't support clj
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
                                         :unresolved-symbol {:level :off}}}})

+ 2 - 2
deps/graph-parser/src/logseq/graph_parser/log.cljs

@@ -1,6 +1,6 @@
 (ns logseq.graph-parser.log
-  "Minimal logging ns that implements basic lambdaisland.glogi fns. May use
-  glogi later if this ns is used more")
+  "Minimal, logging ns that shims lambdaisland.glogi fns for nbb. Could port
+  glogi to nbb later if this shim gets too big")
 
 (defn error [& msgs]
   (apply js/console.error (map clj->js msgs)))

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

@@ -1,4 +1,6 @@
 (ns logseq.graph-parser.mldoc
+  "Wraps https://github.com/logseq/mldoc to parse files into mldoc ast. This ns
+  encapsulates mldoc's json api by only taking and returning edn"
   ;; Disable clj linters since we don't support clj
   #?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
                                         :unresolved-symbol {:level :off}}}})

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

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.property
-  "Property fns needed by graph-parser"
+  "Core vars and util fns for properties"
   (:require [logseq.graph-parser.util :as gp-util]
             [clojure.string :as string]
             [clojure.set :as set]

+ 1 - 0
deps/graph-parser/src/logseq/graph_parser/text.cljs

@@ -1,4 +1,5 @@
 (ns logseq.graph-parser.text
+  "Miscellaneous text util fns for the parser"
   (:require ["path" :as path]
             [goog.string :as gstring]
             [clojure.string :as string]

+ 2 - 1
deps/graph-parser/src/logseq/graph_parser/utf8.cljs

@@ -1,4 +1,5 @@
-(ns logseq.graph-parser.utf8)
+(ns logseq.graph-parser.utf8
+  "Utf8 utilities used by the parser")
 
 (defonce encoder
   (js/TextEncoder. "utf-8"))

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/util/block_ref.cljs

@@ -1,5 +1,5 @@
 (ns logseq.graph-parser.util.block-ref
-  "General purpose vars and util fns for block-refs"
+  "Core vars and util fns for block-refs"
   (:require [clojure.string :as string]))
 
 (def left-parens "Opening characters for block-ref" "((")

+ 2 - 2
deps/graph-parser/src/logseq/graph_parser/util/page_ref.cljs

@@ -1,6 +1,6 @@
 (ns logseq.graph-parser.util.page-ref
-  "General purpose vars and util fns for page-ref. Currently this only handles
-a logseq page-ref e.g. [[page name]]"
+  "Core vars and util fns for page-ref. Currently this only handles a logseq
+  page-ref e.g. [[page name]]"
   (:require [clojure.string :as string]))
 
 (def left-brackets "Opening characters for page-ref" "[[")

+ 7 - 0
docs/dev-practices.md

@@ -52,6 +52,13 @@ bb lint:large-vars
 
 To configure the linter, see the `[:tasks/config :large-vars]` path of bb.edn.
 
+### Document namespaces
+
+Documentation helps teams share their knowledge and enables more individuals to contribute to the codebase. Documenting our namespaces is a good first step to improving our documentation. Currently this linter is only run on our deps/. To run this linter:
+```
+bb lint:ns-docstrings
+```
+
 ### Datalog linting
 
 We use [datascript](https://github.com/tonsky/datascript)'s datalog to power our

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

@@ -25,14 +25,8 @@
 		D32752BE275496C60039291C /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D32752BD275496C60039291C /* CloudKit.framework */; };
 		D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A09275C92880003FBDC /* FileContainer.swift */; };
 		D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
-		FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1B27FF5420007ECE65 /* Extensions.swift */; };
-		FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1D27FF54AA007ECE65 /* Payload.swift */; };
-		FE443F2027FF54C9007ECE65 /* SyncClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE443F1F27FF54C9007ECE65 /* SyncClient.swift */; };
 		FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
 		FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
-		FE8C946B27FD762700C8017B /* FileSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946927FD762700C8017B /* FileSync.swift */; };
-		FE8C946C27FD762700C8017B /* FileSync.m in Sources */ = {isa = PBXBuildFile; fileRef = FE8C946A27FD762700C8017B /* FileSync.m */; };
-		FEE688A428448F8C0019510E /* AgeEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE688A328448F8C0019510E /* AgeEncryption.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -87,14 +81,8 @@
 		D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
 		D3D62A0B275C928F0003FBDC /* FileContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileContainer.m; sourceTree = "<group>"; };
 		DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
-		FE443F1B27FF5420007ECE65 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
-		FE443F1D27FF54AA007ECE65 /* Payload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Payload.swift; sourceTree = "<group>"; };
-		FE443F1F27FF54C9007ECE65 /* SyncClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncClient.swift; 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>"; };
-		FE8C946927FD762700C8017B /* FileSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSync.swift; sourceTree = "<group>"; };
-		FE8C946A27FD762700C8017B /* FileSync.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileSync.m; sourceTree = "<group>"; };
-		FEE688A328448F8C0019510E /* AgeEncryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgeEncryption.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -154,7 +142,6 @@
 				50B271D01FEDC1A000F3C39B /* public */,
 				7435D10B2704659F00AB88E0 /* FolderPicker.swift */,
 				FE647FF327BDFEDE00F3206B /* FsWatcher.swift */,
-				FE443F1A27FF53A2007ECE65 /* FileSync */,
 				FE647FF527BDFEF500F3206B /* FsWatcher.m */,
 				7435D10E2704660B00AB88E0 /* FolderPicker.m */,
 				D3D62A09275C92880003FBDC /* FileContainer.swift */,
@@ -193,19 +180,6 @@
 			path = Pods;
 			sourceTree = "<group>";
 		};
-		FE443F1A27FF53A2007ECE65 /* FileSync */ = {
-			isa = PBXGroup;
-			children = (
-				FE8C946927FD762700C8017B /* FileSync.swift */,
-				FE443F1F27FF54C9007ECE65 /* SyncClient.swift */,
-				FE443F1D27FF54AA007ECE65 /* Payload.swift */,
-				FE443F1B27FF5420007ECE65 /* Extensions.swift */,
-				FE8C946A27FD762700C8017B /* FileSync.m */,
-				FEE688A328448F8C0019510E /* AgeEncryption.swift */,
-			);
-			path = FileSync;
-			sourceTree = "<group>";
-		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -356,20 +330,14 @@
 			buildActionMask = 2147483647;
 			files = (
 				504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
-				FE443F1E27FF54AA007ECE65 /* Payload.swift in Sources */,
 				5FF8632C283B5BFD0047731B /* Utils.m in Sources */,
-				FEE688A428448F8C0019510E /* AgeEncryption.swift in Sources */,
-				FE8C946B27FD762700C8017B /* FileSync.swift in Sources */,
 				FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */,
 				5FF8632A283B5ADB0047731B /* Utils.swift in Sources */,
 				D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */,
 				D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */,
-				FE443F1C27FF5420007ECE65 /* Extensions.swift in Sources */,
-				FE8C946C27FD762700C8017B /* FileSync.m in Sources */,
 				7435D10F2704660B00AB88E0 /* FolderPicker.m in Sources */,
 				7435D10C2704659F00AB88E0 /* FolderPicker.swift in Sources */,
 				FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */,
-				FE443F2027FF54C9007ECE65 /* SyncClient.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -547,7 +515,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.8.6;
+				MARKETING_VERSION = 0.8.7;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -574,7 +542,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.8.6;
+				MARKETING_VERSION = 0.8.7;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -599,7 +567,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.8.6;
+				MARKETING_VERSION = 0.8.7;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -626,7 +594,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.8.6;
+				MARKETING_VERSION = 0.8.7;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 4 - 19
ios/App/App/AppDelegate.swift

@@ -36,14 +36,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
     }
 
-    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
-            
-            var success = true
-            if CAPBridge.handleOpenUrl(url, options) {
-                success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
-            }
-            return success
-        }
+    func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
+        return ApplicationDelegateProxy.shared.application(application, open: url, options: options)
+    }
 
     func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
         // Called when the app was launched with an activity, including Universal Links.
@@ -52,15 +47,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
         return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
     }
 
-    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
-        super.touchesBegan(touches, with: event)
-
-        let statusBarRect = UIApplication.shared.statusBarFrame
-        guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return }
-
-        if statusBarRect.contains(touchPoint) {
-            NotificationCenter.default.post(name: .capacitorStatusBarTapped, object: nil)
-        }
-    }
-
 }
+

+ 0 - 110
ios/App/App/FileSync/AgeEncryption.swift

@@ -1,110 +0,0 @@
-//
-//  AgeEncryption.swift
-//  Logseq
-//
-//  Created by Mono Wang on 5/30/R4.
-//
-
-import Foundation
-import AgeEncryption
-
-public enum AgeEncryption {
-    public static func keygen() -> (String, String) {
-        let cSecretKey = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-        let cPublicKey = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-        
-        rust_age_encryption_keygen(cSecretKey, cPublicKey);
-        let secretKey = String(cString: cSecretKey.pointee!)
-        let publicKey = String(cString: cPublicKey.pointee!)
-        
-        rust_age_encryption_free_str(cSecretKey.pointee!)
-        rust_age_encryption_free_str(cPublicKey.pointee!)
-        cSecretKey.deallocate()
-        cPublicKey.deallocate()
-
-        return (secretKey, publicKey)
-    }
-    
-    public static func toRawX25519Key(_ secretKey: String) -> Data? {
-        let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-
-        let ret = rust_age_encryption_to_raw_x25519_key(secretKey.cString(using: .utf8), cOutput)
-        if ret >= 0 {
-            let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: 32)
-            let rawKey = Data.init(buffer: cOutputBuf)
-            rust_age_encryption_free_vec(cOutput.pointee, ret)
-            cOutput.deallocate()
-            return rawKey
-        } else {
-            return nil
-        }
-    }
-    
-    public static func encryptWithPassphrase(_ plaintext: Data, _ passphrase: String, armor: Bool) -> Data? {
-        plaintext.withUnsafeBytes { (cPlaintext) in
-            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-
-            let ret = rust_age_encrypt_with_user_passphrase(passphrase.cString(using: .utf8), cPlaintext.bindMemory(to: CChar.self).baseAddress, Int32(plaintext.count), armor ? 1 : 0, cOutput)
-            if ret > 0 {
-                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
-                let ciphertext = Data.init(buffer: cOutputBuf)
-                rust_age_encryption_free_vec(cOutput.pointee, ret)
-                cOutput.deallocate()
-                return ciphertext
-            } else {
-                return nil
-            }
-        }
-    }
-
-    public static func decryptWithPassphrase(_ ciphertext: Data, _ passphrase: String) -> Data? {
-        ciphertext.withUnsafeBytes { (cCiphertext) in
-            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-
-            let ret = rust_age_decrypt_with_user_passphrase(passphrase.cString(using: .utf8), cCiphertext.bindMemory(to: CChar.self).baseAddress, Int32(ciphertext.count), cOutput)
-            if ret > 0 {
-                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
-                let plaintext = Data.init(buffer: cOutputBuf)
-                rust_age_encryption_free_vec(cOutput.pointee, ret)
-                cOutput.deallocate()
-                return plaintext
-            } else {
-                return nil
-            }
-        }
-    }
-    
-    public static func encryptWithX25519(_ plaintext: Data, _ publicKey: String, armor: Bool) -> Data? {
-        plaintext.withUnsafeBytes { (cPlaintext) in
-            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-            
-            let ret = rust_age_encrypt_with_x25519(publicKey.cString(using: .utf8), cPlaintext.bindMemory(to: CChar.self).baseAddress, Int32(plaintext.count), armor ? 1 : 0, cOutput)
-            if ret > 0 {
-                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
-                let ciphertext = Data.init(buffer: cOutputBuf)
-                rust_age_encryption_free_vec(cOutput.pointee, ret)
-                cOutput.deallocate()
-                return ciphertext
-            } else {
-                return nil
-            }
-        }
-    }
-    
-    public static func decryptWithX25519(_ ciphertext: Data, _ secretKey: String) -> Data? {
-        ciphertext.withUnsafeBytes { (cCiphertext) in
-            let cOutput = UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>.allocate(capacity: 1)
-
-            let ret = rust_age_decrypt_with_x25519(secretKey.cString(using: .utf8), cCiphertext.bindMemory(to: CChar.self).baseAddress, Int32(ciphertext.count), cOutput)
-            if ret >= 0 {
-                let cOutputBuf = UnsafeBufferPointer.init(start: cOutput.pointee, count: Int(ret))
-                let plaintext = Data.init(buffer: cOutputBuf)
-                rust_age_encryption_free_vec(cOutput.pointee, ret)
-                cOutput.deallocate()
-                return plaintext
-            } else {
-                return nil
-            }
-        }
-    }
-}

+ 0 - 211
ios/App/App/FileSync/Extensions.swift

@@ -1,211 +0,0 @@
-//
-//  Extensions.swift
-//  Logseq
-//
-//  Created by Mono Wang on 4/8/R4.
-//
-
-import Foundation
-import CryptoKit
-
-// via https://github.com/krzyzanowskim/CryptoSwift
-extension Array where Element == UInt8 {
-  public init(hex: String) {
-      self = Array.init()
-      self.reserveCapacity(hex.unicodeScalars.lazy.underestimatedCount)
-      var buffer: UInt8?
-      var skip = hex.hasPrefix("0x") ? 2 : 0
-      for char in hex.unicodeScalars.lazy {
-          guard skip == 0 else {
-              skip -= 1
-              continue
-          }
-          guard char.value >= 48 && char.value <= 102 else {
-              removeAll()
-              return
-          }
-          let v: UInt8
-          let c: UInt8 = UInt8(char.value)
-          switch c {
-          case let c where c <= 57:
-              v = c - 48
-          case let c where c >= 65 && c <= 70:
-              v = c - 55
-          case let c where c >= 97:
-              v = c - 87
-          default:
-              removeAll()
-              return
-          }
-          if let b = buffer {
-              append(b << 4 | v)
-              buffer = nil
-          } else {
-              buffer = v
-          }
-      }
-      if let b = buffer {
-          append(b)
-      }
-  }
-}
-
-extension Data {
-    public init?(hexEncoded: String) {
-        self.init(Array<UInt8>(hex: hexEncoded))
-    }
-    
-    var hexDescription: String {
-        return map { String(format: "%02hhx", $0) }.joined()
-    }
-    
-    var MD5: String {
-        let computed = Insecure.MD5.hash(data: self)
-        return computed.map { String(format: "%02hhx", $0) }.joined()
-    }
-}
-
-extension String {
-    var MD5: String {
-        let computed = Insecure.MD5.hash(data: self.data(using: .utf8)!)
-        return computed.map { String(format: "%02hhx", $0) }.joined()
-    }
-    
-    static func random(length: Int) -> String {
-        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-        return String((0..<length).map{ _ in letters.randomElement()! })
-    }
-    
-    func fnameEncrypt(rawKey: Data) -> String? {
-        guard !self.isEmpty else {
-            return nil
-        }
-        guard let raw = self.data(using: .utf8) else {
-            return nil
-        }
-        
-        let key = SymmetricKey(data: rawKey)
-        let nonce = try! ChaChaPoly.Nonce(data: Data(repeating: 0, count: 12))
-        guard let sealed = try? ChaChaPoly.seal(raw, using: key, nonce: nonce) else { return nil }
-        
-        // strip nonce here, since it's all zero
-        return "e." + (sealed.ciphertext + sealed.tag).hexDescription
-
-    }
-    
-    func fnameDecrypt(rawKey: Data) -> String? {
-        // well-formated, non-empty encrypted string
-        guard self.hasPrefix("e.") && self.count > 36 else {
-            return nil
-        }
-        
-        let encryptedHex = self.suffix(from: self.index(self.startIndex, offsetBy: 2))
-        guard let encryptedRaw = Data(hexEncoded: String(encryptedHex)) else {
-            // invalid hex
-            return nil
-        }
-        
-        let key = SymmetricKey(data: rawKey)
-        let nonce = Data(repeating: 0, count: 12)
-
-        guard let sealed = try? ChaChaPoly.SealedBox(combined: nonce + encryptedRaw) else {
-            return nil
-        }
-        guard let outputRaw = try? ChaChaPoly.open(sealed, using: key) else {
-            return nil
-        }
-        return String(data: outputRaw, encoding: .utf8)
-    }
-}
-
-extension URL {
-    func relativePath(from base: URL) -> String? {
-        // Ensure that both URLs represent files:
-        guard self.isFileURL && base.isFileURL else {
-            return nil
-        }
-        
-        // Remove/replace "." and "..", make paths absolute:
-        let destComponents = self.standardized.pathComponents
-        let baseComponents = base.standardized.pathComponents
-        
-        // Find number of common path components:
-        var i = 0
-        while i < destComponents.count && i < baseComponents.count
-                && destComponents[i] == baseComponents[i] {
-            i += 1
-        }
-        
-        // Build relative path:
-        var relComponents = Array(repeating: "..", count: baseComponents.count - i)
-        relComponents.append(contentsOf: destComponents[i...])
-        return relComponents.joined(separator: "/")
-    }
-    
-    // Download a remote URL to a file
-    func download(toFile file: URL, completion: @escaping (Error?) -> Void) {
-        let task = URLSession.shared.downloadTask(with: self) {
-            (tempURL, response, error) in
-            // Early exit on error
-            guard let tempURL = tempURL else {
-                completion(error)
-                return
-            }
-            
-            if let response = response! as? HTTPURLResponse {
-                if response.statusCode == 404 {
-                    completion(NSError(domain: "",
-                                       code: response.statusCode,
-                                       userInfo: [NSLocalizedDescriptionKey: "remote file not found"]))
-                    return
-                }
-                if response.statusCode != 200 {
-                    completion(NSError(domain: "",
-                                       code: response.statusCode,
-                                       userInfo: [NSLocalizedDescriptionKey: "invalid http status code"]))
-                    return
-                }
-            }
-            
-            do {
-                // Remove any existing document at file
-                if FileManager.default.fileExists(atPath: file.path) {
-                    try FileManager.default.removeItem(at: file)
-                } else {
-                    let baseURL = file.deletingLastPathComponent()
-                    try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true, attributes: nil)
-                }
-                let rawData = try Data(contentsOf: tempURL)
-                guard let decryptedRawData = maybeDecrypt(rawData) else {
-                    throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "can not decrypt remote file"])
-                }
-                try decryptedRawData.write(to: file, options: .atomic)
-
-                completion(nil)
-            }
-            
-            // Handle potential file system errors
-            catch {
-                completion(error)
-            }
-        }
-        
-        // Start the download
-        task.resume()
-    }
-}
-
-// MARK: Crypto helper
-
-extension SymmetricKey {
-    public init(passwordString keyString: String) throws {
-        guard let keyData = keyString.data(using: .utf8) else {
-            print("ERROR: Could not create raw Data from String")
-            throw CryptoKitError.incorrectParameterSize
-        }
-        // SymmetricKeySize.bits256
-        let keyDigest = SHA256.hash(data: keyData)
-        
-        self.init(data: keyDigest)
-    }
-}

+ 0 - 25
ios/App/App/FileSync/FileSync.m

@@ -1,25 +0,0 @@
-//
-//  FileSync.m
-//  Logseq
-//
-//  Created by Mono Wang on 2/24/R4.
-//
-
-#import <Capacitor/Capacitor.h>
-
-CAP_PLUGIN(FileSync, "FileSync",
-           CAP_PLUGIN_METHOD(setEnv, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(keygen, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(getLocalFilesMeta, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(getLocalAllFilesMeta, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(renameLocalFile, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(deleteLocalFiles, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(updateLocalFiles, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(deleteRemoteFiles, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(updateRemoteFiles, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(encryptFnames, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(decryptFnames, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(decryptWithPassphrase, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(encryptWithPassphrase, CAPPluginReturnPromise);
-           CAP_PLUGIN_METHOD(updateLocalVersionFiles, CAPPluginReturnPromise);
-)

+ 0 - 596
ios/App/App/FileSync/FileSync.swift

@@ -1,596 +0,0 @@
-//
-//  FileSync.swift
-//  Logseq
-//
-//  Created by Mono Wang on 2/24/R4.
-//
-
-import Capacitor
-import Foundation
-import AWSMobileClient
-import CryptoKit
-
-
-// MARK: Global variable
-
-// Defualts to dev
-var URL_BASE = URL(string: "https://api-dev.logseq.com/file-sync/")!
-var BUCKET: String = "logseq-file-sync-bucket"
-var REGION: String = "us-east-2"
-
-var ENCRYPTION_SECRET_KEY: String? = nil
-var ENCRYPTION_PUBLIC_KEY: String? = nil
-var FNAME_ENCRYPTION_KEY: Data? = nil
-
-
-// MARK: Helpers
-
-
-@inline(__always) func fnameEncryptionEnabled() -> Bool {
-    guard let _ = FNAME_ENCRYPTION_KEY else {
-        return false
-    }
-    return true
-}
-
-// MARK: encryption helper
-
-func maybeEncrypt(_ plaindata: Data) -> Data! {
-    // avoid encryption twice
-    if plaindata.starts(with: "-----BEGIN AGE ENCRYPTED FILE-----".data(using: .utf8)!) ||
-        plaindata.starts(with: "age-encryption.org/v1\n".data(using: .utf8)!) {
-        return plaindata
-    }
-    if let publicKey = ENCRYPTION_PUBLIC_KEY {
-        // use armor = false, for smaller size
-        if let cipherdata = AgeEncryption.encryptWithX25519(plaindata, publicKey, armor: true) {
-            return cipherdata
-        }
-        return nil // encryption fail
-    }
-    return plaindata
-}
-
-func maybeDecrypt(_ cipherdata: Data) -> Data! {
-    if let secretKey = ENCRYPTION_SECRET_KEY {
-        if cipherdata.starts(with: "-----BEGIN AGE ENCRYPTED FILE-----".data(using: .utf8)!) ||
-            cipherdata.starts(with: "age-encryption.org/v1\n".data(using: .utf8)!) {
-            if let plaindata = AgeEncryption.decryptWithX25519(cipherdata, secretKey) {
-                return plaindata
-            }
-            return nil
-        }
-        // not an encrypted file
-        return cipherdata
-    }
-    return cipherdata
-}
-
-// MARK: Metadata type
-
-public struct SyncMetadata: CustomStringConvertible, Equatable {
-    var md5: String
-    var size: Int
-    var ctime: Int64
-    var mtime: Int64
-
-    public init?(of fileURL: URL) {
-        do {
-            let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .fileSizeKey, .contentModificationDateKey,
-                                                                     .creationDateKey])
-            guard fileAttributes.isRegularFile! else {
-                return nil
-            }
-            size = fileAttributes.fileSize ?? 0
-            mtime = Int64((fileAttributes.contentModificationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
-            ctime = Int64((fileAttributes.creationDate?.timeIntervalSince1970 ?? 0.0) * 1000)
-
-            // incremental MD5 checksum
-            let bufferSize = 512 * 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), mtime=\(mtime))"
-    }
-}
-
-// MARK: FileSync Plugin
-
-@objc(FileSync)
-public class FileSync: CAPPlugin, SyncDebugDelegate {
-    override public func load() {
-        print("debug FileSync iOS plugin loaded!")
-
-        AWSMobileClient.default().initialize { (userState, error) in
-            guard error == nil else {
-                print("error initializing AWSMobileClient. Error: \(error!.localizedDescription)")
-                return
-            }
-        }
-    }
-
-    // NOTE: for debug, or an activity indicator
-    public func debugNotification(_ message: [String: Any]) {
-        self.notifyListeners("debug", data: message)
-    }
-
-    @objc func keygen(_ call: CAPPluginCall) {
-        let (secretKey, publicKey) = AgeEncryption.keygen()
-        call.resolve(["secretKey": secretKey,
-                      "publicKey": publicKey])
-    }
-
-    @objc func setKey(_ call: CAPPluginCall) {
-        let secretKey = call.getString("secretKey")
-        let publicKey = call.getString("publicKey")
-        if secretKey == nil && publicKey == nil {
-            ENCRYPTION_SECRET_KEY = nil
-            ENCRYPTION_PUBLIC_KEY = nil
-            FNAME_ENCRYPTION_KEY = nil
-            return
-        }
-        guard let secretKey = secretKey, let publicKey = publicKey else {
-            call.reject("both secretKey and publicKey should be provided")
-            return
-        }
-        ENCRYPTION_SECRET_KEY = secretKey
-        ENCRYPTION_PUBLIC_KEY = publicKey
-        FNAME_ENCRYPTION_KEY = AgeEncryption.toRawX25519Key(secretKey)
-
-    }
-
-    @objc func setEnv(_ call: CAPPluginCall) {
-        guard let env = call.getString("env") else {
-            call.reject("required parameter: env")
-            return
-        }
-        self.setKey(call)
-
-        switch env {
-        case "production", "product", "prod":
-            URL_BASE = URL(string: "https://api.logseq.com/file-sync/")!
-            BUCKET = "logseq-file-sync-bucket-prod"
-            REGION = "us-east-1"
-        case "development", "develop", "dev":
-            URL_BASE = URL(string: "https://api-dev.logseq.com/file-sync/")!
-            BUCKET = "logseq-file-sync-bucket"
-            REGION = "us-east-2"
-        default:
-            call.reject("invalid env: \(env)")
-            return
-        }
-
-        self.debugNotification(["event": "setenv:\(env)"])
-        call.resolve(["ok": true])
-    }
-
-    @objc func encryptFnames(_ call: CAPPluginCall) {
-        guard fnameEncryptionEnabled() else {
-            call.reject("fname encryption key not set")
-            return
-        }
-        guard var fnames = call.getArray("filePaths") as? [String] else {
-            call.reject("required parameters: filePaths")
-            return
-        }
-
-        let nFiles = fnames.count
-        fnames = fnames.compactMap { $0.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) }
-        if fnames.count != nFiles {
-            call.reject("cannot encrypt \(nFiles - fnames.count) file names")
-        }
-        call.resolve(["value": fnames])
-    }
-
-    @objc func decryptFnames(_ call: CAPPluginCall) {
-        guard fnameEncryptionEnabled() else {
-            call.reject("fname encryption key not set")
-            return
-        }
-        guard var fnames = call.getArray("filePaths") as? [String] else {
-            call.reject("required parameters: filePaths")
-            return
-        }
-        let nFiles = fnames.count
-        fnames = fnames.compactMap { $0.fnameDecrypt(rawKey: FNAME_ENCRYPTION_KEY!)?.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) }
-        if fnames.count != nFiles {
-            call.reject("cannot decrypt \(nFiles - fnames.count) file names")
-        }
-        call.resolve(["value": fnames])
-    }
-    
-    @objc func encryptWithPassphrase(_ call: CAPPluginCall) {
-        guard let passphrase = call.getString("passphrase"),
-              let content = call.getString("content") else {
-                  call.reject("required parameters: passphrase, content")
-                  return
-              }
-        guard let plaintext = content.data(using: .utf8) else {
-            call.reject("cannot decode ciphertext with utf8")
-            return
-        }
-        call.keepAlive = true
-        DispatchQueue.global(qos: .default).async {
-            if let encrypted = AgeEncryption.encryptWithPassphrase(plaintext, passphrase, armor: true) {
-                call.resolve(["data": String(data: encrypted, encoding: .utf8) as Any])
-            } else {
-                call.reject("cannot encrypt with passphrase")
-            }
-        }
-    }
-    
-    
-    @objc func decryptWithPassphrase(_ call: CAPPluginCall) {
-        guard let passphrase = call.getString("passphrase"),
-              let content = call.getString("content") else {
-                  call.reject("required parameters: passphrase, content")
-                  return
-              }
-        guard let ciphertext = content.data(using: .utf8) else {
-            call.reject("cannot decode ciphertext with utf8")
-            return
-        }
-        call.keepAlive = true
-        DispatchQueue.global(qos: .default).async {
-            if let decrypted = AgeEncryption.decryptWithPassphrase(ciphertext, passphrase) {
-                call.resolve(["data": String(data: decrypted, encoding: .utf8) as Any])
-            } else {
-                call.reject("cannot decrypt with passphrase")
-            }
-        }
-    }
-
-    @objc func getLocalFilesMeta(_ call: CAPPluginCall) {
-        // filePaths are url encoded
-        guard let basePath = call.getString("basePath"),
-              let filePaths = call.getArray("filePaths") as? [String] else {
-                  call.reject("required paremeters: basePath, filePaths")
-                  return
-              }
-        guard let baseURL = URL(string: basePath) else {
-            call.reject("invalid basePath")
-            return
-        }
-
-        var fileMetadataDict: [String: [String: Any]] = [:]
-        for percentFilePath in filePaths {
-            let filePath = percentFilePath.removingPercentEncoding!
-            let url = baseURL.appendingPathComponent(filePath)
-            if let meta = SyncMetadata(of: url) {
-                var metaObj: [String: Any] = ["md5": meta.md5,
-                                              "size": meta.size,
-                                              "mtime": meta.mtime]
-                if fnameEncryptionEnabled() {
-                    metaObj["encryptedFname"] = filePath.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!)
-                }
-
-                fileMetadataDict[percentFilePath] = metaObj
-            }
-        }
-
-        call.resolve(["result": fileMetadataDict])
-    }
-
-    @objc func getLocalAllFilesMeta(_ call: CAPPluginCall) {
-        guard let basePath = call.getString("basePath"),
-              let baseURL = URL(string: basePath) else {
-                  call.reject("invalid basePath")
-                  return
-              }
-
-        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 meta = SyncMetadata(of: fileURL) {
-                        let filePath = fileURL.relativePath(from: baseURL)!
-                        var metaObj: [String: Any] = ["md5": meta.md5,
-                                                      "size": meta.size,
-                                                      "mtime": meta.mtime]
-                        if fnameEncryptionEnabled() {
-                            metaObj["encryptedFname"] = filePath.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!)
-                        }
-                        fileMetadataDict[filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!] = metaObj
-                    }
-                } else if fileURL.isICloudPlaceholder() {
-                    try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
-                }
-            }
-        }
-        call.resolve(["result": fileMetadataDict])
-    }
-
-
-    @objc func renameLocalFile(_ call: CAPPluginCall) {
-        guard let basePath = call.getString("basePath"),
-              let baseURL = URL(string: basePath) else {
-                  call.reject("invalid basePath")
-                  return
-              }
-        guard let from = call.getString("from") else {
-            call.reject("invalid from file")
-            return
-        }
-        guard let to = call.getString("to") else {
-            call.reject("invalid to file")
-            return
-        }
-
-        let fromUrl = baseURL.appendingPathComponent(from.removingPercentEncoding!)
-        let toUrl = baseURL.appendingPathComponent(to.removingPercentEncoding!)
-
-        do {
-            try FileManager.default.moveItem(at: fromUrl, to: toUrl)
-        } catch {
-            call.reject("can not rename file: \(error.localizedDescription)")
-            return
-        }
-        call.resolve(["ok": true])
-
-    }
-
-    @objc func deleteLocalFiles(_ call: CAPPluginCall) {
-        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
-              let filePaths = call.getArray("filePaths") as? [String] else {
-                  call.reject("required paremeters: basePath, filePaths")
-                  return
-              }
-
-        for filePath in filePaths {
-            let fileUrl = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
-            try? FileManager.default.removeItem(at: fileUrl) // ignore any delete errors
-        }
-        call.resolve(["ok": true])
-    }
-
-    /// remote -> local
-    @objc func updateLocalFiles(_ call: CAPPluginCall) {
-        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
-              let filePaths = call.getArray("filePaths") as? [String],
-              let graphUUID = call.getString("graphUUID") ,
-              let token = call.getString("token") else {
-                  call.reject("required paremeters: basePath, filePaths, graphUUID, token")
-                  return
-              }
-
-        // [encrypted-fname: original-fname]
-        var encryptedFilePathDict: [String: String] = [:]
-        if fnameEncryptionEnabled() {
-            for filePath in filePaths {
-                if let encryptedPath = filePath.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) {
-                    encryptedFilePathDict[encryptedPath] = filePath
-                } else {
-                    call.reject("cannot decrypt all file names")
-                }
-            }
-        } else {
-            encryptedFilePathDict = Dictionary(uniqueKeysWithValues: filePaths.map { ($0, $0) })
-        }
-
-        let encryptedFilePaths = Array(encryptedFilePathDict.keys)
-
-        let client = SyncClient(token: token, graphUUID: graphUUID)
-        client.delegate = self // receives notification
-
-        client.getFiles(at: encryptedFilePaths) {  (fileURLs, error) in
-            if let error = error {
-                print("debug getFiles error \(error)")
-                self.debugNotification(["event": "download:error", "data": ["message": "error while getting files \(filePaths)"]])
-                call.reject(error.localizedDescription)
-            } else {
-                // handle multiple completionHandlers
-                let group = DispatchGroup()
-
-                var downloaded: [String] = []
-
-                for (encryptedFilePath, remoteFileURL) in fileURLs {
-                    group.enter()
-
-                    let filePath = encryptedFilePathDict[encryptedFilePath]!
-                    // NOTE: fileURLs from getFiles API is percent-encoded
-                    let localFileURL = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
-                    remoteFileURL.download(toFile: localFileURL) {error in
-                        if let error = error {
-                            self.debugNotification(["event": "download:error", "data": ["message": "error while downloading \(filePath): \(error)"]])
-                            print("debug download \(error) in \(filePath)")
-                        } else {
-                            self.debugNotification(["event": "download:file", "data": ["file": filePath]])
-                            downloaded.append(filePath)
-                        }
-                        group.leave()
-                    }
-                }
-                group.notify(queue: .main) {
-                    self.debugNotification(["event": "download:done"])
-                    call.resolve(["ok": true, "data": downloaded])
-                }
-
-            }
-        }
-    }
-
-    @objc func updateLocalVersionFiles(_ call: CAPPluginCall) {
-        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
-              let filePaths = call.getArray("filePaths") as? [String],
-              let graphUUID = call.getString("graphUUID") ,
-              let token = call.getString("token") else {
-                  call.reject("required paremeters: basePath, filePaths, graphUUID, token")
-                  return
-              }
-        let client = SyncClient(token: token, graphUUID: graphUUID)
-        client.delegate = self // receives notification
-
-        client.getVersionFiles(at: filePaths) {  (fileURLDict, error) in
-            if let error = error {
-                print("debug getVersionFiles error \(error)")
-                self.debugNotification(["event": "version-download:error", "data": ["message": "error while getting version files \(filePaths)"]])
-                call.reject(error.localizedDescription)
-            } else {
-                // handle multiple completionHandlers
-                let group = DispatchGroup()
-
-                var downloaded: [String] = []
-
-                for (filePath, remoteFileURL) in fileURLDict {
-                    group.enter()
-
-                    // NOTE: fileURLs from getFiles API is percent-encoded
-                    let localFileURL = baseURL.appendingPathComponent("logseq/version-files/").appendingPathComponent(filePath.removingPercentEncoding!)
-                    remoteFileURL.download(toFile: localFileURL) {error in
-                        if let error = error {
-                            self.debugNotification(["event": "version-download:error", "data": ["message": "error while downloading \(filePath): \(error)"]])
-                            print("debug download \(error) in \(filePath)")
-                        } else {
-                            self.debugNotification(["event": "version-download:file", "data": ["file": filePath]])
-                            downloaded.append(filePath)
-                        }
-                        group.leave()
-                    }
-                }
-                group.notify(queue: .main) {
-                    self.debugNotification(["event": "version-download:done"])
-                    call.resolve(["ok": true, "data": downloaded])
-                }
-
-            }
-        }
-    }
-
-    // filePaths: Encrypted file paths
-    @objc func deleteRemoteFiles(_ call: CAPPluginCall) {
-        guard var filePaths = call.getArray("filePaths") as? [String],
-              let graphUUID = call.getString("graphUUID"),
-              let token = call.getString("token"),
-              let txid = call.getInt("txid") else {
-                  call.reject("required paremeters: filePaths, graphUUID, token, txid")
-                  return
-              }
-        guard !filePaths.isEmpty else {
-            call.reject("empty filePaths")
-            return
-        }
-
-        let nFiles = filePaths.count
-        if fnameEncryptionEnabled() {
-            filePaths = filePaths.compactMap { $0.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) }
-        }
-        if filePaths.count != nFiles {
-            call.reject("cannot encrypt all file names")
-        }
-
-        let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
-        client.deleteFiles(filePaths) { txid, error in
-            guard error == nil else {
-                call.reject("delete \(error!)")
-                return
-            }
-            guard let txid = txid else {
-                call.reject("missing txid")
-                return
-            }
-            call.resolve(["ok": true, "txid": txid])
-        }
-    }
-
-    /// local -> remote
-    @objc func updateRemoteFiles(_ call: CAPPluginCall) {
-        guard let baseURL = call.getString("basePath").flatMap({path in URL(string: path)}),
-              let filePaths = call.getArray("filePaths") as? [String],
-              let graphUUID = call.getString("graphUUID"),
-              let token = call.getString("token"),
-              let txid = call.getInt("txid") else {
-                  call.reject("required paremeters: basePath, filePaths, graphUUID, token, txid")
-                  return
-              }
-        let fnameEncryption = call.getBool("fnameEncryption") ?? false // default to false
-
-        guard !filePaths.isEmpty else {
-            return call.reject("empty filePaths")
-        }
-
-        let client = SyncClient(token: token, graphUUID: graphUUID, txid: txid)
-        client.delegate = self
-
-        // 1. refresh_temp_credential
-        client.getTempCredential() { (credentials, error) in
-            guard error == nil else {
-                self.debugNotification(["event": "upload:error", "data": ["message": "error while refreshing credential: \(error!)"]])
-                call.reject("error(getTempCredential): \(error!)")
-                return
-            }
-
-            var files: [String: URL] = [:]
-            for filePath in filePaths {
-                // NOTE: filePath from js may contain spaces
-                let fileURL = baseURL.appendingPathComponent(filePath.removingPercentEncoding!)
-                files[filePath] = fileURL
-            }
-
-            // 2. upload_temp_file
-            client.uploadTempFiles(files, credentials: credentials!) { (uploadedFileKeyDict, fileMd5Dict, error) in
-                guard error == nil else {
-                    self.debugNotification(["event": "upload:error", "data": ["message": "error while uploading temp files: \(error!)"]])
-                    call.reject("error(uploadTempFiles): \(error!)")
-                    return
-                }
-                // 3. update_files
-                guard !uploadedFileKeyDict.isEmpty else {
-                    self.debugNotification(["event": "upload:error", "data": ["message": "no file to update"]])
-                    call.reject("no file to update")
-                    return
-                }
-
-                // encrypted-file-name: (file-key, md5)
-                var uploadedFileKeyMd5Dict: [String: [String]] = [:]
-
-                if fnameEncryptionEnabled() && fnameEncryption {
-                    for (filePath, fileKey) in uploadedFileKeyDict {
-                        guard let encryptedFilePath = filePath.removingPercentEncoding!.fnameEncrypt(rawKey: FNAME_ENCRYPTION_KEY!) else {
-                            call.reject("cannot encrypt file name")
-                            return
-                        }
-                        uploadedFileKeyMd5Dict[encryptedFilePath] = [fileKey, fileMd5Dict[filePath]!]
-                    }
-                } else {
-                    for (filePath, fileKey) in uploadedFileKeyDict {
-                        uploadedFileKeyMd5Dict[filePath] = [fileKey, fileMd5Dict[filePath]!]
-                    }
-                }
-                client.updateFiles(uploadedFileKeyMd5Dict) { (txid, error) in
-                    guard error == nil else {
-                        self.debugNotification(["event": "upload:error", "data": ["message": "error while updating files: \(error!)"]])
-                        call.reject("error updateFiles: \(error!)")
-                        return
-                    }
-                    guard let txid = txid else {
-                        call.reject("error: missing txid")
-                        return
-                    }
-                    self.debugNotification(["event": "upload:done", "data": ["files": filePaths, "txid": txid]])
-                    call.resolve(["ok": true, "files": uploadedFileKeyDict, "txid": txid])
-                }
-            }
-        }
-    }
-}

+ 0 - 36
ios/App/App/FileSync/Payload.swift

@@ -1,36 +0,0 @@
-//
-//  Payload.swift
-//  Logseq
-//
-//  Created by Mono Wang on 4/8/R4.
-//
-
-import Foundation
-
-struct GetFilesResponse: Decodable {
-    let PresignedFileUrls: [String: String]
-}
-
-struct DeleteFilesResponse: Decodable {
-    let TXId: Int
-    let DeleteSuccFiles: [String]
-    let DeleteFailedFiles: [String: String]
-}
-
-public struct S3Credential: Decodable {
-    let AccessKeyId: String
-    let Expiration: String
-    let SecretKey: String
-    let SessionToken: String
-}
-
-struct GetTempCredentialResponse: Decodable {
-    let Credentials: S3Credential
-    let S3Prefix: String
-}
-
-struct UpdateFilesResponse: Decodable {
-    let TXId: Int
-    let UpdateSuccFiles: [String]
-    let UpdateFailedFiles: [String: String]
-}

+ 0 - 381
ios/App/App/FileSync/SyncClient.swift

@@ -1,381 +0,0 @@
-//
-//  SyncClient.swift
-//  Logseq
-//
-//  Created by Mono Wang on 4/8/R4.
-//
-
-import Foundation
-import AWSMobileClient
-import AWSS3
-
-public protocol SyncDebugDelegate {
-    func debugNotification(_ message: [String: Any])
-}
-
-
-public class SyncClient {
-    private var token: String
-    private var graphUUID: String?
-    private var txid: Int = 0
-    private var s3prefix: String?
-    
-    public var delegate: SyncDebugDelegate? = nil
-    
-    public init(token: String) {
-        self.token = token
-    }
-    
-    public init(token: String, graphUUID: String) {
-        self.token = token
-        self.graphUUID = graphUUID
-    }
-    
-    public init(token: String, graphUUID: String, txid: Int) {
-        self.token = token
-        self.graphUUID = graphUUID
-        self.txid = txid
-    }
-
-    // get_files
-    // => file_path, file_url
-    public func getFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("get_files")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        
-        let payload = [
-            "GraphUUID": self.graphUUID ?? "",
-            "Files": filePaths
-        ] as [String : Any]
-        let bodyData = try? JSONSerialization.data(
-            withJSONObject: payload,
-            options: []
-        )
-        request.httpMethod = "POST"
-        request.httpBody = bodyData
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler([:], error)
-                return
-            }
-            
-            if (response as? HTTPURLResponse)?.statusCode != 200 {
-                let body = String(data: data!, encoding: .utf8) ?? "";
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
-                return
-            }
-            
-            if let data = data {
-                let resp = try? JSONDecoder().decode([String:[String:String]].self, from: data)
-                let files = resp?["PresignedFileUrls"] ?? [:]
-                self.delegate?.debugNotification(["event": "download:prepare"])
-                completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
-            } else {
-                // Handle unexpected error
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    public func getVersionFiles(at filePaths: [String], completionHandler: @escaping ([String: URL], Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("get_version_files")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        
-        let payload = [
-            "GraphUUID": self.graphUUID ?? "",
-            "Files": filePaths
-        ] as [String : Any]
-        let bodyData = try? JSONSerialization.data(
-            withJSONObject: payload,
-            options: []
-        )
-        request.httpMethod = "POST"
-        request.httpBody = bodyData
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler([:], error)
-                return
-            }
-            
-            if (response as? HTTPURLResponse)?.statusCode != 200 {
-                let body = String(data: data!, encoding: .utf8) ?? "";
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "http error \(body)"]))
-                return
-            }
-            
-            if let data = data {
-                let resp = try? JSONDecoder().decode([String:[String:String]].self, from: data)
-                let files = resp?["PresignedFileUrls"] ?? [:]
-                self.delegate?.debugNotification(["event": "version-download:prepare"])
-                completionHandler(files.mapValues({ url in URL(string: url)!}), nil)
-            } else {
-                // Handle unexpected error
-                completionHandler([:], NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    
-    public func deleteFiles(_ filePaths: [String], completionHandler: @escaping  (Int?, Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("delete_files")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        
-        let payload = [
-            "GraphUUID": self.graphUUID ?? "",
-            "Files": filePaths,
-            "TXId": self.txid,
-        ] as [String : Any]
-        let bodyData = try? JSONSerialization.data(
-            withJSONObject: payload,
-            options: []
-        )
-        request.httpMethod = "POST"
-        request.httpBody = bodyData
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler(nil, error)
-                return
-            }
-            
-            if let response = response as? HTTPURLResponse {
-                let body = String(data: data!, encoding: .utf8) ?? ""
-                
-                if response.statusCode == 409 {
-                    if body.contains("txid_to_validate") {
-                        completionHandler(nil, NSError(domain: "",
-                                                       code: 409,
-                                                       userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
-                        return
-                    }
-                    // fallthrough
-                }
-                if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
-                                                   code: response.statusCode,
-                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
-                    return
-                }
-            }
-            
-            if let data = data {
-                do {
-                    let resp = try JSONDecoder().decode(DeleteFilesResponse.self, from: data)
-                    // TODO: handle api resp?
-                    self.delegate?.debugNotification(["event": "delete"])
-                    completionHandler(resp.TXId, nil)
-                } catch {
-                    completionHandler(nil, error)
-                }
-            } else {
-                // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    // (txid, error)
-    // filePath => [S3Key, md5]
-    public func updateFiles(_ fileKeyDict: [String: [String]], completionHandler: @escaping  (Int?, Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("update_files")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        
-        let payload = [
-            "GraphUUID": self.graphUUID ?? "",
-            "Files": Dictionary(uniqueKeysWithValues: fileKeyDict.map { ($0, $1) }) as [String: [String]] as Any,
-            "TXId": self.txid,
-        ] as [String : Any]
-        let bodyData = try? JSONSerialization.data(
-            withJSONObject: payload,
-            options: []
-        )
-        request.httpMethod = "POST"
-        request.httpBody = bodyData
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler(nil, error)
-                return
-            }
-            
-            if let response = response as? HTTPURLResponse {
-                let body = String(data: data!, encoding: .utf8) ?? ""
-                
-                if response.statusCode == 409 {
-                    if body.contains("txid_to_validate") {
-                        completionHandler(nil, NSError(domain: "",
-                                                       code: 409,
-                                                       userInfo: [NSLocalizedDescriptionKey: "invalid txid: \(body)"]))
-                        return
-                    }
-                    // fallthrough
-                }
-                if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
-                                                   code: response.statusCode,
-                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
-                    return
-                }
-            }
-            
-            if let data = data {
-                let resp = try? JSONDecoder().decode(UpdateFilesResponse.self, from: data)
-                if resp?.UpdateFailedFiles.isEmpty ?? true {
-                    completionHandler(resp?.TXId, nil)
-                } else {
-                    completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "update fail for some files: \(resp?.UpdateFailedFiles.debugDescription)"]))
-                }
-            } else {
-                // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    public func getTempCredential(completionHandler: @escaping (S3Credential?, Error?) -> Void) {
-        let url = URL_BASE.appendingPathComponent("get_temp_credential")
-        
-        var request = URLRequest(url: url)
-        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
-        request.setValue("Logseq-sync/0.1", forHTTPHeaderField: "User-Agent")
-        request.setValue("Bearer \(self.token)", forHTTPHeaderField: "Authorization")
-        request.httpMethod = "POST"
-        request.httpBody = Data()
-        
-        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
-            guard error == nil else {
-                completionHandler(nil, error)
-                return
-            }
-            if let response = response as? HTTPURLResponse {
-                let body = String(data: data!, encoding: .utf8) ?? ""
-                if response.statusCode == 401 {
-                    completionHandler(nil, NSError(domain: "", code: 401, userInfo: [NSLocalizedDescriptionKey: "unauthorized"]))
-                    return
-                }
-                if response.statusCode != 200 {
-                    completionHandler(nil, NSError(domain: "",
-                                                   code: response.statusCode,
-                                                   userInfo: [NSLocalizedDescriptionKey: "invalid http status \(response.statusCode): \(body)"]))
-                    return
-                }
-            }
-            if let data = data {
-                let resp = try? JSONDecoder().decode(GetTempCredentialResponse.self, from: data)
-                // NOTE: remove BUCKET prefix here.
-                self.s3prefix = resp?.S3Prefix.replacingOccurrences(of: "\(BUCKET)/", with: "")
-                self.delegate?.debugNotification(["event": "upload:prepare"])
-                completionHandler(resp?.Credentials, nil)
-            } else {
-                // Handle unexpected error
-                completionHandler(nil, NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "unexpected error"]))
-            }
-        }
-        task.resume()
-    }
-    
-    // [filePath, Key]
-    public func uploadTempFiles(_ files: [String: URL], credentials: S3Credential, completionHandler: @escaping ([String: String], [String: String], Error?) -> Void) {
-        let credentialsProvider = AWSBasicSessionCredentialsProvider(
-            accessKey: credentials.AccessKeyId, secretKey: credentials.SecretKey, sessionToken: credentials.SessionToken)
-        var region = AWSRegionType.USEast2
-        if REGION == "us-east-2" {
-            region = .USEast2
-        } else if REGION == "us-east-1" {
-            region = .USEast1
-        } // TODO: string to REGION conversion
-        
-        let configuration = AWSServiceConfiguration(region: region, credentialsProvider: credentialsProvider)
-        configuration?.timeoutIntervalForRequest = 5.0
-        configuration?.timeoutIntervalForResource = 5.0
-        
-        let tuConf = AWSS3TransferUtilityConfiguration()
-        tuConf.bucket = BUCKET
-        //x tuConf.isAccelerateModeEnabled = true
-        
-        let transferKey = String.random(length: 10)
-        AWSS3TransferUtility.register(
-            with: configuration!,
-            transferUtilityConfiguration: tuConf,
-            forKey: transferKey
-        ) { (error) in
-            if let error = error {
-                print("error while register tu \(error)")
-            }
-        }
-        
-        let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: transferKey)
-        let uploadExpression = AWSS3TransferUtilityUploadExpression()
-        
-        let group = DispatchGroup()
-        var keyFileDict: [String: String] = [:]
-        var fileKeyDict: [String: String] = [:]
-        var fileMd5Dict: [String: String] = [:]
-        
-        let uploadCompletionHandler = { (task: AWSS3TransferUtilityUploadTask, error: Error?) -> Void in
-            // ignore any errors in first level of handler
-            if let error = error {
-                self.delegate?.debugNotification(["event": "upload:error", "data": ["key": task.key, "error": error.localizedDescription]])
-            }
-            if let HTTPResponse = task.response {
-                if HTTPResponse.statusCode != 200 || task.status != .completed {
-                    print("debug uploading error \(HTTPResponse)")
-                }
-            }
-            
-            // only save successful keys
-            let filePath = keyFileDict[task.key]!
-            fileKeyDict[filePath] = task.key
-            keyFileDict.removeValue(forKey: task.key)
-            self.delegate?.debugNotification(["event": "upload:file", "data": ["file": filePath, "key": task.key]])
-            group.leave() // notify finish upload
-        }
-        
-        for (filePath, fileLocalURL) in files {
-            print("debug, upload temp \(fileLocalURL) \(filePath)")
-            guard let rawData = try? Data(contentsOf: fileLocalURL) else { continue }
-            guard let encryptedRawDat = maybeEncrypt(rawData) else { continue }
-            group.enter()
-            
-            let randFileName = String.random(length: 15).appending(".").appending(fileLocalURL.pathExtension)
-            let key = "\(self.s3prefix!)/ios\(randFileName)"
-
-            keyFileDict[key] = filePath
-            fileMd5Dict[filePath] = rawData.MD5
-            transferUtility?.uploadData(encryptedRawDat, key: key, contentType: "application/octet-stream", expression: uploadExpression, completionHandler: uploadCompletionHandler)
-                .continueWith(block: { (task) in
-                    if let error = task.error {
-                        completionHandler([:], [:], error)
-                    }
-                    return nil
-                })
-        }
-        
-        group.notify(queue: .main) {
-            AWSS3TransferUtility.remove(forKey: transferKey)
-            completionHandler(fileKeyDict, fileMd5Dict, nil)
-        }
-    }
-}

+ 0 - 5
ios/App/App/Info.plist

@@ -50,11 +50,6 @@
 	<true/>
 	<key>LSSupportsOpeningDocumentsInPlace</key>
 	<true/>
-	<key>NSAppTransportSecurity</key>
-	<dict>
-		<key>NSAllowsArbitraryLoads</key>
-		<true/>
-	</dict>
 	<key>NSCameraUsageDescription</key>
 	<string>We will access your camera when you take a photo, and embed it in your note.</string>
 	<key>NSDocumentsFolderUsageDescription</key>

+ 12 - 4
ios/App/Podfile

@@ -1,4 +1,9 @@
-platform :ios, '12.0'
+require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
+
+# pod specs for AgeEncryption
+source 'https://github.com/CocoaPods/Specs.git'
+
+platform :ios, '13.0'
 use_frameworks!
 
 # workaround to avoid Xcode caching of Pods that requires
@@ -18,6 +23,7 @@ def capacitor_pods
   pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
   pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
+  pod 'LogseqCapacitorFileSync', :path => '../../node_modules/@logseq/capacitor-file-sync'
   pod 'CapacitorVoiceRecorder', :path => '../../node_modules/capacitor-voice-recorder'
   pod 'SendIntent', :path => '../../node_modules/send-intent'
 end
@@ -25,7 +31,9 @@ end
 target 'Logseq' do
   capacitor_pods
   # Add your Pods here
-  pod 'AWSMobileClient'
-  pod 'AWSS3'
-  pod 'AgeEncryption', :podspec => './LogseqSpecs/AgeEncryption.podspec'
+end
+
+
+post_install do |installer|
+  assertDeploymentTarget(installer)
 end

+ 1 - 1
libs/src/LSPlugin.ts

@@ -386,7 +386,7 @@ export interface IAppProxy {
   queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>
 
   /**
-   * @deprecated
+   * @deprecated Use `logseq.UI.showMsg` instead
    * @param content
    * @param status
    */

+ 14 - 13
package.json

@@ -5,7 +5,7 @@
     "main": "static/electron.js",
     "devDependencies": {
         "@axe-core/playwright": "^4.4.4",
-        "@capacitor/cli": "3.2.2",
+        "@capacitor/cli": "^4.0.0",
         "@playwright/test": "^1.24.2",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
@@ -69,20 +69,21 @@
         "android:dev": "cross-env PLATFORM=android gulp cap"
     },
     "dependencies": {
-        "@capacitor/android": "3.2.2",
-        "@capacitor/app": "1.0.6",
-        "@capacitor/camera": "1.2.1",
-        "@capacitor/clipboard": "^1.0.8",
-        "@capacitor/core": "3.2.2",
-        "@capacitor/filesystem": "1.0.6",
-        "@capacitor/haptics": "^1.1.4",
-        "@capacitor/ios": "3.2.2",
-        "@capacitor/keyboard": "^1.2.0",
-        "@capacitor/share": "^1.1.2",
-        "@capacitor/splash-screen": "1.1.3",
-        "@capacitor/status-bar": "1.0.6",
+        "@capacitor/android": "^4.0.0",
+        "@capacitor/app": "^4.0.0",
+        "@capacitor/camera": "^4.0.0",
+        "@capacitor/clipboard": "^4.0.0",
+        "@capacitor/core": "^4.0.0",
+        "@capacitor/filesystem": "^4.0.0",
+        "@capacitor/haptics": "^4.0.0",
+        "@capacitor/ios": "^4.0.0",
+        "@capacitor/keyboard": "^4.0.0",
+        "@capacitor/share": "^4.0.0",
+        "@capacitor/splash-screen": "^4.0.0",
+        "@capacitor/status-bar": "^4.0.0",
         "@excalidraw/excalidraw": "0.10.0",
         "@kanru/rage-wasm": "0.2.1",
+        "@logseq/capacitor-file-sync": "0.0.6",
         "@logseq/react-tweet-embed": "1.3.1-1",
         "@sentry/react": "^6.18.2",
         "@sentry/tracing": "^6.18.2",

+ 1 - 5
resources/css/common.css

@@ -141,7 +141,7 @@ html[data-theme='light'] {
   --ls-scrollbar-thumb-hover-color: rgba(0, 0, 0, 0.2);
   --ls-head-text-color: var(--ls-link-text-color);
   --ls-cloze-text-color: #0000cd;
-  --ls-icon-color: #908e8b;
+  --ls-icon-color: #646464;
   --ls-search-icon-color: var(--ls-icon-color);
   --ls-a-chosen-bg: #f7f7f7;
   --ls-right-sidebar-code-bg-color: var(--ls-secondary-background-color);
@@ -174,10 +174,6 @@ body {
   word-break: break-word; /* compatible for overflow-wrap: anywhere */
 }
 
-a svg {
-  color: var(--ls-icon-color);
-}
-
 svg {
   pointer-events: none;
 }

+ 1 - 0
resources/electron.html

@@ -52,6 +52,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/interact.min.js"></script>
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/main.js"></script>
+<script defer src="./js/tabler.min.js"></script>
 <script defer src="./js/code-editor.js"></script>
 <script defer src="./js/age-encryption.js"></script>
 <script defer src="./js/excalidraw.js"></script>

+ 1 - 0
resources/index.html

@@ -51,6 +51,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/interact.min.js"></script>
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/main.js"></script>
+<script defer src="./js/tabler.min.js"></script>
 <script defer src="./js/code-editor.js"></script>
 <script defer src="./js/age-encryption.js"></script>
 <script defer src="./js/excalidraw.js"></script>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
resources/js/tabler.min.js


+ 2 - 2
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.8.6",
+  "version": "0.8.7",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",
@@ -37,7 +37,7 @@
     "https-proxy-agent": "5.0.0",
     "@sentry/electron": "2.5.1",
     "posthog-js": "1.10.2",
-    "@logseq/rsapi": "0.0.38",
+    "@logseq/rsapi": "0.0.44",
     "electron-deeplink": "1.0.10",
     "abort-controller": "3.0.0"
   },

+ 1 - 1
shadow-cljs.edn

@@ -32,7 +32,7 @@
                                                 :redef false}}
         :closure-defines  {goog.debug.LOGGING_ENABLED       true
                            frontend.config/ENABLE-PLUGINS   #shadow/env ["ENABLE_PLUGINS"   :as :bool :default true]
-                           frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default false]}
+                           frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true]}
 
         ;; NOTE: electron, browser/mobile-app use different asset-paths.
         ;;   For browser/mobile-app devs, assets are located in /static/js(via HTTP root).

+ 1 - 0
src/electron/electron/core.cljs

@@ -317,6 +317,7 @@
                                   (when @*quit-dirty? ;; when not updating
                                     (.preventDefault e)
                                     (let [web-contents (. win -webContents)]
+                                      (.send web-contents "persist-zoom-level" (.getZoomLevel web-contents))
                                       (.send web-contents "persistent-dbs"))
                                     (async/go
                                       (let [_ (async/<! state/persistent-dbs-chan)]

+ 6 - 9
src/electron/electron/file_sync_rsapi.cljs

@@ -3,8 +3,8 @@
 
 (defn key-gen [] (rsapi/keygen))
 
-(defn set-env [env private-key public-key]
-  (rsapi/setEnv env private-key public-key))
+(defn set-env [graph-uuid env private-key public-key]
+  (rsapi/setEnv graph-uuid env private-key public-key))
 
 (defn get-local-files-meta [graph-uuid base-path file-paths]
   (rsapi/getLocalFilesMeta graph-uuid base-path (clj->js file-paths)))
@@ -27,17 +27,14 @@
 (defn delete-remote-files [graph-uuid base-path file-paths txid token]
   (rsapi/deleteRemoteFiles graph-uuid base-path (clj->js file-paths) txid token))
 
-(defn update-remote-file [graph-uuid base-path file-path txid token]
-  (rsapi/updateRemoteFile graph-uuid base-path file-path txid token))
-
 (defn update-remote-files [graph-uuid base-path file-paths txid token]
   (rsapi/updateRemoteFiles graph-uuid base-path (clj->js file-paths) txid token true))
 
-(defn encrypt-fnames [fnames]
-  (mapv rsapi/encryptFname fnames))
+(defn encrypt-fnames [graph-uuid fnames]
+  (rsapi/encryptFnames graph-uuid (clj->js fnames)))
 
-(defn decrypt-fnames [fnames]
-  (mapv rsapi/decryptFname fnames))
+(defn decrypt-fnames [graph-uuid fnames]
+  (rsapi/decryptFnames graph-uuid (clj->js fnames)))
 
 (defn encrypt-with-passphrase [passphrase data]
   (rsapi/ageEncryptWithPassphrase passphrase data))

+ 0 - 3
src/electron/electron/handler.cljs

@@ -564,9 +564,6 @@
 (defmethod handle :delete-remote-files [_ args]
   (apply rsapi/delete-remote-files (rest args)))
 
-(defmethod handle :update-remote-file [_ args]
-  (apply rsapi/update-remote-file (rest args)))
-
 (defmethod handle :update-remote-files [_ args]
   (apply rsapi/update-remote-files (rest args)))
 

+ 1 - 1
src/electron/electron/window.cljs

@@ -146,7 +146,7 @@
         (.on "new-window" new-win-handler)
         (.on "will-navigate" will-navigate-handler)
         (.on "did-start-navigation" #(.send web-contents "persist-zoom-level" (.getZoomLevel web-contents)))
-        (.on "did-navigate-in-page" #(.send web-contents "restore-zoom-level")))
+        (.on "page-title-updated" #(.send web-contents "restore-zoom-level")))
 
       (doto win
         (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))

+ 54 - 54
src/main/frontend/components/block.css

@@ -177,8 +177,9 @@
   min-width: 22px;
   min-height: 22px;
   padding: 2px;
-  color: initial;
+  color: var(--ls-secondary-text-color);
   user-select: none;
+  opacity: .4;
 
   .control-hide {
     display: none;
@@ -186,40 +187,40 @@
 }
 
 .block-left-menu {
-    background-color: var(--ls-secondary-background-color);
-    background: linear-gradient(90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%);
-
-    .commands-button {
-        overflow: hidden;
-        max-width: 40px;
-        text-align: center;
-        margin: auto 0;
-
-        .indent {
-            opacity: 30%;
-        }
+  background-color: var(--ls-secondary-background-color);
+  background: linear-gradient(90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%);
+
+  .commands-button {
+    overflow: hidden;
+    max-width: 40px;
+    text-align: center;
+    margin: auto 0;
+
+    .indent {
+      opacity: 30%;
     }
+  }
 }
 
 .block-right-menu {
-    background-color: var(--ls-secondary-background-color);
-    /* background: linear-gradient(-90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%); */
-
-    .commands-button {
-        overflow: hidden;
-        text-align: center;
-        margin: auto 0;
-
-        .outdent {
-            margin: 0 12px;
-            opacity: 30%;
-        }
-
-        .more {
-            margin: 0 12px;
-            opacity: 30%;
-        }
+  background-color: var(--ls-secondary-background-color);
+  /* background: linear-gradient(-90deg, var(--ls-primary-background-color) 0%, var(--ls-secondary-background-color) 100%); */
+
+  .commands-button {
+    overflow: hidden;
+    text-align: center;
+    margin: auto 0;
+
+    .outdent {
+      margin: 0 12px;
+      opacity: 30%;
+    }
+
+    .more {
+      margin: 0 12px;
+      opacity: 30%;
     }
+  }
 }
 
 .block-ref {
@@ -279,16 +280,16 @@
   }
 
   &.is-doc {
-      &:before {
-          content: "[[📜";
-          opacity: .7;
-          margin-right: 4px;
-      }
+    &:before {
+      content: "[[📜";
+      opacity: .7;
+      margin-right: 4px;
+    }
 
-      &:after {
-          content: "]]";
-          opacity: .7;
-      }
+    &:after {
+      content: "]]";
+      opacity: .7;
+    }
   }
 }
 
@@ -309,8 +310,7 @@
 
   color: #fff;
 
-  a,
-  .page-reference:not(:hover), {
+  a, .page-reference:not(:hover) {
     color: #aacece;
 
     .bracket {
@@ -633,7 +633,7 @@ a.cloze-revealed {
 }
 
 .page-property-key:hover {
-    background-color: var(--ls-selection-background-color);
+  background-color: var(--ls-selection-background-color);
 }
 
 .block-parents a {
@@ -651,24 +651,24 @@ a.cloze-revealed {
 }
 
 html.is-native-ios {
-    audio {
-        width: 100%;
-        max-width: 350px;
-    }
+  audio {
+    width: 100%;
+    max-width: 350px;
+  }
 }
 
 html:not(.is-mac) {
-    body[data-active-keystroke="Control"] {
-        .block-content {
-            cursor: pointer;
-        }
+  body[data-active-keystroke="Control"] {
+    .block-content {
+      cursor: pointer;
     }
+  }
 }
 
 html.is-mac {
-    body[data-active-keystroke="Meta"] {
-        .block-content {
-            cursor: pointer;
-        }
+  body[data-active-keystroke="Meta"] {
+    .block-content {
+      cursor: pointer;
     }
+  }
 }

+ 24 - 15
src/main/frontend/components/command_palette.css

@@ -34,6 +34,15 @@
     .input-wrap {
       height: var(--palettle-input-height);
     }
+
+    .recent-search {
+      > .wrap {
+        > div:last-child {
+          display: flex !important;
+          justify-items: center;
+        }
+      }
+    }
   }
 
   &-input {
@@ -50,25 +59,25 @@
 }
 
 html.is-ios {
-    .cp__palette-main {
-        margin-bottom: 0px;
-    }
+  .cp__palette-main {
+    margin-bottom: 0;
+  }
 }
 
 .cards-review .cp__select {
-    &-main {
-        margin: 0;
-        @screen lg {
-            width: 240px;
-        }
+  &-main {
+    margin: 0;
+    @screen lg {
+      width: 240px;
     }
+  }
 
-    .input-wrap {
-        height: initial;
-    }
+  .input-wrap {
+    height: initial;
+  }
 
-    &-input {
-        padding: 16px;
-        font-size: 16px;
-    }
+  &-input {
+    padding: 16px;
+    font-size: 16px;
+  }
 }

+ 9 - 9
src/main/frontend/components/encryption.cljs

@@ -141,7 +141,7 @@
 
     [:div.encryption-password.max-w-2xl.-mb-2
      [:div.cp__file-sync-related-normal-modal
-      [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "lock-access")]]
+      [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "lock-access" {:size 28})]]
 
       [:div.mt-3.text-center.sm:mt-0.sm:text-left
        [:h1#modal-headline.text-2xl.font-bold.text-center
@@ -160,7 +160,7 @@
          [:div.folder-tip.flex.flex-col.items-center
           [:h3
            [:span.flex.space-x-2.leading-none.pb-1
-            (ui/icon "cloud-lock")
+            (ui/icon "cloud-lock" {:size 20})
             [:span GraphName]
             [:span.scale-75 (ui/icon "arrow-right")]
             [:span (ui/icon "folder")]]]
@@ -169,10 +169,10 @@
          [:div.input-hints.text-sm.py-2.px-3.rounded.mb-2.mt-2.flex.items-center
           (if-let [display-str (:fail set-remote-graph-pwd-result)]
             [:<>
-             [:span.scale-125.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
+             [:span.flex.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
              [:span.text-red-600 display-str]]
             [:<>
-             [:span.scale-125.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})]
+             [:span.flex.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})]
              [:span "Please enter the password for this graph to continue syncing."]])]])
 
       ;; secure this remote graph
@@ -186,9 +186,9 @@
                     (not (string/blank? @*pw-confirm)))
               (if (or (not (pattern-ok?))
                       (not= @*password @*pw-confirm))
-                [:span.scale-125.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
-                [:span.scale-125.pr-1.text-green-600 (ui/icon "circle-check" {:class "text-md mr-1"})])
-              [:span.scale-125.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})])
+                [:span.flex.pr-1.text-red-600 (ui/icon "alert-circle" {:class "text-md mr-1"})]
+                [:span.flex.pr-1.text-green-600 (ui/icon "circle-check" {:class "text-md mr-1"})])
+              [:span.flex.pr-1 (ui/icon "bulb" {:class "text-md mr-1"})])
 
             (if (not (string/blank? @*password))
               (if-not (pattern-ok?)
@@ -252,7 +252,7 @@
       (when init-graph-keys
         [:div.init-remote-pw-tips.space-x-4.pt-2.hidden.sm:flex
          [:div.flex-1.flex.items-center
-          [:span.px-3.scale-125 (ui/icon "key")]
+          [:span.px-3.flex (ui/icon "key")]
           [:p.dark:text-gray-100
            [:span "Please make sure you "]
            "remember the password you have set, "
@@ -261,7 +261,7 @@
            [:span "of the password."]]]
 
          [:div.flex-1.flex.items-center
-          [:span.px-3.scale-125 (ui/icon "lock")]
+          [:span.px-3.flex (ui/icon "lock")]
           [:p.dark:text-gray-100
            "If you lose your password, all of your data in the cloud can’t be decrypted. "
            [:span "You will still be able to access the local version of your graph."]]]])]

+ 44 - 48
src/main/frontend/components/file_sync.cljs

@@ -142,7 +142,7 @@
                                      (state/get-repos))))))))]
 
     [:div.cp__file-sync-related-normal-modal
-     [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-upload")]]
+     [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-upload" {:size 20})]]
 
      [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
       "Are you sure you want to create a new remote graph?"]
@@ -250,12 +250,11 @@
                                              :queuing queuing?
                                              :idle    (and (not queuing?) idle?)}])}
                [:span.flex.items-center
-                (ui/icon "cloud"
-                         {:style {:fontSize ui/icon-size}})]]
+                (ui/icon "cloud" {:size ui/icon-size})]]
 
               [:a.button.cloud.off
                {:on-click turn-on}
-               (ui/icon "cloud-off" {:style {:fontSize ui/icon-size}})]))
+               (ui/icon "cloud-off" {:size ui/icon-size})]))
 
           (cond-> []
             synced-file-graph?
@@ -265,8 +264,9 @@
                         [:span.opacity-60 "Everything is synced!"]]
                  :as-link? false}]
                (if need-password?
-                 [{:title   [:div.file-item
-                             (ui/icon "lock") "Password is required"]
+                 [{:title   [:div.file-item.flex
+                             (ui/icon "lock")
+                             [:span.pl-1 "Password is required"]]
                    :options {:on-click fs-sync/sync-need-password!}}]
                  [{:title   [:div.file-item.is-first ""]
                    :options {:class "is-first-placeholder"}}]))
@@ -323,12 +323,6 @@
             ]}))])))
 
 (rum/defc pick-local-graph-for-sync [graph]
-  (rum/use-effect!
-   (fn []
-     (file-sync-handler/set-wait-syncing-graph graph)
-     #(file-sync-handler/set-wait-syncing-graph nil))
-   [graph])
-
   [:div.cp__file-sync-related-normal-modal
    [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-download")]]
 
@@ -346,41 +340,43 @@
 
    [:div.-mt-1
     (ui/button
-      (str "Open a local directory")
+      "Open a local directory"
       :class "w-full rounded-t-none py-4"
-      :on-click #(->
-                  (page-handler/ls-dir-files!
-                   (fn [{:keys [url]}]
-                     (file-sync-handler/init-remote-graph url)
-                     ;; TODO: wait for switch done
-                     (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))
-
-                   {:empty-dir?-or-pred
-                    (fn [ret]
-                      (let [empty-dir? (nil? (second ret))]
-                        (if-let [root (first ret)]
-
-                          ;; verify directory
-                          (-> (if empty-dir?
-                                (p/resolved nil)
-                                (if (util/electron?)
-                                  (ipc/ipc :readGraphTxIdInfo root)
-                                  (fs-util/read-graphs-txid-info root)))
-
-                              (p/then (fn [^js info]
-                                        (when (and (not empty-dir?)
-                                                   (or (nil? info)
-                                                       (nil? (second info))
-                                                       (not= (second info) (:GraphUUID graph))))
-                                          (if (js/confirm "This directory is not empty, are you sure to sync the remote graph to it? Make sure to back up the directory first.")
-                                            (do
-                                              (state/set-state! :graph/remote-binding? true)
-                                              (p/resolved nil))
-                                            (throw (js/Error. nil)))))))
-
-                          ;; cancel pick a directory
-                          (throw (js/Error. nil)))))})
-                  (p/catch (fn []))))
+      :on-click #(do
+                   (state/close-modal!)
+                   (fs-sync/<sync-stop)
+                   (->
+                    (page-handler/ls-dir-files!
+                     (fn [{:keys [url]}]
+                       (file-sync-handler/init-remote-graph url graph)
+                       (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))
+
+                     {:empty-dir?-or-pred
+                      (fn [ret]
+                        (let [empty-dir? (nil? (second ret))]
+                          (if-let [root (first ret)]
+
+                            ;; verify directory
+                            (-> (if empty-dir?
+                                  (p/resolved nil)
+                                  (if (util/electron?)
+                                    (ipc/ipc :readGraphTxIdInfo root)
+                                    (fs-util/read-graphs-txid-info root)))
+
+                                (p/then (fn [^js info]
+                                          (when (and (not empty-dir?)
+                                                     (or (nil? info)
+                                                         (nil? (second info))
+                                                         (not= (second info) (:GraphUUID graph))))
+                                            (if (js/confirm "This directory is not empty, are you sure to sync the remote graph to it? Make sure to back up the directory first.")
+                                              (do
+                                                (state/set-state! :graph/remote-binding? true)
+                                                (p/resolved nil))
+                                              (throw (js/Error. nil)))))))
+
+                            ;; cancel pick a directory
+                            (throw (js/Error. nil)))))})
+                    (p/catch (fn [])))))
     [:p.text-xs.opacity-50.px-1 (ui/icon "alert-circle") " An empty directory or an existing remote graph!"]]])
 
 (defn pick-dest-to-sync-panel [graph]
@@ -593,7 +589,7 @@
   [close-fn]
 
   [:div.cp__file-sync-related-normal-modal
-   [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "checkup-list")]]
+   [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "checkup-list" {:size 28})]]
 
    [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
     [:span.dark:opacity-80 "Congrats on your first successful sync!"]]
@@ -608,7 +604,7 @@
 
    [:div.cloud-tip.rounded-md.mt-6.py-4
     [:div.items-center.opacity-90.flex.justify-center
-     [:span.pr-2 (ui/icon "bell-ringing" {:class "font-semibold"})]
+     [:span.pr-2.flex (ui/icon "bell-ringing" {:class "font-semibold"})]
      [:strong "Logseq Sync is still in Beta and we're working on a Pro plan!"]]
 
     ;; [:ul.flex.py-6.px-4

+ 8 - 8
src/main/frontend/components/header.cljs

@@ -31,7 +31,7 @@
                    (when (mobile-util/native-iphone?)
                      (state/set-left-sidebar-open! false))
                    (route-handler/redirect-to-home!))}
-     (ui/icon "home" {:style {:fontSize ui/icon-size}})]))
+     (ui/icon "home" {:size ui/icon-size})]))
 
 (rum/defc login < rum/reactive
   < {:key-fn #(identity "login-button")}
@@ -55,7 +55,7 @@
     [:button.#left-menu.cp__header-left-menu.button.icon
      {:title "Toggle left menu"
       :on-click on-click}
-     (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]))
+     (ui/icon "menu-2" {:size ui/icon-size})]))
 
 (rum/defc dropdown-menu < rum/reactive
   < {:key-fn #(identity "repos-dropdown-menu")}
@@ -68,7 +68,7 @@
        [:button.button.icon.toolbar-dots-btn
         {:on-click toggle-fn
          :title "More"}
-        (ui/icon "dots" {:style {:fontSize ui/icon-size}})])
+        (ui/icon "dots" {:size ui/icon-size})])
      (->>
       [(when (state/enable-editing?)
          {:title (t :settings)
@@ -118,12 +118,12 @@
    (ui/with-shortcut :go/backward "bottom"
      [:button.it.navigation.nav-left.button.icon
       {:title "Go back" :on-click #(js/window.history.back)}
-      (ui/icon "arrow-left" {:style {:fontSize ui/icon-size}})])
+      (ui/icon "arrow-left" {:size ui/icon-size})])
 
    (ui/with-shortcut :go/forward "bottom"
      [:button.it.navigation.nav-right.button.icon
       {:title "Go forward" :on-click #(js/window.history.forward)}
-      (ui/icon "arrow-right" {:style {:fontSize ui/icon-size}})])])
+      (ui/icon "arrow-right" {:size ui/icon-size})])])
 
 (rum/defc updater-tips-new-version
   [t]
@@ -186,14 +186,14 @@
                                         (mobile-util/native-iphone?))
                                 (state/set-left-sidebar-open! false))
                               (state/pub-event! [:go/search]))}
-              (ui/icon "search" {:style {:fontSize ui/icon-size}})]))])
+              (ui/icon "search" {:size ui/icon-size})]))])
       (when (mobile-util/native-platform?)
         (if (or (state/home?) custom-home-page?)
           left-menu
           (ui/with-shortcut :go/backward "bottom"
-            [:button.it.navigation.nav-left.button.icon
+            [:button.it.navigation.nav-left.button.icon.opacity-70
              {:title "Go back" :on-click #(js/window.history.back)}
-             (ui/icon "chevron-left" {:style {:fontSize 25}})])))]
+             (ui/icon "chevron-left" {:size 26})])))]
 
      [:div.r.flex
       (when (and sync-enabled?

+ 10 - 10
src/main/frontend/components/header.css

@@ -1,5 +1,6 @@
 .cp__header {
   @apply z-10;
+
   -webkit-app-region: drag;
 
   padding-top: var(--ls-headbar-inner-top-padding);
@@ -17,7 +18,8 @@
   white-space: nowrap;
 
   > .l {
-    @apply pl-4;
+    @apply pl-2;
+    
     width: var(--ls-left-sidebar-width);
     height: 100%;
     align-items: center;
@@ -38,10 +40,6 @@
     transform: translate3d(0, 0, 0);
   }
 
-  .it svg {
-    transform: scale(0.8);
-  }
-
   .button {
     display: flex;
     align-items: center;
@@ -130,10 +128,6 @@
   }
 }
 
-.cp__header .navigation svg {
-  transform: scale(0.7);
-}
-
 .is-electron.is-mac.is-fullscreen .cp__header > .l {
   padding-left: 1rem;
 }
@@ -178,8 +172,14 @@
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: 15ch;
+  max-width: 20ch;
   height: 14px;
+
+  .ui__icon {
+    position: relative;
+    top: 5px;
+    right: -1px;
+  }
 }
 
 .button {

+ 12 - 6
src/main/frontend/components/page.cljs

@@ -920,23 +920,27 @@
              [:a.button.journal
               {:class    (util/classnames [{:active (boolean @*journal?)}])
                :on-click #(reset! *journal? (not @*journal?))}
-              (ui/icon "calendar" {:style {:fontSize ui/icon-size}})])]
+              (ui/icon "calendar" {:size ui/icon-size})])]
 
            [:div.paginates
             [:span.flex.items-center
              {:class (util/classnames [{:is-first (= 1 @*current-page)
                                         :is-last  (= @*current-page total-pages)}])}
              (when has-prev?
-               [:a.py-4.pr-2.fade-link {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))])
+               [:a.py-4.pr-2.fade-link.flex.items-center
+                {:on-click #(to-page (dec @*current-page))}
+                (ui/icon "caret-left") (str " " (t :paginates/prev))])
              [:span.opacity-60 (str @*current-page "/" total-pages)]
              (when has-next?
-               [:a.py-4.pl-2.fade-link {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")])]]
+               [:a.py-4.pl-2.fade-link.flex.items-center
+                {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ")
+                (ui/icon "caret-right")])]]
 
            (ui/dropdown-with-links
             (fn [{:keys [toggle-fn]}]
               [:a.button.fade-link
                {:on-click toggle-fn}
-               (ui/icon "dots" {:style {:fontSize ui/icon-size}})])
+               (ui/icon "dots" {:size ui/icon-size})])
             [{:title (t :remove-orphaned-pages)
               :options {:on-click (fn []
                                     (let [orphaned-pages (model/get-orphaned-pages {})
@@ -1010,6 +1014,8 @@
           [:span]
           [:span.flex.items-center
            (when has-prev?
-             [:a.py-4.text-sm.fade-link {:on-click #(to-page (dec @*current-page))} (ui/icon "caret-left") (str " " (t :paginates/prev))])
+             [:a.py-4.text-sm.fade-link.flex.items-center {:on-click #(to-page (dec @*current-page))}
+              (ui/icon "caret-left") (str " " (t :paginates/prev))])
            (when has-next?
-             [:a.py-4.pl-2.text-sm.fade-link {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ") (ui/icon "caret-right")])]]]))]))
+             [:a.py-4.pl-2.text-sm.fade-link.flex.items-center {:on-click #(to-page (inc @*current-page))} (str (t :paginates/next) " ")
+              (ui/icon "caret-right")])]]]))]))

+ 5 - 5
src/main/frontend/components/page.css

@@ -132,13 +132,15 @@
     }
 
     .r {
-      font-size: 14px;
-
+      @apply text-base space-x-2;
+      
       a.button {
         color: var(--ls-primary-text-color);
         margin-top: 1px;
         height: unset;
         padding: 4px;
+        display: flex;
+        align-items: center;
 
         &.active {
           opacity: 1;
@@ -325,9 +327,7 @@ html.is-native-ios {
   .cp__all_pages {
     .actions > .r {
       position: relative;
-      padding: 15px 0;
-      padding-right: 10px;
-      padding-bottom: 25px;
+      padding: 15px 10px 25px 0;
       justify-content: space-between;
 
       .paginates {

+ 15 - 12
src/main/frontend/components/plugins.cljs

@@ -130,14 +130,14 @@
 (rum/defc category-tabs
   [t category on-action]
 
-  [:div.secondary-tabs.categories
+  [:div.secondary-tabs.categories.flex
    (ui/button
-    [:span (ui/icon "puzzle") (t :plugins)]
+    [:span.flex.items-center (ui/icon "puzzle") (t :plugins)]
     :intent "logseq"
     :on-click #(on-action :plugins)
     :class (if (= category :plugins) "active" ""))
    (ui/button
-    [:span (ui/icon "palette") (t :themes)]
+    [:span.flex.items-center (ui/icon "palette") (t :themes)]
     :intent "logseq"
     :on-click #(on-action :themes)
     :class (if (= category :themes) "active" ""))])
@@ -232,8 +232,10 @@
        [:strong (ui/icon "coffee")]
        [:ul.menu-list
         (for [link sponsors]
-          [:li [:a {:href link :target "_blank"}
-                [:span.flex.items-center link (ui/icon "external-link")]]])]])]
+          [:li {:key link}
+           [:a {:href link :target "_blank"}
+            [:span.flex.items-center link (ui/icon "external-link")]]])
+        ]])]
 
    [:div.r.flex.items-center
     (when (and unpacked? (not disabled?))
@@ -438,7 +440,8 @@
          (ui/tippy {:html  [:div (t :plugin/unpacked-tips)]
                     :arrow true}
                    (ui/button
-                    [:span (ui/icon "upload") (t :plugin/load-unpacked)]
+                    [:span.flex.items-center
+                     (ui/icon "upload") (t :plugin/load-unpacked)]
                     :intent "logseq"
                     :class "load-unpacked"
                     :on-click plugin-handler/load-unpacked-plugin))
@@ -534,21 +537,21 @@
           :intent "link"))
 
        (concat (if market?
-                 [{:title   [:span (ui/icon "rotate-clockwise") (t :plugin/refresh-lists)]
+                 [{:title   [:span.flex.items-center (ui/icon "rotate-clockwise") (t :plugin/refresh-lists)]
                    :options {:on-click #(reload-market-fn)}}]
-                 [{:title   [:span (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
+                 [{:title   [:span.flex.items-center (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
                    :options {:on-click #(plugin-handler/check-enabled-for-updates (not= :plugins category))}}])
 
-               [{:title   [:span (ui/icon "world") (t :settings-page/network-proxy)]
+               [{:title   [:span.flex.items-center (ui/icon "world") (t :settings-page/network-proxy)]
                  :options {:on-click #(state/pub-event! [:go/proxy-settings agent-opts])}}]
 
                (when (state/developer-mode?)
                  [{:hr true}
-                  {:title   [:span (ui/icon "file-code") "Open Preferences"]
+                  {:title   [:span.flex.items-center (ui/icon "file-code") "Open Preferences"]
                    :options {:on-click
                              #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                                 (js/apis.openPath (str root "/preferences.json")))}}
-                  {:title   [:span (ui/icon "bug") "Open " [:code " ~/.logseq"]]
+                  {:title   [:span.flex.items-center (ui/icon "bug") "Open " [:code " ~/.logseq"]]
                    :options {:on-click
                              #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                                 (js/apis.openPath root))}}]))
@@ -852,7 +855,7 @@
    (fn [{:keys [toggle-fn]}]
      [:div.toolbar-plugins-manager
       {:on-click toggle-fn}
-      [:a.button (ui/icon "puzzle")]])
+      [:a.button (ui/icon "puzzle" {:size 20})]])
 
    ;; items
    (for [[_ {:keys [key pinned?] :as opts} pid] items

+ 5 - 0
src/main/frontend/components/plugins.css

@@ -231,6 +231,11 @@
           opacity: .8;
         }
       }
+
+      a {
+        color: var(--ls-primary-text-color);
+        opacity: .7;
+      }
     }
 
     > .l {

+ 82 - 76
src/main/frontend/components/query_table.cljs

@@ -2,6 +2,7 @@
   (:require [frontend.components.svg :as svg]
             [frontend.date :as date]
             [frontend.db :as db]
+            [frontend.db.query-dsl :as query-dsl]
             [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.state :as state]
@@ -13,38 +14,9 @@
             [rum.core :as rum]
             [frontend.modules.outliner.tree :as tree]))
 
-;; TODO: extract to table utils
-(defn- sort-result-by
-  [by-item desc? result]
-  (let [comp (if desc? > <)]
-    (sort-by by-item comp result)))
-
-(rum/defc sortable-title
-  [title key by-item desc? block-id]
-  [:th.whitespace-nowrap
-   [:a {:on-click (fn []
-                    (reset! by-item key)
-                    (swap! desc? not)
-                    (when block-id
-                      (when key
-                        (editor-handler/set-block-property! block-id :query-sort-by (name key)))
-                      (editor-handler/set-block-property! block-id :query-sort-desc @desc?)))}
-    [:div.flex.items-center
-     [:span.mr-1 title]
-     (when (= @by-item key)
-       [:span
-        (if @desc? (svg/caret-down) (svg/caret-up))])]]])
-
-(defn get-keys
-  [result page?]
-  (let [keys (->> (distinct (mapcat keys (map :block/properties result)))
-                  (remove (property/built-in-properties))
-                  (remove #{:template}))
-        keys (if page? (cons :page keys) (cons :block keys))
-        keys (if page? (distinct (concat keys [:created-at :updated-at])) keys)]
-    keys))
-
-(defn attach-clock-property
+;; Util fns
+;; ========
+(defn- attach-clock-property
   [result]
   (let [ks [:block/properties :clock-time]
         result (map (fn [b]
@@ -55,8 +27,8 @@
       (map #(medley/dissoc-in % ks) result)
       result)))
 
-(defn- sort-by-fn [sort-by-item item]
-  (case sort-by-item
+(defn- sort-by-fn [sort-by-column item]
+  (case sort-by-column
     :created-at
     (:block/created-at item)
     :updated-at
@@ -65,72 +37,106 @@
     (:block/content item)
     :page
     (:block/name item)
-    (get-in item [:block/properties sort-by-item])))
+    (get-in item [:block/properties sort-by-column])))
+
+(defn- sort-result [result {:keys [sort-by-column sort-desc?]}]
+  (if (some? sort-by-column)
+    (let [comp (if sort-desc? > <)]
+      (sort-by (fn [item]
+                 (block/normalize-block (sort-by-fn sort-by-column item)))
+               comp
+               result))
+    result))
+
+(defn- get-sort-state
+  "Return current sort direction and column being sorted, respectively
+  :sort-desc? and :sort-by-column. :sort-by-column is nil if no sorting is to be
+  done"
+  [current-block]
+  (let [p-desc? (get-in current-block [:block/properties :query-sort-desc])
+        desc? (if (some? p-desc?) p-desc? true)
+        p-sort-by (keyword (get-in current-block [:block/properties :query-sort-by]))
+        sort-by-column (or (some-> p-sort-by keyword)
+                         (if (query-dsl/query-contains-filter? (:block/content current-block) "sort-by")
+                           nil
+                           :updated-at))]
+    {:sort-desc? desc?
+     :sort-by-column sort-by-column}))
+
+;; Components
+;; ==========
+(rum/defc sortable-title
+  [title column {:keys [sort-by-column sort-desc?]} block-id]
+  [:th.whitespace-nowrap
+   [:a {:on-click (fn []
+                    (editor-handler/set-block-property! block-id :query-sort-by (name column))
+                    (editor-handler/set-block-property! block-id :query-sort-desc (not sort-desc?)))}
+    [:div.flex.items-center
+     [:span.mr-1 title]
+     (when (= sort-by-column column)
+       [:span
+        (if sort-desc? (svg/caret-down) (svg/caret-up))])]]])
+
+(defn get-keys
+  "Get keys for a query table result, which are the columns in a table"
+  [result page?]
+  (let [keys (->> (distinct (mapcat keys (map :block/properties result)))
+                  (remove (property/built-in-properties))
+                  (remove #{:template}))
+        keys (if page? (cons :page keys) (cons :block keys))
+        keys (if page? (distinct (concat keys [:created-at :updated-at])) keys)]
+    keys))
 
-(defn- desc?
-  [*desc? p-desc?]
-  (cond
-    (some? @*desc?)
-    @*desc?
-    (some? p-desc?)
-    p-desc?
-    :else
-    true))
+(defn- get-columns [current-block result {:keys [page?]}]
+  (let [query-properties (some-> (get-in current-block [:block/properties :query-properties] "")
+                                 (common-handler/safe-read-string "Parsing query properties failed"))
+        columns (if (seq query-properties)
+                  query-properties
+                  (get-keys result page?))
+        included-columns #{:created-at :updated-at}]
+    (distinct
+     (if (some included-columns columns)
+       (concat (remove included-columns columns)
+               (filter included-columns columns)
+               included-columns)
+       columns))))
 
+;; Table rows are called items
 (rum/defcs result-table < rum/reactive
-  (rum/local nil ::sort-by-item)
-  (rum/local nil ::desc?)
   (rum/local false ::select?)
   [state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text]
   (when current-block
     (let [result (tree/filter-top-level-blocks result)
-          p-sort-by (keyword (get-in current-block [:block/properties :query-sort-by]))
-          p-desc? (get-in current-block [:block/properties :query-sort-desc])
           select? (get state ::select?)
-          *sort-by-item (get state ::sort-by-item)
-          *desc? (get state ::desc?)
-          sort-by-item (or @*sort-by-item (some-> p-sort-by keyword) :updated-at)
           ;; remove templates
           result (remove (fn [b] (some? (get-in b [:block/properties :template]))) result)
           result (if page? result (attach-clock-property result))
           clock-time-total (when-not page?
                              (->> (map #(get-in % [:block/properties :clock-time] 0) result)
                                   (apply +)))
-          query-properties (some-> (get-in current-block [:block/properties :query-properties] "")
-                                   (common-handler/safe-read-string "Parsing query properties failed"))
-          keys (if (seq query-properties)
-                 query-properties
-                 (get-keys result page?))
-          included-keys #{:created-at :updated-at}
-          keys (distinct
-                (if (some included-keys keys)
-                  (concat (remove included-keys keys)
-                          (filter included-keys keys)
-                          included-keys)
-                  keys))
-          desc? (desc? *desc? p-desc?)
-          result (sort-result-by (fn [item]
-                                   (block/normalize-block (sort-by-fn sort-by-item item)))
-                                 desc?
-                                 result)]
+          columns (get-columns current-block result {:page? page?})
+          ;; Sort state needs to be in sync between final result and sortable title
+          ;; as user needs to know if there result is sorted
+          sort-state (get-sort-state current-block)
+          result' (sort-result result sort-state)]
       [:div.overflow-x-auto {:on-mouse-down (fn [e] (.stopPropagation e))
                              :style {:width "100%"}
                              :class (when-not page? "query-table")}
        [:table.table-auto
         [:thead
          [:tr.cursor
-          (for [key keys]
-            (let [key-name (if (and (= key :clock-time) (integer? clock-time-total))
+          (for [column columns]
+            (let [title (if (and (= column :clock-time) (integer? clock-time-total))
                              (util/format "clock-time(total: %s)" (clock/minutes->days:hours:minutes
                                                                    clock-time-total))
-                             (name key))]
-              (sortable-title key-name key *sort-by-item *desc? (:block/uuid current-block))))]]
+                             (name column))]
+              (sortable-title title column sort-state (:block/uuid current-block))))]]
         [:tbody
-         (for [item result]
+         (for [item result']
            (let [format (:block/format item)]
              [:tr.cursor
-              (for [key keys]
-                (let [value (case key
+              (for [column columns]
+                (let [value (case column
                               :page
                               [:string (or (:block/original-name item)
                                            (:block/name item))]
@@ -154,7 +160,7 @@
                               [:string (when-let [updated-at (:block/updated-at item)]
                                          (date/int->local-time-2 updated-at))]
 
-                              [:string (get-in item [:block/properties key])])]
+                              [:string (get-in item [:block/properties column])])]
                   [:td.whitespace-nowrap {:on-mouse-down (fn [] (reset! select? false))
                                           :on-mouse-move (fn [] (reset! select? true))
                                           :on-mouse-up (fn []

+ 6 - 3
src/main/frontend/components/repo.cljs

@@ -97,7 +97,8 @@
                                      (state/set-modal! (confirm-fn)))
                                    (do
                                      (repo-handler/remove-repo! repo)
-                                     (file-sync/load-session-graphs))))}
+                                     (file-sync/load-session-graphs)
+                                     (state/pub-event! [:graph/unlinked]))))}
                     (if only-cloud? "Remove" "Unlink")])])]]))
 
 (rum/defc repos < rum/reactive
@@ -163,7 +164,7 @@
                           {:title        [:span.flex.items-center.whitespace-nowrap short-repo-name
                                           (when remote? [:span.pl-1
                                                          {:title (str "<" GraphName "> #" GraphUUID)}
-                                                         (ui/icon "cloud")])]
+                                                         (ui/icon "cloud" {:size 18})])]
                            :hover-detail repo-path ;; show full path on hover
                            :options      {:on-click (fn [e]
                                                       (if (gobj/get e "shiftKey")
@@ -227,7 +228,9 @@
                                              (check-multiple-windows? state)
                                              (toggle-fn))
                                  :title    repo-path}       ;; show full path on hover
-                                (ui/icon "database mr-2" {:style {:font-size 16} :id "database-icon"})
+                                [:span.flex.pr-2.relative
+                                 {:style {:top 1}}
+                                 (ui/icon "database" {:size 16 :id "database-icon"})]
                                 [:div.graphs
                                  [:span#repo-switch.block.pr-2.whitespace-nowrap
                                   [:span [:span#repo-name.font-medium

+ 13 - 9
src/main/frontend/components/right_sidebar.cljs

@@ -162,6 +162,8 @@
         min-ratio 0.1
         max-ratio 0.7
         keyboard-step 5
+        add-resizing-class #(.. js/document.documentElement -classList (add "is-resizing-buf"))
+        remove-resizing-class #(.. js/document.documentElement -classList (remove "is-resizing-buf"))
         set-width! (fn [ratio element]
                      (when (and el-ref element)
                        (let [width (str (* ratio 100) "%")]
@@ -199,20 +201,22 @@
                          #(.. js/document.documentElement -classList (remove cursor-class)))
                        (when (> ratio (/ min-ratio 2)) (state/open-right-sidebar!)))))}}))
              (.styleCursor false)
-             (.on "dragstart" #(.. js/document.documentElement -classList (add "is-resizing-buf")))
-             (.on "dragend" #(.. js/document.documentElement -classList (remove "is-resizing-buf")))
+             (.on "dragstart" add-resizing-class)
+             (.on "dragend" remove-resizing-class)
              (.on "keydown" (fn [e]
                               (when-let [sidebar-el (js/document.getElementById sidebar-id)]
                                 (let [width js/document.documentElement.clientWidth
-                                      offset (+
-                                              (.-x (.getBoundingClientRect sidebar-el))
-                                              (case (.-code e)
-                                                "ArrowLeft" (- keyboard-step)
-                                                "ArrowRight" keyboard-step
-                                                :else 0))
+                                      keyboard-step (case (.-code e)
+                                                      "ArrowLeft" (- keyboard-step)
+                                                      "ArrowRight" keyboard-step
+                                                      0)
+                                      offset (+ (.-x (.getBoundingClientRect sidebar-el)) keyboard-step)
                                       ratio (.toFixed (/ offset width) 6)
                                       ratio (if (= handler-position :west) (- 1 ratio) ratio)]
-                                  (when (and (> ratio min-ratio) (< ratio max-ratio)) (set-width! ratio sidebar-el))))))))
+                                  (when (and (> ratio min-ratio) (< ratio max-ratio) (not (zero? keyboard-step)))
+                                    ((add-resizing-class)
+                                     (set-width! ratio sidebar-el)))))))
+             (.on "keyup" remove-resizing-class)))
        #())
      [])
     [:.resizer {:ref el-ref

+ 4 - 4
src/main/frontend/components/search.cljs

@@ -267,12 +267,12 @@
 (rum/defc recent-search-and-pages
   [in-page-search?]
   [:div.recent-search
-   [:div.px-4.py-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items
+   [:div.wrap.px-4.py-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items.mx-1.sm:mx-0
     [:div "Recent search:"]
     (ui/with-shortcut :go/search-in-page "bottom"
       [:div.flex-row.flex.align-items
-       [:div.mr-2 "Search blocks in page:"]
-       [:div {:style {:margin-top 3}}
+       [:div.mr-3.flex "Search blocks in page:"]
+       [:div.flex.items-center
         (ui/toggle in-page-search?
                    (fn [_value]
                      (state/set-search-mode! (if in-page-search? :global :page)))
@@ -283,7 +283,7 @@
                   :interactive     true
                   :arrow           true
                   :theme       "monospace"}
-                 [:a.inline-block.fade-link
+                 [:a.flex.fade-link.items-center
                   {:style {:margin-left 12}
                    :on-click #(state/toggle! :ui/command-palette-open?)}
                   (ui/icon "command" {:style {:font-size 20}})])])]

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

@@ -517,6 +517,7 @@
               (when-let [e (and protocol host port (str protocol "://" host ":" port))]
                 [:strong.pr-1 e])
               (ui/icon "edit")]
+             :small? true
              :on-click #(state/set-sub-modal!
                          (fn [_] (plugins/user-proxy-settings-panel agent-opts))
                          {:id :https-proxy-panel :center? true})))
@@ -699,7 +700,7 @@
                       (not (file-sync-handler/synced-file-graph? current-repo)))
                  [:git "git" (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
                [:advanced "advanced" (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
-               [:features "features" (t :settings-page/tab-features) (ui/icon "app-feature" {:style {:font-size 20}
+               [:features "features" (t :settings-page/tab-features) (ui/icon "app-feature" {:style {:font-size 18}
                                                                                              :extension? true})]
                (when plugins-of-settings
                  [:plugins-setting "plugins" (t :settings-of-plugins) (ui/icon "puzzle")])]]

+ 12 - 19
src/main/frontend/components/settings.css

@@ -15,7 +15,7 @@
 
   &-inner {
     @apply flex flex-col md:flex-row;
-    
+
     > aside {
       border-right: 0 solid var(--ls-quaternary-background-color);
       border-bottom: 1px solid var(--ls-quaternary-background-color);
@@ -41,10 +41,8 @@
             color: var(--ls-primary-text-color);
 
             > i {
-              width: 20px;
-              height: 20px;
               overflow: hidden;
-              opacity: .6;
+              opacity: .9;
             }
 
             > strong {
@@ -290,31 +288,26 @@
   }
 }
 
-/* Styles for the category icon on the left of settings-modal */
-.cp__settings-inner > aside ul > li > a > i {
-  margin-right: 4px;
-}
-
 html.is-native-android,
 html.is-native-iphone,
 html.is-native-iphone-without-notch {
 
-    .cp__settings-inner {
-        > article {
-            padding-bottom: 0px;
-        }
+  .cp__settings-inner {
+    > article {
+      padding-bottom: 0;
+    }
 
-        .panel-wrap {
-            padding-bottom: 0px;
-        }
+    .panel-wrap {
+      padding-bottom: 0;
     }
+  }
 }
 
 svg.git {
-    margin-left: -4px;
-    transform: scale(0.9);
+  margin-left: -4px;
+  transform: scale(0.9);
 }
 
 svg.cmd {
-    margin-left: -1px;
+  margin-left: -1px;
 }

+ 21 - 19
src/main/frontend/components/sidebar.cljs

@@ -70,19 +70,21 @@
 (rum/defc page-name
   [name icon recent?]
   (let [original-name (db-model/get-page-original-name name)]
-    [:a {:on-click (fn [e]
-                     (let [name (util/safe-page-name-sanity-lc name)
-                           source-page (db-model/get-alias-source-page (state/get-current-repo) name)
-                           name (if (empty? source-page) name (:block/name source-page))]
-                       (if (gobj/get e "shiftKey")
-                         (when-let [page-entity (if (empty? source-page) (db/entity [:block/name name]) source-page)]
-                           (state/sidebar-add-block!
-                            (state/get-current-repo)
-                            (:db/id page-entity)
-                            :page))
-                         (route-handler/redirect-to-page! name {:click-from-recent? recent?}))))}
+    [:a.flex.items-center
+     {:on-click
+      (fn [e]
+        (let [name        (util/safe-page-name-sanity-lc name)
+              source-page (db-model/get-alias-source-page (state/get-current-repo) name)
+              name        (if (empty? source-page) name (:block/name source-page))]
+          (if (gobj/get e "shiftKey")
+            (when-let [page-entity (if (empty? source-page) (db/entity [:block/name name]) source-page)]
+              (state/sidebar-add-block!
+               (state/get-current-repo)
+               (:db/id page-entity)
+               :page))
+            (route-handler/redirect-to-page! name {:click-from-recent? recent?}))))}
      [:span.page-icon icon]
-     (pdf-assets/fix-local-asset-filename original-name)]))
+     [:span.page-title (pdf-assets/fix-local-asset-filename original-name)]]))
 
 (defn get-page-icon [page-entity]
   (let [default-icon (ui/icon "file-text")
@@ -126,8 +128,8 @@
   [t]
   (nav-content-item
    [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
-    (ui/icon "star mr-1" {:style {:font-size 16}})
-    [:span.flex-1.ml-1 (string/upper-case (t :left-side-bar/nav-favorites))]]
+    (ui/icon "star" {:size 16})
+    [:span.flex-1.ml-2 (string/upper-case (t :left-side-bar/nav-favorites))]]
 
    {:class "favorites"
     :edit-fn
@@ -150,8 +152,8 @@
   [t]
   (nav-content-item
    [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
-    (ui/icon "history mr-2" {:style {:font-size 16}})
-    [:span.flex-1
+    (ui/icon "history" {:size 16})
+    [:span.flex-1.ml-2
      (string/upper-case (t :left-side-bar/nav-recent-pages))]]
 
    {:class "recent"}
@@ -238,7 +240,7 @@
         [:div.fake-bar.absolute
          [:button
           {:on-click state/toggle-left-sidebar!}
-          (ui/icon "menu-2" {:style {:fontSize ui/icon-size}})]])
+          (ui/icon "menu-2" {:size ui/icon-size})]])
 
       [:nav.px-4.flex.flex-col.gap-1
        {:aria-label "Navigation menu"}
@@ -299,7 +301,7 @@
                        (and (util/sm-breakpoint?)
                             (state/toggle-left-sidebar!))
                        (state/pub-event! [:go/search]))}
-          (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
+          (ui/icon "circle-plus" {:style {:font-size 20}})
           [:span.flex-1 (t :right-side-bar/new-page)]])]]]))
 
 (rum/defc left-sidebar < rum/reactive
@@ -353,7 +355,7 @@
      (left-sidebar {:left-sidebar-open? left-sidebar-open?
                     :route-match route-match})
 
-     [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row
+     [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none
 
       {:tabIndex "-1"}
 

+ 26 - 24
src/main/frontend/components/sidebar.css

@@ -45,12 +45,12 @@
 }
 
 #main-container {
-    /* Hack: (overflow-y) to fix sticky header not working */
-    /* To reproduce: quick creating blocks */
-    overflow-y: hidden;
-    position: relative;
-    height: 100%;
-    transition: padding-left .3s;
+  /* Hack: (overflow-y) to fix sticky header not working */
+  /* To reproduce: quick creating blocks */
+  overflow-y: hidden;
+  position: relative;
+  height: 100%;
+  transition: padding-left .3s;
 
   &.is-left-sidebar-open {
     padding-left: 0;
@@ -112,13 +112,14 @@
   }
 
   .page-icon {
-    @apply mr-1 align-baseline;
+    @apply flex items-center mr-1 align-baseline;
+
     width: 16px;
-    height: 16px;
+    height: 18px;
     text-align: center;
     display: inline-block;
     line-height: 1em;
-    color: var(--ls-icon-color);
+    opacity: .6;
   }
 
   a.item {
@@ -130,7 +131,7 @@
     > .ti {
       font-size: 16px;
       margin-right: 8px;
-      opacity: .6;
+      opacity: .9;
       position: relative;
     }
 
@@ -139,11 +140,7 @@
     }
 
     &:hover {
-      opacity: .8;
-
-      .ti {
-        opacity: .8;
-      }
+      color: var(--ls-secondary-text-color);
     }
   }
 
@@ -232,13 +229,21 @@
         a {
           width: 100%;
           padding: 2px 24px;
-          display: block;
-          text-overflow: ellipsis;
-          overflow: hidden;
-          white-space: nowrap;
           color: var(--ls-primary-text-color);
           transition: background-color .3s;
 
+          .page-title {
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            flex-grow: 1;
+          }
+
+          .page-icon {
+            display: flex;
+            align-items: center;
+          }
+
           &:hover {
             background-color: var(--ls-quaternary-background-color);
           }
@@ -369,9 +374,6 @@
 }
 
 .ls-left-sidebar-open {
-  .cp__header > .r {
-    display: none;
-  }
 
   @screen sm {
     .cp__header > .r {
@@ -464,8 +466,8 @@ html[data-theme='dark'] {
   &.closed {
     width: 0 !important;
 
-    .resizer {
-      left: -4px;
+    @screen lg {
+      width: 4px !important;
     }
   }
 

+ 31 - 37
src/main/frontend/db/query_dsl.cljs

@@ -38,11 +38,7 @@
 ;; project (block, TBD)
 
 ;; Sort by (field, asc/desc):
-
-;; created_at
-;; last_modified_at
-
-;; (sort-by last_modified_at asc)
+;; (sort-by created-at asc)
 
 ;; (between -7d +7d)
 
@@ -151,13 +147,16 @@
 
       :else
       (->> clauses
-           (map (fn [result]
-                  (if (list? result)
-                    result
-                    (let [result (if (vector? (ffirst result))
-                                   (apply concat result)
-                                   result)]
-                      (cons 'and (seq result))))))
+           (mapcat (fn [result]
+                     (cond
+                       ;; rule like (task ?b #{"NOW"})
+                       (list? result)
+                       [result]
+                       ;; datalog clause like [[?b :block/uuid]]
+                       (vector? result)
+                       result
+                       :else
+                       [(cons 'and (seq result))])))
            (apply list fe)))
 
     :else
@@ -325,33 +324,24 @@
   (when-let [num (second e)]
     (when (integer? num)
       (reset! sample num)
-      {:query [['?p :block/uuid]]})))
+      ;; blank b/c this post-process filter doesn't effect query
+      {})))
 
 (defn- build-sort-by
   [e sort-by_]
-  (let [[k order] (rest e)
-             order (if (and order (contains? #{:asc :desc}
-                                             (keyword (string/lower-case (name order)))))
-                     (keyword (string/lower-case (name order)))
-                     :desc)
-             k (-> (string/lower-case (name k))
-                   (string/replace "_" "-"))
-             get-value (cond
-                         (= k "created-at")
-                         :block/created-at
-
-                         (= k "updated-at")
-                         :block/updated-at
-
-                         :else
-                         #(get-in % [:block/properties k]))
-             comp (if (= order :desc) >= <=)]
-         (reset! sort-by_
-                 (fn [result]
-                   (->> result
-                        flatten
-                        (sort-by get-value comp))))
-         nil))
+  (let [[k order*] (map keyword (rest e))
+        order (if (contains? #{:asc :desc} order*)
+                order*
+                :desc)
+        comp (if (= order :desc) >= <=)]
+    (reset! sort-by_
+            (fn sort-results [result]
+              ;; first because there is one binding result in query-wrapper
+              (sort-by #(-> % first (get-in [:block/properties k]))
+                       comp
+                       result)))
+    ;; blank b/c this post-process filter doesn't effect query
+    {}))
 
 (defn- build-page
   [e]
@@ -397,7 +387,7 @@ Some bindings in this fn:
          page-ref? (page-ref/page-ref? e)]
      (when (or (and page-ref?
                     (not (contains? #{'page-property 'page-tags} (:current-filter env))))
-               (contains? #{'between 'property 'todo 'task 'priority 'sort-by 'page} fe)
+               (contains? #{'between 'property 'todo 'task 'priority 'page} fe)
                (and (not page-ref?) (string? e)))
        (reset! blocks? true))
      (cond
@@ -592,6 +582,10 @@ Some bindings in this fn:
                             (when sort-by
                               {:transform-fn sort-by})))))))
 
+(defn query-contains-filter?
+  [query filter-name]
+  (string/includes? query (str "(" filter-name)))
+
 (comment
   ;; {{query (and (page-property foo bar) [[hello]])}}
 

+ 53 - 33
src/main/frontend/dicts.cljc

@@ -217,7 +217,7 @@
         :open-new-window "New window"
         :sync-from-local-files "Refresh"
         :sync-from-local-files-detail "Import changes from local files"
-        :sync-from-local-changes-detected "Refresh detects and processes files modified on your disk and diverged from the actual Logseq page content. Continue?"
+        :sync-from-local-changes-detected "Refresh detects and processes files modified on your disk that have diverged from the current Logseq page content. Continue?"
 
         :unlink "unlink"
         :search/publishing "Search"
@@ -2097,6 +2097,7 @@
            :help/key-commands "Nøkkelkommandoer"
            :help/working-with-lists " (arbeide med lister)"
            :help/select-nfs-browser " Vennligst bruk en annen nettleser (f.eks. siste Chrome) som støtter NFS for å åpne en lokal mappe."
+           :help/forum-community "Forum-samfunn"
            :undo "Angre"
            :redo "Gjør om"
            :general "Generell"
@@ -2126,6 +2127,8 @@
            :right-side-bar/all-pages "Alle sider"
            :right-side-bar/flashcards "Flashcards"
            :right-side-bar/new-page "Ny side"
+           :right-side-bar/separator "Høyre sidestolpe størrelsesendring"
+           :right-side-bar/show-journals "Vis dagbøker"
            :left-side-bar/journals "Dagbøker"
            :left-side-bar/new-page "Ny side"
            :left-side-bar/nav-favorites "Favoritter"
@@ -2236,6 +2239,12 @@
            :settings-page/tab-shortcuts "Snarveier"
            :settings-page/tab-version-control "Versjonskontroll"
            :settings-page/tab-advanced "Avansert"
+           :settings-page/custom-global-configuration "Egendefinert global konfigurasjon"
+           :settings-page/edit-global-config-edn "Rediger global config.edn"
+           :settings-page/enable-flashcards "Flashcards"
+           :settings-page/export-theme "Eksporter tema"
+           :settings-page/sync "Synkronisering"
+           :settings-page/tab-features "Funksjoner"
            :logseq "Logseq"
            :on "PÅ"
            :more-options "Flere valg"
@@ -2378,12 +2387,23 @@
            :graph/save "Lagrer..."
            :graph/save-error "Lagring feilet"
            :graph/save-success "Lagring vellykket"
+           :graph/all-graphs "Alle grafer"
+           :graph/local-graphs "Lokale grafer"
+           :graph/remote-graphs "Fjerngrafer"
            :page/copy-page-url "Kopier side URL"
            :page/open-backup-directory "Åpne mappe med sidens sikkerhetskopier"
            :plugin/not-installed "Ikke installert"
            :settings-page/edit-export-css "Rediger export.css"
            :settings-page/network-proxy "Nettverksproxy"
-           :settings-page/plugin-system "System for utvidelser"}
+           :settings-page/plugin-system "System for utvidelser"
+           :discourse-title "Vårt forum!"
+           :importing "Import"
+           :asset/copy "Kopier bilde"
+           :asset/delete "Slett bilde"
+           :asset/maximize "Maksimer bilde"
+           :asset/open-in-browser "Åpne bilde i nettleser"
+           :asset/show-in-folder "Vis bilde i mappe"
+           :linked-references/filter-search "Søk i lenkede referanser"}
 
    :pt-BR {:on-boarding/demo-graph "Esse é um grafo de demonstração, mudanças não serão salvas enquanto uma pasta local não for aberta."
            :on-boarding/add-graph "Adicionar grafo"
@@ -4711,11 +4731,11 @@
                           :default "tutorial-pl.md")
         :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-pl.md")
                                  :default "dummy-notes-pl.md")
-        :on-boarding/demo-graph "To jest przykładowy graf, zmiany nie zostaną zapisane dopóki nie otworzysz jakiegoś lokalnego katalogu."
+        :on-boarding/demo-graph "To jest przykładowy graf, zmiany nie zostaną zapisane dopóki nie otworzysz lokalnego katalogu."
         :on-boarding/add-graph "Dodaj graf"
         :on-boarding/open-local-dir "Otwórz lokalny katalog"
         :on-boarding/new-graph-desc-1 "Logseq wspiera zarówno składnię Markdown jak i Org-mode. Możesz otworzyć istniejący już folder lub utworzyć nowy katalog na swoim urządzeniu. Wszystkie dane będą przechowywane tylko na tym urządzeniu."
-        :on-boarding/new-graph-desc-2 "Po otwarciu wybranego katalogu zostaną w nim uworzone trzy dodatkowe katalogi:"
+        :on-boarding/new-graph-desc-2 "Po otwarciu wybranego katalogu zostaną w nim utworzone trzy dodatkowe katalogi:"
         :on-boarding/new-graph-desc-3 "/journals - przechowuje wszystkie strony twojego dziennika"
         :on-boarding/new-graph-desc-4 "/pages - przechowuje wszystkie pozostałe, utworzone przez Ciebie strony"
         :on-boarding/new-graph-desc-5 "/logseq - przechowuje konfigurację, custom.css oraz niektóre meta dane."
@@ -4732,19 +4752,19 @@
         :help/community "Społeczność Discord"
         :help/forum-community "Forum dyskusyjne"
         :help/awesome-logseq "Awesome Logseq"
-        :help/shortcuts "Skróty klawiaturowe"
+        :help/shortcuts "Skróty klawiszowe"
         :help/shortcuts-triggers "Akcja"
         :help/shortcut "Skrót"
         :help/slash-autocomplete "Autouzupełnianie poleceń /"
         :help/block-content-autocomplete "Autouzupełnianie treści bloku"
         :help/reference-autocomplete "Autouzupełnianie referencji bloku"
         :help/block-reference "Referencja bloku"
-        :help/key-commands "Kluczowe"
+        :help/key-commands "Polecenia klawiszowe"
         :help/working-with-lists " (praca z listami)"
         :help/select-nfs-browser "Proszę użyć innej przeglądarki (na przykład ostatniego Chrome), która wspiera otwieranie lokalnych katalogów (NFS feature)"
         :undo "Cofnij"
         :redo "Ponów"
-        :general "General"
+        :general "Ogólne"
         :more "Więcej"
         :search/result-for "Wyniki wyszukiwania dla"
         :search/items "elementów"
@@ -4767,7 +4787,7 @@
         :right-side-bar/favorites "Ulubione"
         :right-side-bar/page-graph "Graf strony"
         :right-side-bar/block-ref "Ref bloku"
-        :right-side-bar/graph-view "Widok gafu"
+        :right-side-bar/graph-view "Widok grafu"
         :right-side-bar/all-pages "Wszystkie strony"
         :right-side-bar/flashcards "Fiszki"
         :right-side-bar/new-page "Nowa strona"
@@ -4781,7 +4801,7 @@
         :format/markdown "Markdown"
         :format/org-mode "Org mode"
         :reference/linked "Powiązane referencje"
-        :reference/unlinked-ref "Niepołączone referencje"
+        :reference/unlinked-ref "Niepowiązane referencje"
         :page/presentation-mode "Tryb prezentacji"
         :page/edit-properties-placeholder "Właściwości"
         :page/delete-success "Strona {1} została usunięta!"
@@ -4796,7 +4816,7 @@
         :page/make-public "Oznacz jako publiczną"
         :page/version-history "Sprawdź historię zmian"
         :page/open-backup-directory "Otwórz katalog z kopiami bezpieczeństwa"
-        :page/make-private "Oznacz jako prywantą"
+        :page/make-private "Oznacz jako prywatną"
         :page/delete "Usuń stronę"
         :page/add-to-favorites "Dodaj do ulubionych"
         :page/unfavorite "Usuń z ulubionych"
@@ -4826,29 +4846,29 @@
         :draw/specify-title "Proszę najpierw określić tytuł strony!"
         :draw/rename-success "Nazwa pliku została zmieniona!"
         :draw/rename-failure "Zmiana nazwy pliku nieudana. Powód: "
-        :draw/title-placeholder "Nienazwany"
+        :draw/title-placeholder "Bez nazwy"
         :draw/save "Zapisz"
         :draw/save-changes "Zapisz zmiany"
         :draw/new-file "Nowy plik"
         :draw/list-files "Wylistuj pliki"
         :draw/delete "Usuń"
         :draw/more-options "Więcej opcji"
-        :draw/back-to-logseq "Powrót do LogSeq"
+        :draw/back-to-logseq "Powrót do Logseq"
         :text/image "Obrazek"
         :asset/confirm-delete "Jesteś pewien, że chcesz usunąć ten {1}?"
         :asset/physical-delete "Usuń ten plik z dysku (operacja nie może zostać cofnięta)"
         :content/copy "Kopiuj"
         :content/cut "Wytnij"
         :content/make-todos "Stwórz {1}s"
-        :content/copy-block-ref "Kopiuje ref bloku"
+        :content/copy-block-ref "Kopiuj ref bloku"
         :content/copy-block-emebed "Kopiuj blok jako embed"
         :content/focus-on-block "Skup się na bloku"
         :content/open-in-sidebar "Otwórz w panelu bocznym"
         :content/copy-as-json "Kopiuj jako JSON"
-        :content/click-to-edit "Kliknij aby edytować"
-        :settings-page/git-desc "jest użyty do kontroli wersji stron, możesz kliknąć pionowe trzy kropki żeby zobaczyć historię zmian strony."
-        :settings-page/git-confirm "Musisz uruchomić ponownie aplikację żeby zastosować zmiany w ustawieniach GITa."
-        :settings-page/git-switcher-label "Włącz Autocommit GITa"
+        :content/click-to-edit "Kliknij, aby edytować"
+        :settings-page/git-desc "jest użyty do kontroli wersji stron, aby zobaczyć historię zmian strony, kliknij na ikonę trzech pionowych kropek."
+        :settings-page/git-confirm "Musisz uruchomić ponownie aplikację żeby zastosować zmiany w ustawieniach Gita."
+        :settings-page/git-switcher-label "Włącz opcję autocommit w Git"
         :settings-page/git-commit-delay "Wykonaj commit co każde [s.]"
         :settings-page/edit-config-edn "Edytuj config.edn"
         :settings-page/edit-custom-css "Edytuj custom.css"
@@ -4858,15 +4878,15 @@
         :settings-page/export-theme "Eksportuj motyw graficzny"
         :settings-page/show-brackets "Pokaż kwadratowe nawiasy"
         :settings-page/spell-checker "Sprawdzanie pisowni"
-        :settings-page/auto-updater "Auto aktualizacje"
+        :settings-page/auto-updater "Automatyczne aktualizacje"
         :settings-page/disable-sentry "Wyślij statystyki użycia aplikacji do Logseq"
         :settings-page/preferred-outdenting "Logiczne zmniejszanie wcięć"
         :settings-page/custom-date-format "Preferowany format daty"
         :settings-page/preferred-file-format "Preferowany format pliku"
         :settings-page/preferred-workflow "Preferowany flow TODOsów"
-        :settings-page/enable-shortcut-tooltip "Włącz dymek podpowiedzi skrótu klawiszowego"
+        :settings-page/enable-shortcut-tooltip "Włącz podpowiedzi skrótu klawiszowego"
         :settings-page/enable-timetracking "Mierzenie czasu"
-        :settings-page/enable-tooltip "Dymki podpowiedzi"
+        :settings-page/enable-tooltip "Podpowiedzi"
         :settings-page/enable-journals "Dzienniki"
         :settings-page/enable-all-pages-public "Publikuj wszystkie strony"
         :settings-page/enable-encryption "Szyfrowanie"
@@ -4901,14 +4921,14 @@
         :delete "Usuń"
         :save "Zapisz"
         :type "Typ"
-        :re-index "Ponownie indeksuj"
+        :re-index "Indeksuj ponownie"
         :re-index-detail "Przebuduj graf od nowa"
-        :re-index-multiple-windows-warning "Musisz zamknąć inne okna zanim rozpoczniesz proces ponownej indeksacji"
-        :re-index-discard-unsaved-changes-warning "Ponowna indeksacja odrzuci zmiany w obecnym grafie a następnie przejdzie po wszystkich plikach zapisanych na dysku. Utracisz wszystkie niezapisane zmiany a sam proces może trochę potrwać. Kontynuować?"
+        :re-index-multiple-windows-warning "Musisz zamknąć inne okna, zanim rozpoczniesz proces ponownej indeksacji"
+        :re-index-discard-unsaved-changes-warning "Ponowna indeksacja odrzuci zmiany w obecnym grafie, a następnie przejdzie po wszystkich plikach zapisanych na dysku. Utracisz wszystkie niezapisane zmiany, a sam proces może trochę potrwać. Kontynuować?"
         :open-new-window "Nowe okno"
         :sync-from-local-files "Odśwież"
         :sync-from-local-files-detail "Importuj zmiany z lokalnych plików"
-        :sync-from-local-changes-detected "Funkcja Odśwież wykrywa i procesuje pliki zmianione na dysku. Wszystkie pliki różniące się od tych w Logseq zostaną ponownie wczytane. Kontynuować?"
+        :sync-from-local-changes-detected "Funkcja Odśwież wykrywa i procesuje pliki zmienione na dysku. Wszystkie pliki różniące się od tych w Logseq zostaną ponownie wczytane. Kontynuować?"
 
         :unlink "Usuń link"
         :search/publishing "Szukaj"
@@ -4952,13 +4972,13 @@
         :settings-of-plugins "Ustawienia pluginu"
         :plugins "Pluginy"
         :themes "Motywy graficzne"
-        :developer-mode-alert "Musisz uruchomić ponownie aplikację żeby włączyć system pluginów. Zrobić to teraz?"
-        :relaunch-confirm-to-work "Powinienem uruchomić ponownie aplikację żeby to zaczęło działać. Zrobić to teraz?"
+        :developer-mode-alert "Aby włączyć system pluginów, aplikacja musi zostać uruchomiona ponownie. Zrobić to teraz?"
+        :relaunch-confirm-to-work "Aby wprowadzić zmiany, aplikacja musi zostać uruchomiona ponownie. Zrobić to teraz?"
         :join-community "Dołącz do społeczności"
         :sponsor-us "Sponsoruj nas"
         :discourse-title "Nasze forum!"
         :discord-title "Nasza grupa Discord!" ;; unused
-        :help-shortcut-title "Kliknij aby sprawdzić skróty klawiszowe i inne takie"
+        :help-shortcut-title "Kliknij, aby sprawdzić skróty klawiszowe oraz inne porady"
         :loading "Ładuję"
         :cloning "Klonuję"
         :parsing-files "Parsuję pliki"
@@ -4999,7 +5019,7 @@
         :plugin/all "Wszystkie"
         :plugin/unpacked "Rozpakowany"
         :plugin/marketplace "Marketplace"
-        :plugin/delete-alert "Czy napeno chcesz usunąć plugin [{1}]?"
+        :plugin/delete-alert "Czy na pewno chcesz usunąć plugin [{1}]?"
         :plugin/open-settings "Otwórz ustawienia"
         :plugin/open-package "Otwórz paczkę"
         :plugin/load-unpacked "Załaduj rozpakowany plugin"
@@ -5009,15 +5029,15 @@
         :plugin/contribute "✨ Napisz i prześlij nowy plugin"
         :plugin/marketplace-tips "Jeżeli plugin po załadowaniu nie działa, spróbuj zrestartować Logseq."
         :plugin/up-to-date "Wszystko jest aktualne"
-        :plugin/custom-js-alert "Found the custom.js file, is it allowed to execute? (If you don't understand the content of this file, it is recommended not to allow execution, which has certain security risks.)"
+        :plugin/custom-js-alert "Wykryto plik custom.js, czy chcesz go wykonać? Ze względów bezpieczeństwa, nie rekomendujemy wykonywania tego pliku, jeżeli nie znasz jego zawartości."
 
         :pdf/copy-ref "Kopiuj ref"
         :pdf/copy-text "Kopiuj tekst"
         :pdf/linked-ref "Połączone odwołania"
-        :pdf/toggle-dashed "Dashed style for area highlight"
+        :pdf/toggle-dashed "Styl kreskowany dla podświetlenia obszaru"
 
         :updater/new-version-install "Nowa wersja została ściągnięta."
-        :updater/quit-and-install "Uruchom ponownie żeby zainstalować"
+        :updater/quit-and-install "Uruchom ponownie, aby zainstalować"
 
         :paginates/pages "{1} wszystkich stron"
         :paginates/prev "← Poprzednia"
@@ -5031,7 +5051,7 @@
         :select.graph/empty-placeholder-description "Brak pasujących grafów. Chcesz dodać nowy?"
         :select.graph/add-graph "Tak, dodaj nowy graf"
 
-        :file-sync/other-user-graph "Obecny lokalny graf jest przypisany do zdalnego grafu innego użytkownika. Nie mo rozpocząć synchronizacji."
+        :file-sync/other-user-graph "Obecny lokalny graf jest przypisany do zdalnego grafu innego użytkownika. Nie można rozpocząć synchronizacji."
         :file-sync/graph-deleted "Obecny zdalny graf został usunięty"
         }
    :tongue/fallback :en})
@@ -5045,7 +5065,7 @@
                 {:label "Afrikaans" :value :af}
                 {:label "Español" :value :es}
                 {:label "Norsk (bokmål)" :value :nb-NO}
-                {:label "Polski" :value :pl}
+                {:label "polski" :value :pl}
                 {:label "Português (Brasileiro)" :value :pt-BR}
                 {:label "Português (Europeu)" :value :pt-PT}
                 {:label "Русский" :value :ru}

+ 3 - 3
src/main/frontend/encrypt.cljs

@@ -97,7 +97,7 @@
             encrypted (ipc/ipc "encrypt-with-passphrase" passphrase raw-content)]
       (utf8/decode encrypted))
 
-    (mobile-util/native-ios?)
+    (mobile-util/native-platform?)
     (p/chain (.encryptWithPassphrase mobile-util/file-sync
                                      (clj->js {:passphrase passphrase :content content}))
              #(js->clj % :keywordize-keys true)
@@ -117,12 +117,12 @@
             decrypted (ipc/ipc "decrypt-with-passphrase" passphrase raw-content)]
       (utf8/decode decrypted))
 
-    (mobile-util/native-ios?)
+    (mobile-util/native-platform?)
     (p/chain (.decryptWithPassphrase mobile-util/file-sync
                                      (clj->js {:passphrase passphrase :content content}))
              #(js->clj % :keywordize-keys true)
              :data)
-    
+
     :else
     (p/let [_ (loader/load :age-encryption)
             lazy-decrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/decrypt-with-user-passphrase)

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

@@ -210,7 +210,7 @@
   [dir path]
   (util/p-handle
    (stat dir path)
-   (fn [_stat] true)
+   (fn [stat] (not (nil? stat)))
    (fn [_e] false)))
 
 (defn dir-exists?

+ 28 - 41
src/main/frontend/fs/capacitor_fs.cljs

@@ -62,12 +62,7 @@
 
 (defn- <stat [path]
   (-> (p/chain (.stat Filesystem (clj->js {:path path}))
-               #(js->clj % :keywordize-keys true)
-               #(update % :type (fn [v]
-                                  (case v
-                                    "NSFileTypeDirectory" "directory"
-                                    "NSFileTypeRegular" "file"
-                                    v))))
+               #(js->clj % :keywordize-keys true))
       (p/catch (fn [error]
                  (js/console.error "stat Error: " path ": " error)
                  nil))))
@@ -82,36 +77,26 @@
                      (p/let [d (first dirs)
                              files (<readdir d)
                              files (->> files
-                                        (remove (fn [file]
-                                                  (or (string/starts-with? file ".")
-                                                      (and (mobile-util/native-android?)
-                                                           (or (string/includes? file "#")
-                                                               (string/includes? file "%")))
-                                                      (= file "bak")))))
-                             files (->> files
-                                        (map (fn [file]
-                                               ;; TODO: use uri-join
-                                               (str (string/replace d #"/+$" "")
-                                                    "/"
-                                                    (if (mobile-util/native-ios?)
-                                                      (js/encodeURI file)
-                                                      file)))))
-                             files-with-stats (p/all (mapv <stat files))
-                             files-dir (->> files-with-stats
+                                        (remove (fn [{:keys [name  type]}]
+                                                  (or (string/starts-with? name ".")
+                                                      (and (= type "directory")
+                                                           (or (= name "bak")
+                                                               (= name "version-files")))))))
+                             files-dir (->> files
                                             (filterv #(= (:type %) "directory"))
                                             (mapv :uri))
                              files-result
                              (p/all
-                              (->> files-with-stats
+                              (->> files
                                    (filter #(= (:type %) "file"))
                                    (filter
                                     (fn [{:keys [uri]}]
                                       (some #(string/ends-with? uri %)
                                             [".md" ".markdown" ".org" ".edn" ".css"])))
                                    (mapv
-                                    (fn [{:keys [uri] :as file-result}]
+                                    (fn [{:keys [uri] :as file-info}]
                                       (p/chain (<read-file-with-utf8 uri)
-                                               #(assoc file-result :content %))))))]
+                                               #(assoc file-info :content %))))))]
                        (p/recur (concat result files-result)
                                 (concat (rest dirs) files-dir)))))]
     (js->clj result :keywordize-keys true)))
@@ -221,7 +206,7 @@
 
 (defn get-file-path [dir path]
   (let [dir (some-> dir (string/replace #"/+$" ""))
-        dir (if (string/starts-with? dir "/")
+        dir (if (and (not-empty dir) (string/starts-with? dir "/"))
               (do
                 (js/console.trace "WARN: detect absolute path, use URL instead")
                 (str "file://" (js/encodeURI dir)))
@@ -271,13 +256,18 @@
                    (log/error :mkdir! {:path dir
                                        :error error})))))
   (mkdir-recur! [_this dir]
-    (p/let [result (.mkdir Filesystem
-                           (clj->js
-                            {:path dir
-                             ;; :directory (.-ExternalStorage Directory)
-                             :recursive true}))]
-      (js/console.log result)
-      result))
+    (p/let
+     [_ (-> (.mkdir Filesystem
+                    (clj->js
+                     {:path dir
+                      :recursive true}))
+            (p/catch (fn [error]
+                       (log/error :mkdir-recur! {:path dir
+                                                 :error error}))))
+      stat (<stat dir)]
+      (if (= (:type stat) "directory")
+        (p/resolved true)
+        (p/rejected (js/Error. "mkdir-recur! failed")))))
   (readdir [_this dir]                  ; recursive
     (let [dir (if-not (string/starts-with? dir "file://")
                 (str "file://" dir)
@@ -286,13 +276,13 @@
   (unlink! [this repo path _opts]
     (p/let [path (get-file-path nil path)
             repo-url (config/get-local-dir repo)
-            recycle-dir (str repo-url config/app-name "/.recycle") ;; logseq/.recycle
+            recycle-dir (util/safe-path-join repo-url config/app-name ".recycle") ;; logseq/.recycle
             ;; convert url to pure path
             file-name (-> (string/replace path repo-url "")
                           (string/replace "/" "_")
                           (string/replace "\\" "_"))
-            new-path (str recycle-dir "/" file-name)]
-      (protocol/mkdir! this recycle-dir)
+            new-path (str recycle-dir "/" file-name)
+            _ (protocol/mkdir-recur! this recycle-dir)]
       (protocol/rename! this repo path new-path)))
   (rmdir! [_this _dir]
     ;; Too dangerous!!! We'll never implement this.
@@ -330,11 +320,8 @@
          (log/error :copy-file-failed error)))))
   (stat [_this dir path]
     (let [path (get-file-path dir path)]
-      (p/let [result (.stat Filesystem (clj->js
-                                        {:path path
-                                         ;; :directory (.-ExternalStorage Directory)
-                                         }))]
-        result)))
+      (p/chain (.stat Filesystem (clj->js {:path path}))
+               #(js->clj % :keywordize-keys true))))
   (open-dir [_this _ok-handler]
     (p/let [_ (when (mobile-util/native-android?) (android-check-permission))
             {:keys [path localDocumentsPath]} (-> (.pickFolder mobile-util/folder-picker)

+ 115 - 48
src/main/frontend/fs/sync.cljs

@@ -26,7 +26,8 @@
             [frontend.fs :as fs]
             [frontend.encrypt :as encrypt]
             [medley.core :refer [dedupe-by]]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [promesa.core :as p]))
 
 ;;; ### Commentary
 ;; file-sync related local files/dirs:
@@ -181,8 +182,8 @@
   [latest-txid graph-uuid user-uuid repo]
   {:pre [(int? latest-txid) (>= latest-txid 0)]}
   (persist-var/-reset-value! graphs-txid [user-uuid graph-uuid latest-txid] repo)
-  (some-> (persist-var/persist-save graphs-txid)
-          p->c)
+  (p/let [_ (persist-var/persist-save graphs-txid)]
+    (state/pub-event! [:graph/refresh]))
   (when (state/developer-mode?) (assert-local-txid<=remote-txid)))
 
 (defn clear-graphs-txid! [repo]
@@ -633,7 +634,7 @@
 (defprotocol IRSAPI
   (rsapi-ready? [this graph-uuid] "return true when rsapi ready")
   (<key-gen [this] "generate public+private keys")
-  (<set-env [this prod? private-key public-key graph-uuid] "set environment")
+  (<set-env [this graph-uuid prod? private-key public-key] "set environment")
   (<get-local-files-meta [this graph-uuid base-path filepaths] "get local files' metadata")
   (<get-local-all-files-meta [this graph-uuid base-path] "get all local files' metadata")
   (<rename-local-file [this graph-uuid base-path from to])
@@ -642,8 +643,8 @@
   (<delete-local-files [this graph-uuid base-path filepaths])
   (<update-remote-files [this graph-uuid base-path filepaths local-txid] "local -> remote, return err or txid")
   (<delete-remote-files [this graph-uuid base-path filepaths local-txid] "return err or txid")
-  (<encrypt-fnames [this fnames])
-  (<decrypt-fnames [this fnames]))
+  (<encrypt-fnames [this graph-uuid fnames])
+  (<decrypt-fnames [this graph-uuid fnames]))
 
 (defprotocol IRemoteAPI
   (<user-info [this] "user info")
@@ -652,6 +653,7 @@
   (<get-remote-graph [this graph-name-opt graph-uuid-opt] "get graph info by GRAPH-NAME-OPT or GRAPH-UUID-OPT")
   (<get-remote-file-versions [this graph-uuid filepath] "get file's version list")
   (<list-remote-graphs [this] "list all remote graphs")
+  (<get-deletion-logs [this graph-uuid from-txid] "get deletion logs from FROM-TXID")
   (<get-diff [this graph-uuid from-txid] "get diff from FROM-TXID, return [txns, latest-txid, min-txid]")
   (<create-graph [this graph-name] "create graph")
   (<delete-graph [this graph-uuid] "delete graph")
@@ -671,17 +673,17 @@
   it happens on macos (case-insensitive fs)
 
   return canonicalized filepath if exists"
-  [irsapi base-path filepath]
+  [graph-uuid irsapi base-path filepath]
   (go
-    (let [r (<! (<get-local-files-meta irsapi "" base-path [filepath]))]
+    (let [r (<! (<get-local-files-meta irsapi graph-uuid base-path [filepath]))]
       (when (some-> r first :path (not= filepath))
         (-> r first :path)))))
 
 
 (defn <local-file-not-exist?
-  [irsapi base-path filepath]
+  [graph-uuid irsapi base-path filepath]
   (go
-    (let [r (<! (<get-local-files-meta irsapi "" base-path [filepath]))]
+    (let [r (<! (<get-local-files-meta irsapi graph-uuid base-path [filepath]))]
 
       (or
        ;; not found at all
@@ -718,13 +720,13 @@
   (rsapi-ready? [_ graph-uuid] (and (= graph-uuid graph-uuid') private-key' public-key'))
   (<key-gen [_] (go (js->clj (<! (p->c (ipc/ipc "key-gen")))
                              :keywordize-keys true)))
-  (<set-env [_ prod? private-key public-key graph-uuid]
+  (<set-env [_ graph-uuid prod? private-key public-key]
     (when (not-empty private-key)
       (print (util/format "[%s] setting sync age-encryption passphrase..." graph-uuid)))
     (set! graph-uuid' graph-uuid)
     (set! private-key' private-key)
     (set! public-key' public-key)
-    (p->c (ipc/ipc "set-env" (if prod? "prod" "dev") private-key public-key)))
+    (p->c (ipc/ipc "set-env" graph-uuid (if prod? "prod" "dev") private-key public-key)))
   (<get-local-all-files-meta [_ graph-uuid base-path]
     (go
       (let [r (<! (<retry-rsapi #(p->c (ipc/ipc "get-local-all-files-meta" graph-uuid base-path))))]
@@ -779,9 +781,9 @@
         (<!
          (<retry-rsapi
           #(p->c (ipc/ipc "delete-remote-files" graph-uuid base-path filepaths local-txid token)))))))
-  (<encrypt-fnames [_ fnames] (go (js->clj (<! (p->c (ipc/ipc "encrypt-fnames" fnames))))))
-  (<decrypt-fnames [_ fnames] (go
-                                (let [r (<! (p->c (ipc/ipc "decrypt-fnames" fnames)))]
+  (<encrypt-fnames [_ graph-uuid fnames] (go (js->clj (<! (p->c (ipc/ipc "encrypt-fnames" graph-uuid fnames))))))
+  (<decrypt-fnames [_ graph-uuid fnames] (go
+                                (let [r (<! (p->c (ipc/ipc "decrypt-fnames" graph-uuid fnames)))]
                                   (if (instance? ExceptionInfo r)
                                     (ex-info "decrypt-failed" {:fnames fnames} (ex-cause r))
                                     (js->clj r))))))
@@ -804,7 +806,7 @@
     (go (let [r (<! (p->c (.keygen mobile-util/file-sync #js {})))]
           (-> r
               (js->clj :keywordize-keys true)))))
-  (<set-env [_ prod? secret-key public-key graph-uuid]
+  (<set-env [_ graph-uuid prod? secret-key public-key]
     (set! graph-uuid' graph-uuid)
     (set! private-key secret-key)
     (set! public-key' public-key)
@@ -897,14 +899,14 @@
          r
          (get (js->clj r) "txid")))))
 
-  (<encrypt-fnames [_ fnames]
+  (<encrypt-fnames [_ _graph-uuid fnames]
     (go
       (let [r (<! (p->c (.encryptFnames mobile-util/file-sync
                                         (clj->js {:filePaths fnames}))))]
         (if (instance? ExceptionInfo r)
           (.-cause r)
           (get (js->clj r) "value")))))
-  (<decrypt-fnames [_ fnames]
+  (<decrypt-fnames [_ _graph-uuid fnames]
     (go (let [r (<! (p->c (.decryptFnames mobile-util/file-sync
                                           (clj->js {:filePaths fnames}))))]
           (if (instance? ExceptionInfo r)
@@ -1103,9 +1105,9 @@
                        (recur next-continuation-token)))))))]
         (if (instance? ExceptionInfo exp-r)
           exp-r
-          (let [file-meta-list*          (persistent! file-meta-list)
-                encrypted-path-list*     (persistent! encrypted-path-list)
-                path-list-or-exp (<! (<decrypt-fnames rsapi encrypted-path-list*))]
+          (let [file-meta-list*      (persistent! file-meta-list)
+                encrypted-path-list* (persistent! encrypted-path-list)
+                path-list-or-exp     (<! (<decrypt-fnames rsapi graph-uuid encrypted-path-list*))]
             (if (instance? ExceptionInfo path-list-or-exp)
               path-list-or-exp
               (let [encrypted-path->path-map (zipmap encrypted-path-list* path-list-or-exp)]
@@ -1122,12 +1124,12 @@
   (<get-remote-files-meta [this graph-uuid filepaths]
     {:pre [(coll? filepaths)]}
     (go
-      (let [encrypted-paths* (<! (<encrypt-fnames rsapi filepaths))
+      (let [encrypted-paths* (<! (<encrypt-fnames rsapi graph-uuid filepaths))
             r                (<! (.<request this "get_files_meta" {:GraphUUID graph-uuid :Files encrypted-paths*}))]
         (if (instance? ExceptionInfo r)
           r
           (let [encrypted-paths (mapv :FilePath r)
-                paths-or-exp    (<! (<decrypt-fnames rsapi encrypted-paths))]
+                paths-or-exp    (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths))]
             (if (instance? ExceptionInfo paths-or-exp)
               paths-or-exp
               (let [encrypted-path->path-map (zipmap encrypted-paths paths-or-exp)]
@@ -1150,12 +1152,29 @@
 
   (<get-remote-file-versions [this graph-uuid filepath]
     (go
-      (let [encrypted-path (first (<! (<encrypt-fnames rsapi [filepath])))]
+      (let [encrypted-path (first (<! (<encrypt-fnames rsapi graph-uuid [filepath])))]
         (<! (.<request this "get_file_version_list" {:GraphUUID graph-uuid :File encrypted-path})))))
 
   (<list-remote-graphs [this]
     (.<request this "list_graphs"))
 
+  (<get-deletion-logs [this graph-uuid from-txid]
+    (go
+      (let [r (<! (.<request this "get_deletion_log" {:GraphUUID graph-uuid :FromTXId from-txid}))]
+        (if (instance? ExceptionInfo r)
+          r
+          (let [txns-with-encrypted-paths (mapv #(update % :path remove-user-graph-uuid-prefix) (:Transactions r))
+                encrypted-paths           (mapv :path txns-with-encrypted-paths)
+                encrypted-path->path-map
+                (zipmap
+                 encrypted-paths
+                 (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
+                txns
+                (mapv
+                 (fn [txn] (update txn :path #(get encrypted-path->path-map %)))
+                 txns-with-encrypted-paths)]
+            txns)))))
+
   (<get-diff [this graph-uuid from-txid]
     ;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
     (go
@@ -1186,7 +1205,7 @@
                 encrypted-path->path-map
                 (zipmap
                  encrypted-paths
-                 (<! (<decrypt-fnames rsapi encrypted-paths)))
+                 (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
                 txns
                 (mapv
                  (fn [txn]
@@ -1315,11 +1334,11 @@
                   [@graphs-txid local-txid remote-txid])))))
 
 (defn- get-local-files-checksum
-  [base-path relative-paths]
+  [graph-uuid base-path relative-paths]
   (go
     (into {}
           (map (juxt #(.-path ^FileMetadata %) #(.-etag ^FileMetadata %)))
-          (<! (<get-local-files-meta rsapi "" base-path relative-paths)))))
+          (<! (<get-local-files-meta rsapi graph-uuid base-path relative-paths)))))
 
 (declare sync-state--add-current-local->remote-files
          sync-state--add-current-remote->local-files
@@ -1387,7 +1406,8 @@
                                      (remove nil?))]
 
         (doseq [relative-p (map relative-path filetxns)]
-          (when-some [relative-p* (<! (<case-different-local-file-exist? rsapi base-path relative-p))]
+          (when-some [relative-p*
+                      (<! (<case-different-local-file-exist? graph-uuid rsapi base-path relative-p))]
             (let [recent-remote->local-file-item {:remote->local-type :delete
                                                   :checksum nil
                                                   :path relative-p*}]
@@ -1414,7 +1434,7 @@
       (.-deleted? (first filetxns))
       (let [filetxn (first filetxns)]
         (assert (= 1 (count filetxns)))
-        (if (<! (<local-file-not-exist? rsapi base-path (relative-path filetxn)))
+        (if (<! (<local-file-not-exist? graph-uuid rsapi base-path (relative-path filetxn)))
           ;; not exist, ignore
           true
           (let [r (<! (<delete-local-files rsapi graph-uuid base-path [(relative-path filetxn)]))]
@@ -1528,7 +1548,7 @@
 
 
 (defn- <file-change-event=>recent-remote->local-file-item
-  [^FileChangeEvent e]
+  [graph-uuid ^FileChangeEvent e]
   (go
     (let [tp (case (.-type e)
                ("add" "change") :update
@@ -1536,7 +1556,7 @@
           path (relative-path e)]
       {:remote->local-type tp
        :checksum (if (= tp :delete) nil
-                     (val (first (<! (get-local-files-checksum (.-dir e) [path])))))
+                     (val (first (<! (get-local-files-checksum graph-uuid (.-dir e) [path])))))
        :path path})))
 
 (defn- distinct-file-change-events-xf
@@ -1571,6 +1591,7 @@
    (map #(partition-all n %))
    cat))
 
+(declare sync-state--valid-to-accept-filewatcher-event?)
 (defonce local-changes-chan (chan (async/dropping-buffer 1000)))
 (defn file-watch-handler
   "file-watcher callback"
@@ -1578,12 +1599,13 @@
   (when-let [current-graph (state/get-current-repo)]
     (when (string/ends-with? current-graph dir)
       (let [sync-state (state/get-file-sync-state current-graph)]
-        (when (and sync-state (not (sync-state--stopped? sync-state)))
+        (when (and sync-state (sync-state--valid-to-accept-filewatcher-event? sync-state))
           (when (or (:mtime stat) (= type "unlink"))
             (go
               (let [path (remove-dir-prefix dir path)
                     files-meta (and (not= "unlink" type)
-                                    (<! (<get-local-files-meta rsapi "" dir [path])))
+                                    (<! (<get-local-files-meta
+                                         rsapi (:current-syncing-graph-uuid sync-state) dir [path])))
                     checksum (and (coll? files-meta) (some-> files-meta first :etag))]
                 (>! local-changes-chan (->FileChangeEvent type dir path stat checksum))))))))))
 
@@ -1834,7 +1856,7 @@
   (let [{:keys [private-key public-key]} (get @pwd-map graph-uuid)]
     (assert (and private-key public-key) (pr-str :private-key private-key :public-key public-key
                                                  :pwd-map @pwd-map))
-    (<set-env rsapi prod? private-key public-key graph-uuid)))
+    (<set-env rsapi graph-uuid prod? private-key public-key)))
 
 (defn- <ensure-set-env&keys
   [graph-uuid *stopped?]
@@ -2018,6 +2040,13 @@
   {:pre [(s/valid? ::sync-state sync-state)]}
   (= ::stop (:state sync-state)))
 
+(defn sync-state--valid-to-accept-filewatcher-event?
+  [sync-state]
+  {:pre [(s/valid? ::sync-state sync-state)]}
+  (contains? #{::idle ::local->remote ::remote->local ::local->remote-full-sync ::remote->local-full-sync}
+             (:state sync-state)))
+
+
 ;;; ### remote->local syncer & local->remote syncer
 
 (defprotocol IRemote->LocalSync
@@ -2140,18 +2169,18 @@
       (case (.-type e)
         "unlink"
         ;; keep this e when it's not found
-        (<! (<local-file-not-exist? rsapi basepath r-path))
+        (<! (<local-file-not-exist? graph-uuid rsapi basepath r-path))
 
         ("add" "change")
         ;; 1. local file exists
         ;; 2. compare with remote file, and changed
-        (and (not (<! (<local-file-not-exist? rsapi basepath r-path)))
+        (and (not (<! (<local-file-not-exist? graph-uuid rsapi basepath r-path)))
              (<! (<file-changed? graph-uuid r-path basepath)))))))
 
 (defn- <filter-checksum-not-consistent
   "filter out FileChangeEvents checksum changed,
   compare checksum in FileChangeEvent and checksum calculated now"
-  [es]
+  [graph-uuid es]
   {:pre [(or (nil? es) (coll? es))
          (every? #(instance? FileChangeEvent %) es)]}
   (go
@@ -2159,7 +2188,8 @@
       (if (= "unlink" (.-type ^FileChangeEvent (first es)))
         es
         (let [base-path            (.-dir (first es))
-              files-meta           (<! (<get-local-files-meta rsapi "" base-path (mapv relative-path es)))
+              files-meta           (<! (<get-local-files-meta
+                                        rsapi graph-uuid base-path (mapv relative-path es)))
               current-checksum-map (when (coll? files-meta) (into {} (mapv (juxt :path :etag) files-meta)))
               origin-checksum-map  (into {} (mapv (juxt relative-path #(.-checksum ^FileChangeEvent %)) es))
               origin-map           (into {} (mapv (juxt relative-path identity) es))]
@@ -2189,6 +2219,21 @@
          (every? #(instance? FileChangeEvent %) es)]}
   (filterv filter-too-huge-files-aux es))
 
+(defn- filter-local-files-in-deletion-logs
+  [local-all-files-meta deletion-logs]
+  (let [deletion-logs-map (into {} (map (juxt :path identity)) deletion-logs)
+        *keep             (transient #{})
+        *delete           (transient #{})]
+    (doseq [f local-all-files-meta]
+      (let [epoch-long (some-> (get deletion-logs-map (:path f))
+                               :epoch
+                               (* 1000))]
+        (if (and epoch-long (> epoch-long (:last-modified f)))
+          (conj! *delete f)
+          (conj! *keep f))))
+    {:keep   (persistent! *keep)
+     :delete (persistent! *delete)}))
+
 
 (defrecord ^:large-vars/cleanup-todo
     Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi
@@ -2206,10 +2251,11 @@
                    true)
                  (or (string/starts-with? (.-dir e) base-path)
                      (string/starts-with? (str "file://" (.-dir e)) base-path)) ; valid path prefix
-                 (not (ignored? e)) ;not ignored
+                 (not (ignored? e))     ;not ignored
                  ;; download files will also trigger file-change-events, ignore them
                  (let [r (not (contains? (:recent-remote->local-files @*sync-state)
-                                         (<! (<file-change-event=>recent-remote->local-file-item e))))]
+                                         (<! (<file-change-event=>recent-remote->local-file-item
+                                              graph-uuid e))))]
                    (when (and (true? r)
                               (seq (:recent-remote->local-files @*sync-state)))
                      (println :debug (:recent-remote->local-files @*sync-state) e))
@@ -2252,12 +2298,12 @@
     (<sync-local->remote! [_ es]
       (if (empty? es)
         (go {:succ true})
-        (let [type          (.-type ^FileChangeEvent (first es))
-              es->paths-xf  (comp
-                             (map #(relative-path %))
-                             (remove ignored?))]
+        (let [type         (.-type ^FileChangeEvent (first es))
+              es->paths-xf (comp
+                            (map #(relative-path %))
+                            (remove ignored?))]
           (go
-            (let [es*   (<! (<filter-checksum-not-consistent es))
+            (let [es*   (<! (<filter-checksum-not-consistent graph-uuid es))
                   _     (when (not= (count es*) (count es))
                           (println :debug :filter-checksum-changed
                                    (mapv relative-path (set/difference (set es) (set es*)))))
@@ -2315,7 +2361,9 @@
       (go
         (let [remote-all-files-meta-c      (<get-remote-all-files-meta remoteapi graph-uuid)
               local-all-files-meta-c       (<get-local-all-files-meta rsapi graph-uuid base-path)
-              remote-all-files-meta-or-exp (<! remote-all-files-meta-c)]
+              deletion-logs-c              (<get-deletion-logs remoteapi graph-uuid @*txid)
+              remote-all-files-meta-or-exp (<! remote-all-files-meta-c)
+              deletion-logs                (<! deletion-logs-c)]
           (if (or (storage-exceed-limit? remote-all-files-meta-or-exp)
                   (sync-stop-when-api-flying? remote-all-files-meta-or-exp)
                   (decrypt-exp? remote-all-files-meta-or-exp))
@@ -2326,6 +2374,8 @@
                 {:stop true})
             (let [remote-all-files-meta remote-all-files-meta-or-exp
                   local-all-files-meta  (<! local-all-files-meta-c)
+                  {local-all-files-meta :keep delete-local-files :delete}
+                  (filter-local-files-in-deletion-logs local-all-files-meta deletion-logs)
                   diff-local-files      (diff-file-metadata-sets local-all-files-meta remote-all-files-meta)
                   change-events
                   (sequence
@@ -2341,7 +2391,25 @@
                    (partition-file-change-events 10)
                    (distinct-file-change-events change-events))]
               (println "[full-sync(local->remote)]"
-                       (count (flatten change-events-partitions)) "files need to sync")
+                       (count (flatten change-events-partitions)) "files need to sync and"
+                       (count delete-local-files) "local files need to delete")
+              ;; 1. delete local files
+              (loop [[f & fs] delete-local-files]
+                (when f
+                  (let [relative-p (relative-path f)]
+                    (when-not (<! (<local-file-not-exist? graph-uuid rsapi base-path relative-p))
+                      (let [fake-recent-remote->local-file-item {:remote->local-type :delete
+                                                                 :checksum nil
+                                                                 :path relative-p}]
+                        (swap! *sync-state sync-state--add-recent-remote->local-files
+                               [fake-recent-remote->local-file-item])
+                        (<! (<delete-local-files rsapi graph-uuid base-path [(relative-path f)]))
+                        (go (<! (timeout 5000))
+                            (swap! *sync-state sync-state--remove-recent-remote->local-files
+                                   [fake-recent-remote->local-file-item])))))
+                  (recur fs)))
+
+              ;; 2. upload local files
               (loop [es-partitions change-events-partitions]
                 (if @*stopped
                   {:stop true}
@@ -2779,7 +2847,6 @@
     (go
       (when (and (graph-sync-off? repo) @network-online-cursor)
         (<! (p->c (persist-var/-load graphs-txid)))
-
         (let [[user-uuid graph-uuid txid] @graphs-txid]
           (when (and user-uuid graph-uuid txid
                      (user/logged-in?)

+ 9 - 4
src/main/frontend/handler/events.cljs

@@ -106,7 +106,14 @@
       (route-handler/redirect! {:to :import :query-params {:from "picker"}})
       (route-handler/redirect-to-home!)))
   (repo-handler/refresh-repos!)
-  (file-sync-stop!))
+  (file-sync-restart!))
+
+(defmethod handle :graph/unlinked [_]
+  (repo-handler/refresh-repos!)
+  (file-sync-restart!))
+
+(defmethod handle :graph/refresh [_]
+  (repo-handler/refresh-repos!))
 
 (defn- graph-switch
   ([graph]
@@ -568,9 +575,7 @@
                      (state/close-modal!)
                      (repo-handler/re-index!
                       nfs-handler/rebuild-index!
-                      #(do
-                         (page-handler/create-today-journal!)
-                         (file-sync-restart!)))))]])))
+                      page-handler/create-today-journal!)))]])))
 
 ;; encryption
 (defmethod handle :modal/encryption-setup-dialog [[_ repo-url close-fn]]

+ 3 - 10
src/main/frontend/handler/file_sync.cljs

@@ -93,8 +93,7 @@
   (let [repo (state/get-current-repo)
         user-uuid (user/user-uuid)]
     (sync/update-graphs-txid! 0 graph-uuid user-uuid repo)
-    (swap! refresh-file-sync-component not)
-    (state/pub-event! [:graph/switch repo {:persist? false}])))
+    (swap! refresh-file-sync-component not)))
 
 (defn download-version-file
   ([graph-uuid file-uuid version-uuid]
@@ -165,15 +164,9 @@
             all-version-list))))))
 
 
-(def *wait-syncing-graph (atom nil))
-
-(defn set-wait-syncing-graph
-  [graph]
-  (reset! *wait-syncing-graph graph))
-
 (defn init-remote-graph
-  [local]
-  (when-let [graph (and local @*wait-syncing-graph)]
+  [local graph]
+  (when (and local graph)
     (notification/show!
      (str "Start syncing the remote graph "
           (:GraphName graph)

+ 6 - 3
src/main/frontend/handler/repo.cljs

@@ -525,9 +525,12 @@
 
 (defn refresh-repos!
   []
-  (p/let [repos (get-repos)]
-    (state/set-repos! repos)
-    repos))
+  (p/let [repos (get-repos)
+          repos' (combine-local-&-remote-graphs
+                  repos
+                  (state/get-remote-repos))]
+    (state/set-repos! repos')
+    repos'))
 
 (defn graph-ready!
   "Call electron that the graph is loaded."

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

@@ -7,7 +7,6 @@
             [cljs-time.core :as t]
             [cljs-time.coerce :as tc]
             [cljs-http.client :as http]
-            [lambdaisland.glogi :as log]
             [cljs.core.async :as async :refer [go go-loop <! timeout]]))
 
 (defn set-preferred-format!
@@ -68,11 +67,11 @@
 
 (defn- set-token-to-localstorage!
   ([id-token access-token]
-   (log/info :debug "set-token-to-localstorage!")
+   (prn :debug "set-token-to-localstorage!")
    (js/localStorage.setItem "id-token" id-token)
    (js/localStorage.setItem "access-token" access-token))
   ([id-token access-token refresh-token]
-   (log/info :debug "set-token-to-localstorage!")
+   (prn :debug "set-token-to-localstorage!")
    (js/localStorage.setItem "id-token" id-token)
    (js/localStorage.setItem "access-token" access-token)
    (js/localStorage.setItem "refresh-token" refresh-token)))

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

@@ -188,7 +188,7 @@
                           (p/let [files (map #(dissoc % :file/file) result)
                                   graphs-txid-meta (util-fs/read-graphs-txid-info dir-name)
                                   graph-uuid (and (vector? graphs-txid-meta) (second graphs-txid-meta))]
-                            (if-let [exists-graph (state/get-sync-graph-by-uuid graph-uuid)]
+                            (if-let [exists-graph (state/get-sync-graph-by-id graph-uuid)]
                               (state/pub-event!
                                [:notification/show
                                 {:content (str "This graph already exists in \"" (:root exists-graph) "\"")

+ 1 - 1
src/main/frontend/mobile/footer.cljs

@@ -17,7 +17,7 @@
                      (command-handler))}
    (if (= icon "player-stop")
      svg/circle-stop
-     (ui/icon icon {:style {:fontSize ui/icon-size}}))])
+     (ui/icon icon {:size ui/icon-size}))])
 
 (defn seconds->minutes:seconds
   [seconds]

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů