浏览代码

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

Peng Xiao 3 年之前
父节点
当前提交
4d86f4f53f
共有 84 个文件被更改,包括 1584 次插入1078 次删除
  1. 23 4
      .clj-kondo/config.edn
  2. 19 15
      .github/workflows/build-desktop-release.yml
  3. 1 1
      deps.edn
  4. 1 1
      deps/db/deps.edn
  5. 1 1
      deps/graph-parser/deps.edn
  6. 12 15
      deps/graph-parser/src/logseq/graph_parser.cljs
  7. 1 2
      deps/graph-parser/src/logseq/graph_parser/cli.cljs
  8. 10 10
      e2e-tests/accessibility.spec.ts
  9. 1 1
      package.json
  10. 1 1
      resources/css/common.css
  11. 0 2
      resources/css/tooltip.css
  12. 4 1
      resources/forge.config.js
  13. 4 0
      resources/js/preload.js
  14. 10 7
      src/electron/electron/core.cljs
  15. 2 2
      src/electron/electron/exceptions.cljs
  16. 105 60
      src/electron/electron/fs_watcher.cljs
  17. 23 16
      src/electron/electron/handler.cljs
  18. 1 1
      src/electron/electron/search.cljs
  19. 8 7
      src/electron/electron/updater.cljs
  20. 1 1
      src/electron/electron/utils.cljs
  21. 3 1
      src/electron/electron/window.cljs
  22. 1 1
      src/main/electron/listener.cljs
  23. 10 15
      src/main/frontend/components/block.cljs
  24. 15 0
      src/main/frontend/components/block.css
  25. 2 2
      src/main/frontend/components/content.cljs
  26. 2 7
      src/main/frontend/components/editor.cljs
  27. 3 3
      src/main/frontend/components/encryption.cljs
  28. 8 8
      src/main/frontend/components/file_sync.cljs
  29. 8 4
      src/main/frontend/components/header.cljs
  30. 2 2
      src/main/frontend/components/page.cljs
  31. 1 1
      src/main/frontend/components/page_menu.cljs
  32. 2 2
      src/main/frontend/components/plugins.cljs
  33. 6 6
      src/main/frontend/components/plugins_settings.cljs
  34. 53 16
      src/main/frontend/components/right_sidebar.cljs
  35. 12 2
      src/main/frontend/components/settings.cljs
  36. 1 1
      src/main/frontend/components/shortcut.cljs
  37. 10 8
      src/main/frontend/components/sidebar.cljs
  38. 23 12
      src/main/frontend/components/sidebar.css
  39. 11 0
      src/main/frontend/components/theme.css
  40. 7 3
      src/main/frontend/config.cljs
  41. 3 2
      src/main/frontend/context/i18n.cljs
  42. 3 6
      src/main/frontend/db.cljs
  43. 4 3
      src/main/frontend/db/query_react.cljs
  44. 90 39
      src/main/frontend/dicts.cljc
  45. 6 4
      src/main/frontend/extensions/latex.cljs
  46. 1 1
      src/main/frontend/extensions/zotero/setting.cljs
  47. 2 2
      src/main/frontend/fs.cljs
  48. 1 1
      src/main/frontend/fs/bfs.cljs
  49. 7 6
      src/main/frontend/fs/capacitor_fs.cljs
  50. 1 1
      src/main/frontend/fs/nfs.cljs
  51. 2 2
      src/main/frontend/fs/node.cljs
  52. 1 1
      src/main/frontend/fs/protocol.cljs
  53. 59 49
      src/main/frontend/fs/sync.cljs
  54. 8 6
      src/main/frontend/fs/watcher_handler.cljs
  55. 82 85
      src/main/frontend/handler.cljs
  56. 1 32
      src/main/frontend/handler/common.cljs
  57. 79 0
      src/main/frontend/handler/common/file.cljs
  58. 30 3
      src/main/frontend/handler/config.cljs
  59. 26 19
      src/main/frontend/handler/events.cljs
  60. 51 119
      src/main/frontend/handler/file.cljs
  61. 65 0
      src/main/frontend/handler/global_config.cljs
  62. 3 3
      src/main/frontend/handler/page.cljs
  63. 65 49
      src/main/frontend/handler/repo.cljs
  64. 63 0
      src/main/frontend/handler/repo_config.cljs
  65. 3 3
      src/main/frontend/handler/ui.cljs
  66. 14 12
      src/main/frontend/handler/user.cljs
  67. 35 25
      src/main/frontend/handler/web/nfs.cljs
  68. 2 2
      src/main/frontend/modules/instrumentation/posthog.cljs
  69. 1 1
      src/main/frontend/modules/shortcut/config.cljs
  70. 4 4
      src/main/frontend/modules/shortcut/core.cljs
  71. 5 4
      src/main/frontend/modules/shortcut/data_helper.cljs
  72. 32 20
      src/main/frontend/modules/shortcut/dicts.cljc
  73. 3 2
      src/main/frontend/search.cljs
  74. 1 3
      src/main/frontend/spec/storage.cljc
  75. 284 261
      src/main/frontend/state.cljs
  76. 32 0
      src/main/frontend/state_test.cljs
  77. 16 9
      src/main/frontend/ui.cljs
  78. 5 1
      src/main/frontend/ui.css
  79. 2 2
      src/main/frontend/util/cursor.cljs
  80. 25 25
      src/main/frontend/util/fs.cljs
  81. 23 18
      src/main/frontend/util/persist_var.cljs
  82. 3 3
      src/main/logseq/api.cljs
  83. 12 0
      templates/global-config.edn
  84. 21 8
      yarn.lock

+ 23 - 4
.clj-kondo/config.edn

@@ -1,4 +1,13 @@
-{:linters
+{:ns-groups [{:pattern "frontend.components.*" :name all-components}]
+
+ :config-in-ns
+ ;; :used-underscored-binding is turned off for components because of false positive
+ ;; for rum/defcs and _state.
+ {all-components {:linters {:used-underscored-binding {:level :off}}}
+  ;; false positive with match/match and _
+  frontend.handler.paste {:linters {:used-underscored-binding {:level :off}}}}
+
+ :linters
  {:unresolved-symbol {:exclude [goog.DEBUG
                                 goog.string.unescapeEntities
                                 ;; TODO:lint: Fix when fixing all type hints
@@ -25,6 +34,13 @@
              frontend.format.mldoc mldoc
              frontend.format.block block
              frontend.handler.extract extract
+             frontend.handler.common common-handler
+             frontend.handler.common.file file-common-handler
+             frontend.handler.config config-handler
+             frontend.handler.global-config global-config-handler
+             frontend.handler.repo-config repo-config-handler
+             frontend.mobile.util mobile-util
+             frontend.state state
              logseq.graph-parser graph-parser
              logseq.graph-parser.text text
              logseq.graph-parser.block gp-block
@@ -32,12 +48,15 @@
              logseq.graph-parser.util gp-util
              logseq.graph-parser.property gp-property
              logseq.graph-parser.config gp-config
-             logseq.graph-parser.date-time-util date-time-util
              logseq.graph-parser.util.page-ref page-ref
-             logseq.graph-parser.util.block-ref block-ref}}}
+             logseq.graph-parser.util.block-ref block-ref
+             logseq.graph-parser.date-time-util date-time-util}}
+
+  :namespace-name-mismatch {:level :warning}
+  :used-underscored-binding {:level :warning}}
 
  :hooks {:analyze-call {rum.core/defc hooks.rum/defc
-                        rum.core/defcs hooks.rum/defcs}}
+                         rum.core/defcs hooks.rum/defcs}}
  :lint-as {promesa.core/let clojure.core/let
            promesa.core/loop clojure.core/loop
            promesa.core/recur clojure.core/recur

+ 19 - 15
.github/workflows/build-desktop-release.yml

@@ -248,29 +248,26 @@ jobs:
         run: yarn run postinstall
         working-directory: ./static/node_modules/dugite/
 
+      - name: Prepare Code Sign
+        if: ${{ github.repository == 'logseq/logseq' }}
+        run: |
+          [IO.File]::WriteAllBytes($(Get-Location).Path + "\codesign.pfx", [Convert]::FromBase64String($env:CERTIFICATE))
+        env:
+          CERTIFICATE: ${{ secrets.CODE_SIGN_CERTIFICATE }}
+
       - name: Build/Release Electron app
         run: yarn electron:make
         working-directory: ./static
         env:
-          CSC_LINK: ${{ secrets.CSC_LINK }}
-          CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
+          CODE_SIGN_CERTIFICATE_FILE: ../codesign.pfx
+          CODE_SIGN_CERTIFICATE_PASSWORD: ${{ secrets.CODE_SIGN_CERTIFICATE_PASSWORD }}
 
       - name: Save Artifact
         run: |
           mkdir builds
-          mv static\out\make\squirrel.windows\x64\*.exe builds\Logseq-win-x64-${{ steps.ref.outputs.version }}.exe
-
-      - name: Code Sign the Installer
-        uses: andelf/code-sign-action@master
-        if: ${{ github.repository == 'logseq/logseq' }}
-        with:
-          certificate: ${{ secrets.CODE_SIGN_CERTIFICATE }}
-          password: ${{ secrets.CODE_SIGN_CERTIFICATE_PASSWORD }}
-          certificatesha1: ${{ secrets.CODE_SIGN_CERTIFICATE_SHA1 }}
-          certificatename: "Logseq, Inc."
-          timestampUrl: http://timestamp.digicert.com
-          folder: "builds"
-          recursive: false
+          mv static\out\make\squirrel.windows\x64\*.exe    builds\Logseq-win-x64-${{ steps.ref.outputs.version }}.exe
+          mv static\out\make\squirrel.windows\x64\*.nupkg  builds\Logseq-win-x64-${{ steps.ref.outputs.version }}-full.nupkg
+          mv static\out\make\squirrel.windows\x64\RELEASES builds\RELEASES
 
       - name: Upload Artifact
         uses: actions/upload-artifact@v2
@@ -546,12 +543,17 @@ jobs:
           pkgver=$(cat VERSION)
           echo ::set-output name=version::$pkgver
 
+      - name: Fix .nupkg name in RELEASES file
+        run: |
+          sed -i "s/Logseq-.*.nupkg/Logseq-win-x64-${{ steps.ref.outputs.version }}-full.nupkg/g" RELEASES
+
       - name: Generate SHA256 checksums
         run: |
           sha256sum *-darwin-* > SHA256SUMS.txt
           sha256sum *-win-* >> SHA256SUMS.txt
           sha256sum *-linux-* >> SHA256SUMS.txt
           sha256sum *.apk >> SHA256SUMS.txt
+          sha256sum RELEASES >> SHA256SUMS.txt
           cat SHA256SUMS.txt
 
       - name: Create Release Draft
@@ -570,5 +572,7 @@ jobs:
             ./*.zip
             ./*.dmg
             ./*.exe
+            ./*.nupkg
+            ./RELEASES
             ./*.AppImage
             ./*.apk

+ 1 - 1
deps.edn

@@ -47,5 +47,5 @@
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 
            ;; Use :replace-deps for tools. See https://github.com/clj-kondo/clj-kondo/issues/1536#issuecomment-1013006889
-           :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.05.31"}}
+           :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.09.08"}}
                        :main-opts  ["-m" "clj-kondo.main"]}}}

+ 1 - 1
deps/db/deps.edn

@@ -3,5 +3,5 @@
  {datascript/datascript {:mvn/version "1.3.8"}}
  :aliases
  {:clj-kondo
-  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.05.31"}}
+  {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.09.08"}}
    :main-opts  ["-m" "clj-kondo.main"]}}}

+ 1 - 1
deps/graph-parser/deps.edn

@@ -20,5 +20,5 @@
                       org.clojure/clojurescript {:mvn/version "1.11.54"}}
          :main-opts ["-m" "cljs-test-runner.main"]}
 
-  :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.05.31"}}
+  :clj-kondo {:replace-deps {clj-kondo/clj-kondo {:mvn/version "2022.09.08"}}
               :main-opts  ["-m" "clj-kondo.main"]}}}

+ 12 - 15
deps/graph-parser/src/logseq/graph_parser.cljs

@@ -8,20 +8,13 @@
             [clojure.string :as string]
             [clojure.set :as set]))
 
-(defn- db-set-file-content!
-  "Modified copy of frontend.db.model/db-set-file-content!"
-  [conn path content]
-  (let [tx-data {:file/path path
-                 :file/content content}]
-    (d/transact! conn [tx-data] {:skip-refresh? true})))
-
 (defn parse-file
   "Parse file and save parsed data to the given db. Main parse fn used by logseq app"
-  [conn file content {:keys [new? delete-blocks-fn extract-options]
+  [conn file content {:keys [new? delete-blocks-fn extract-options skip-db-transact?]
                       :or {new? true
-                           delete-blocks-fn (constantly [])}
+                           delete-blocks-fn (constantly [])
+                           skip-db-transact? false}
                       :as options}]
-  (db-set-file-content! conn file content)
   (let [format (gp-util/get-format file)
         file-content [{:file/path file}]
         {:keys [tx ast]}
@@ -53,14 +46,18 @@
               pages (extract/with-ref-pages pages blocks)
               pages-index (map #(select-keys % [:block/name]) pages)]
                ;; does order matter?
-          {:tx (concat file-content pages-index delete-blocks pages block-ids blocks)
-           :ast ast})
-        tx (concat tx [(cond-> {:file/path file}
-                         new?
+               {:tx (concat file-content pages-index delete-blocks pages block-ids blocks)
+                :ast ast})
+             {:tx file-content})
+        tx (concat tx [(cond-> {:file/path file
+                                :file/content content}
+                               new?
                                ;; TODO: use file system timestamp?
                          (assoc :file/created-at (date-time-util/time-ms)))])
         tx' (gp-util/remove-nils tx)
-        result (d/transact! conn tx' (select-keys options [:new-graph? :from-disk?]))]
+        result (if skip-db-transact?
+                 tx'
+                 (d/transact! conn tx' (select-keys options [:new-graph? :from-disk?])))]
     {:tx result
      :ast ast}))
 

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

@@ -34,8 +34,7 @@ TODO: Fail fast when process exits 1"
     (mapv #(assoc % :file/content (slurp (:file/path %))) files)))
 
 (defn- read-config
-  "Commandline version of frontend.handler.common/read-config without graceful
-  handling of broken config. Config is assumed to be at $dir/logseq/config.edn "
+  "Reads repo-specific config from logseq/config.edn"
   [dir]
   (let [config-file (str dir "/" gp-config/app-name "/config.edn")]
     (if (fs/existsSync config-file)

+ 10 - 10
e2e-tests/accessibility.spec.ts

@@ -1,15 +1,15 @@
-import { injectAxe, checkA11y } from 'axe-playwright'
 import { test } from './fixtures'
 import { createRandomPage } from './utils'
+import { expect } from '@playwright/test'
+import AxeBuilder from '@axe-core/playwright'
 
-
-test('check a11y for the whole page', async ({ page }) => {
-    await page.waitForTimeout(2000) // wait for everything be ready
-    await injectAxe(page)
-    await page.waitForTimeout(2000) // wait for everything be ready
+test('should not have any automatically detectable accessibility issues', async ({ page }) => {
     await createRandomPage(page)
-    await page.waitForTimeout(2000) // wait for everything be ready
-    await checkA11y(page, null, {
-        detailedReport: true,
-    })
+    await page.waitForTimeout(2000)
+    const accessibilityScanResults = await new AxeBuilder({ page })
+        .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
+        .setLegacyMode()
+        .analyze()
+
+    expect(accessibilityScanResults.violations).toEqual([]);
 })

+ 1 - 1
package.json

@@ -4,11 +4,11 @@
     "private": true,
     "main": "static/electron.js",
     "devDependencies": {
+        "@axe-core/playwright": "^4.4.4",
         "@capacitor/cli": "3.2.2",
         "@playwright/test": "^1.24.2",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
-        "axe-playwright": "^1.1.11",
         "cross-env": "^7.0.3",
         "cssnano": "^4.1.10",
         "del": "^6.0.0",

+ 1 - 1
resources/css/common.css

@@ -3,7 +3,7 @@
   --ls-tag-text-hover-opacity: 1;
   --ls-page-text-size: 1em;
   --ls-page-title-size: 36px;
-  --ls-main-content-max-width: 810px;
+  --ls-main-content-max-width: 1200px;
   --ls-main-content-max-width-wide: 100%;
   --ls-font-family: Inter;
   --ls-scrollbar-width: 6px;

+ 0 - 2
resources/css/tooltip.css

@@ -588,7 +588,6 @@
   color: var(--ls-primary-text-color);
   border-radius: 4px;
   text-align: center;
-  will-change: transform;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
   background-color: var(--ls-quaternary-background-color);
@@ -625,7 +624,6 @@
 
 .tippy-tooltip [x-circle] {
   position: absolute;
-  will-change: transform;
   background-color: var(--ls-quaternary-background-color);
   border-radius: 50%;
   width: 130%;

+ 4 - 1
resources/forge.config.js

@@ -30,7 +30,10 @@ module.exports = {
       'config': {
         'name': 'Logseq',
         'setupIcon': './icons/logseq.ico',
-        'loadingGif': './icons/installing.gif'
+        'loadingGif': './icons/installing.gif',
+        'certificateFile': process.env.CODE_SIGN_CERTIFICATE_FILE,
+        'certificatePassword': process.env.CODE_SIGN_CERTIFICATE_PASSWORD,
+        "rfc3161TimeStampServer": "http://timestamp.digicert.com"
       }
     },
     {

+ 4 - 0
resources/js/preload.js

@@ -168,5 +168,9 @@ contextBridge.exposeInMainWorld('apis', {
     webFrame.setZoomFactor(factor)
   },
 
+  setZoomLevel (level) {
+    webFrame.setZoomLevel(level)
+  },
+
   isAbsolutePath: path.isAbsolute.bind(path)
 })

+ 10 - 7
src/electron/electron/core.cljs

@@ -106,8 +106,10 @@
                              (-> (. fs copy (path/join assets-from-dir filename) (path/join assets-to-dir filename))
                                  (p/catch
                                   (fn [e]
-                                    (println (str "Failed to copy " (path/join assets-from-dir filename) " to " (path/join assets-to-dir filename)))
-                                    (js/console.error e)))))
+                                    (.error logger "Failed to copy"
+                                            (str {:from (path/join assets-from-dir filename)
+                                                  :to (path/join assets-to-dir filename)})
+                                            e)))))
                            asset-filenames)
 
                           (map
@@ -165,7 +167,7 @@
                  (try
                    (js-invoke app type args)
                    (catch js/Error e
-                     (js/console.error e)))))
+                     (.error logger (str call-app-channel " " e))))))
 
       (.handle call-win-channel
                (fn [^js e type & args]
@@ -173,7 +175,7 @@
                    (try
                      (js-invoke win type args)
                      (catch js/Error e
-                       (js/console.error e)))))))
+                       (.error logger (str call-win-channel " " e))))))))
 
     #(do (clear-win-effects!)
          (.removeHandler ipcMain toggle-win-channel)
@@ -270,11 +272,12 @@
                (win/switch-to-window! window))))
 
       (.on app "window-all-closed" (fn []
+                                     (.debug logger "window-all-closed" "Quiting...")
                                      (try
                                        (fs-watcher/close-watcher!)
                                        (search/close!)
                                        (catch js/Error e
-                                         (js/console.error e)))
+                                         (.error logger "window-all-closed" e)))
                                      (.quit app)))
       (.on app "ready"
            (fn []
@@ -340,9 +343,9 @@
                (.on app "activate" #(when @*win (.show win)))))))))
 
 (defn start []
-  (js/console.log "Main - start")
+  (.debug logger "Main - start")
   (when @*setup-fn (@*setup-fn)))
 
 (defn stop []
-  (js/console.log "Main - stop")
+  (.debug logger "Main - stop")
   (when @*teardown-fn (@*teardown-fn)))

+ 2 - 2
src/electron/electron/exceptions.cljs

@@ -17,9 +17,9 @@
     (show-error-tip "[Main Exception]" msg stack))
 
   ;; for debug log
-  (js/console.error uncaughtExceptionChan e))
+  (.error utils/logger uncaughtExceptionChan (str e)))
 
 (defn setup-exception-listeners!
   []
   (js/process.on uncaughtExceptionChan app-uncaught-handler)
-  #(js/process.off uncaughtExceptionChan app-uncaught-handler))
+  #(js/process.off uncaughtExceptionChan app-uncaught-handler))

+ 105 - 60
src/electron/electron/fs_watcher.cljs

@@ -1,4 +1,7 @@
 (ns electron.fs-watcher
+  "This ns is a wrapper around the chokidar file watcher,
+  https://www.npmjs.com/package/chokidar. File watcher events are sent to the
+  `file-watcher` ipc channel"
   (:require [cljs-bean.core :as bean]
             ["fs" :as fs]
             ["chokidar" :as watcher]
@@ -21,19 +24,25 @@
                         (send file-watcher-chan
                               (bean/->js {:type type :payload payload})))
                     true))
-        wins (window/get-graph-all-windows dir)]
-    (if (contains? #{"unlinkDir" "addDir"} type)
+        wins (if (:global-dir payload)
+               (window/get-all-windows)
+               (window/get-graph-all-windows dir))]
+    (if (or (contains? #{"unlinkDir" "addDir"} type)
+            ;; Only change events to a global dir are emitted to all windows.
+            ;; Add* events are not emitted to all since each client adds
+            ;; files at different times.
+            (and (:global-dir payload) (= "change" type)))
       ;; notify every windows
       (doseq [win wins] (send-fn win))
 
       ;; Should only send to one window; then dbsync will do his job
       ;; If no window is on this graph, just ignore
       (let [sent? (some send-fn wins)]
-        (when-not sent? (prn "unhandled file event will cause uncatched file modifications!.
-                          target:" dir))))))
+        (when-not sent? (.warn utils/logger
+                               (str "unhandled file event will cause uncatched file modifications!. target:" dir)))))))
 
 (defn- publish-file-event!
-  [dir path event]
+  [dir path event options]
   (let [dir-path? (= dir path)
         content (when (and (not= event "unlink")
                            (not dir-path?)
@@ -42,64 +51,100 @@
         stat (when (and (not= event "unlink")
                         (not dir-path?))
                (fs/statSync path))]
-    (send-file-watcher! dir event {:dir (utils/fix-win-path! dir)
-                                   :path (utils/fix-win-path! path)
-                                   :content content
-                                   :stat stat})))
+    (send-file-watcher! dir event (merge {:dir (utils/fix-win-path! dir)
+                                          :path (utils/fix-win-path! path)
+                                          :content content
+                                          :stat stat}
+                                         (select-keys options [:global-dir])))))
+(defn- create-dir-watcher
+  [dir options]
+  (let [watcher-opts (clj->js
+                      {:ignored (fn [path]
+                                  (utils/ignored-path? dir path))
+                       :ignoreInitial false
+                       :ignorePermissionErrors true
+                       :interval polling-interval
+                       :binaryInterval polling-interval
+                       :persistent true
+                       :disableGlobbing true
+                       :usePolling false
+                       :awaitWriteFinish true})
+        dir-watcher (.watch watcher dir watcher-opts)]
+    ;; TODO: batch sender
+    (.on dir-watcher "unlinkDir"
+         (fn [path]
+           (when (= dir path)
+             (publish-file-event! dir dir "unlinkDir" options))))
+    (.on dir-watcher "addDir"
+         (fn [path]
+           (when (= dir path)
+             (publish-file-event! dir dir "addDir" options))))
+    (.on dir-watcher "add"
+         (fn [path]
+           (publish-file-event! dir path "add" options)))
+    (.on dir-watcher "change"
+         (fn [path]
+           (publish-file-event! dir path "change" options)))
+    (.on dir-watcher "unlink"
+         ;; delay 500ms for syncing disks
+         (fn [path]
+           (js/setTimeout #(when (not (fs/existsSync path))
+                             (publish-file-event! dir path "unlink" options))
+                          500)))
+    (.on dir-watcher "error"
+         (fn [path]
+           (.warn utils/logger "Watch error happened: " (str {:path path}))))
 
-(defn watch-dir!
-  "Watch a directory if no such file watcher exists"
-  [dir]
-  (when-not (get @*file-watcher dir)
-    (if (fs/existsSync dir)
-      (let [watcher-opts (clj->js
-                          {:ignored (fn [path]
-                                      (utils/ignored-path? dir path))
-                           :ignoreInitial false
-                           :ignorePermissionErrors true
-                           :interval polling-interval
-                           :binaryInterval polling-interval
-                           :persistent true
-                           :disableGlobbing true
-                           :usePolling false
-                           :awaitWriteFinish true})
-            dir-watcher (.watch watcher dir watcher-opts)
-            watcher-del-f #(.close dir-watcher)]
-        (swap! *file-watcher assoc dir [dir-watcher watcher-del-f])
-        ;; TODO: batch sender
-        (.on dir-watcher "unlinkDir"
-             (fn [path]
-               (when (= dir path)
-                 (publish-file-event! dir dir "unlinkDir"))))
-        (.on dir-watcher "addDir"
-             (fn [path]
-               (when (= dir path)
-                 (publish-file-event! dir dir "addDir"))))
-        (.on dir-watcher "add"
-             (fn [path]
-               (publish-file-event! dir path "add")))
-        (.on dir-watcher "change"
-             (fn [path]
-               (publish-file-event! dir path "change")))
-        (.on dir-watcher "unlink"
-             ;; delay 500ms for syncing disks
-             (fn [path]
-               (js/setTimeout #(when (not (fs/existsSync path))
-                                 (publish-file-event! dir path "unlink"))
-                              500)))
-        (.on dir-watcher "error"
-             (fn [path]
-               (println "Watch error happened: "
-                        {:path path})))
+    dir-watcher))
+
+(defn- seed-client-with-initial-global-dir-data
+  "Ensures that secondary clients initialize their databases efficiently and in
+  the same way as the primary client. This fn achieves this by creating a
+  temporary watcher whose sole purpose is to seed the db and then close  when
+  its done seeding a.k.a. ready event fires."
+  [dir options]
+  (let [dir-watcher (create-dir-watcher dir options)]
+    (.on dir-watcher "ready" (fn []
+                               (.close dir-watcher)))))
+
+(defn- create-and-save-watcher
+  [dir options]
+  (let [dir-watcher (create-dir-watcher dir options)
+        watcher-del-f #(.close dir-watcher)]
+    (swap! *file-watcher assoc dir [dir-watcher watcher-del-f])
+    ;; electron app extends `EventEmitter`
+    ;; TODO check: duplicated with the logic in "window-all-closed" ?
+    (.on app "quit" watcher-del-f)))
 
-        ;; electron app extends `EventEmitter`
-        ;; TODO check: duplicated with the logic in "window-all-closed" ?
-        (.on app "quit" watcher-del-f)
+(defn- watch-global-dir!
+  "Only one watcher exists per global dir so only create the watcher for the
+  primary client. Secondary clients only seed their client database."
+  [dir options]
+  (if (get @*file-watcher dir)
+    (seed-client-with-initial-global-dir-data dir options)
+    (create-and-save-watcher dir options)))
+
+(defn watch-dir!
+  "Watches a directory and emits file events. In addition to file
+  watching, clients rely on watchers to initially seed their database with
+  the file contents of a dir. This is done with the ignoreInitial option
+  set to false, https://github.com/paulmillr/chokidar#path-filtering. The
+  watcher emits addDir and add file events which then seed the client database.
+  This fn has the following options:
 
-        true)
-      ;; retry if the `dir` not exists, which is useful when a graph's folder is
-      ;; back after refreshing the window
-      (js/setTimeout #(watch-dir! dir) 5000))))
+* :global-dir - Boolean that indicates the watched directory is global. This
+  type of directory has different behavior then a normal watcher as it
+  broadcasts its change events to all clients. This option needs to be passed to
+  clients in order for them to identify the correct db"
+  [dir options]
+  (if (:global-dir options)
+    (watch-global-dir! dir options)
+    (when-not (get @*file-watcher dir)
+      (if (fs/existsSync dir)
+        (create-and-save-watcher dir options)
+        ;; retry if the `dir` not exists, which is useful when a graph's folder is
+        ;; back after refreshing the window
+        (js/setTimeout #(watch-dir! dir options) 5000)))))
 
 (defn close-watcher!
   "If no `dir` provided, close all watchers;

+ 23 - 16
src/electron/electron/handler.cljs

@@ -1,4 +1,6 @@
 (ns electron.handler
+  "This ns starts the event handling for the electron main process and defines
+  all the application-specific event types"
   (:require ["electron" :refer [ipcMain dialog app autoUpdater shell]]
             [cljs-bean.core :as bean]
             ["fs" :as fs]
@@ -111,8 +113,7 @@
         (let [backup-path (try
                             (backup-file/backup-file repo :backup-dir path (path/extname path) content)
                             (catch :default e
-                              (println "Backup file failed")
-                              (js/console.dir e)))]
+                              (.error utils/logger (str "Backup file failed: " e))))]
           (utils/send-to-renderer window "notification" {:type "error"
                                                          :payload (str "Write to the file " path
                                                                        " failed, "
@@ -220,8 +221,8 @@
         (when-let [sync-meta (and (not (string/blank? root))
                                   (.toString (.readFileSync fs txid-path)))]
           (reader/read-string sync-meta))))
-    (catch js/Error _e
-      (js/console.debug "[read txid meta] #" root (.-message _e)))))
+    (catch js/Error e
+      (js/console.debug "[read txid meta] #" root (.-message e)))))
 
 (defmethod handle :inflateGraphsInfo [_win [_ graphs]]
   (if (seq graphs)
@@ -304,7 +305,7 @@
         (try
           (fs-extra/removeSync path)
           (catch js/Error e
-            (js/console.error e)))))
+            (.error utils/logger (str "Clear cache: " e))))))
     (utils/send-to-renderer window "redirect" {:payload {:to :home}})))
 
 (defmethod handle :clearCache [window _]
@@ -407,7 +408,7 @@
 
 (def *request-abort-signals (atom {}))
 
-(defmethod handle :httpRequest [_ [_ _req-id opts]]
+(defmethod handle :httpRequest [_ [_ req-id opts]]
   (let [{:keys [url abortable method data returnType headers]} opts]
     (when-let [[method type] (and (not (string/blank? url))
                                   [(keyword (string/upper-case (or method "GET")))
@@ -420,7 +421,7 @@
                                     {:body (js/JSON.stringify (bean/->js data))})
 
                                   (when-let [^js controller (and abortable (AbortController.))]
-                                    (swap! *request-abort-signals assoc _req-id controller)
+                                    (swap! *request-abort-signals assoc req-id controller)
                                     {:signal (.-signal controller)}))))
           (p/then (fn [^js res]
                     (case type
@@ -442,10 +443,10 @@
              (throw e)))
           (p/finally
            (fn []
-             (swap! *request-abort-signals dissoc _req-id)))))))
+             (swap! *request-abort-signals dissoc req-id)))))))
 
-(defmethod handle :httpRequestAbort [_ [_ _req-id]]
-  (when-let [^js controller (get @*request-abort-signals _req-id)]
+(defmethod handle :httpRequestAbort [_ [_ req-id]]
+  (when-let [^js controller (get @*request-abort-signals req-id)]
     (.abort controller)))
 
 (defmethod handle :quitAndInstall []
@@ -471,18 +472,20 @@
         windows (win/get-graph-all-windows dir)]
     (> (count windows) 1)))
 
-(defmethod handle :addDirWatcher [^js _window [_ dir]]
+(defmethod handle :addDirWatcher [^js _window [_ dir options]]
   ;; receive dir path (not repo / graph) from frontend
   ;; Windows on same dir share the same watcher
   ;; Only close file watcher when:
   ;;    1. there is no one window on the same dir
   ;;    2. reset file watcher to resend `add` event on window refreshing
   (when dir
-    (watcher/watch-dir! dir)))
+    (watcher/watch-dir! dir options)
+    nil))
 
 (defmethod handle :unwatchDir [^js _window [_ dir]]
   (when dir
-    (watcher/close-watcher! dir)))
+    (watcher/close-watcher! dir)
+    nil))
 
 (defn open-new-window!
   "Persist db first before calling! Or may break db persistency"
@@ -564,7 +567,7 @@
   (apply rsapi/decrypt-with-passphrase (rest args)))
 
 (defmethod handle :default [args]
-  (println "Error: no ipc handler for: " (bean/->js args)))
+  (.error utils/logger "Error: no ipc handler for: " (bean/->js args)))
 
 (defn broadcast-persist-graph!
   "Receive graph-name (not graph path)
@@ -602,11 +605,15 @@
              (fn [^js event args-js]
                (try
                  (let [message (bean/->clj args-js)]
+                   ;; Be careful with the return values of `handle` defmethods.
+                   ;; Values that are not non-JS objects will cause this
+                   ;; exception -
+                   ;; https://www.electronjs.org/docs/latest/breaking-changes#behavior-changed-sending-non-js-objects-over-ipc-now-throws-an-exception
                    (bean/->js (handle (or (utils/get-win-from-sender event) window) message)))
                  (catch js/Error e
                    (when-not (contains? #{"mkdir" "stat"} (nth args-js 0))
-                     (println "IPC error: " {:event event
-                                             :args args-js}
+                     (.error utils/logger "IPC error: " (str {:event event
+                                                              :args args-js})
                               e))
                    e))))
     #(.removeHandler ipcMain main-channel)))

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

@@ -225,7 +225,7 @@
   (when-let [database (get-db repo)]
     (.close database)
     (let [[db-name db-full-path db-ver-path] (get-db-version-path repo)]
-      (println "Delete search indice: " db-full-path)
+      (.info logger "Delete search indice" (str {:path db-full-path}))
       (fs/unlinkSync db-full-path)
       (fs/unlinkSync db-ver-path)
       (swap! databases dissoc db-name))))

+ 8 - 7
src/electron/electron/updater.cljs

@@ -1,5 +1,5 @@
 (ns electron.updater
-  (:require [electron.utils :refer [mac? prod? open fetch logger *win]]
+  (:require [electron.utils :refer [mac? win32? prod? open fetch logger *win]]
             [frontend.version :refer [version]]
             [clojure.string :as string]
             [promesa.core :as p]
@@ -13,7 +13,7 @@
 
 (def *update-ready-to-install (atom nil))
 (def *update-pending (atom nil))
-(def debug (partial (.-warn logger) "[updater]"))
+(def debug (partial (.-debug logger) "[updater]"))
 
 ;Event: 'error'
 ;Event: 'checking-for-update'
@@ -30,8 +30,7 @@
 
 (defn get-latest-artifact-info
   [repo]
-  (let [;endpoint "https://update.electronjs.org/xyhp915/cljs-todo/darwin-x64/0.0.4"
-        endpoint (str "https://update.electronjs.org/" repo "/" js/process.platform "-" js/process.arch "/" electron-version)]
+  (let [endpoint (str "https://update.electronjs.org/" repo "/" js/process.platform "-" js/process.arch "/" electron-version)]
     (debug "checking" endpoint)
     (p/catch
      (p/let [res (fetch endpoint)
@@ -42,7 +41,7 @@
            (bean/->clj info))
          (throw (js/Error. (str "[" status "] " text)))))
      (fn [e]
-       (js/console.warn "[update server error] " e)
+       (.warn logger "[update server error]" e)
        (throw e)))))
 
 (defn check-for-updates
@@ -128,9 +127,11 @@
            ;; start auto updater
           (do
             (debug "Found remote version" remote-version)
-            (when mac?
+            (when (or mac? win32?)
+              (debug "forward update to autoUpdater")
+              ;; FIXME: It seems that update-electron-app doesn't work on linux
               (when-let [f (js/require "update-electron-app")]
-                (f #js{:notifyUser false})
+                (f #js{:notifyUser false :logger logger})
                 (.once autoUpdater "update-downloaded"
                        new-version-downloaded-cb))))
 

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

@@ -97,7 +97,7 @@
     (when (fs/existsSync path)
       (.toString (fs/readFileSync path)))
     (catch js/Error e
-      (js/console.error e))))
+      (.error logger (str "Read file: " e)))))
 
 (defn get-focused-window
   []

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

@@ -143,7 +143,9 @@
 
       (doto web-contents
         (.on "new-window" new-win-handler)
-        (.on "will-navigate" will-navigate-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")))
 
       (doto win
         (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))

+ 1 - 1
src/main/electron/listener.cljs

@@ -166,7 +166,7 @@
                      ;; Handle open new window in renderer, until the destination graph doesn't rely on setting local storage
                      ;; No db cache persisting ensured. Should be handled by the caller
                      (fn [repo]
-                       (ui-handler/open-new-window! nil repo))))
+                       (ui-handler/open-new-window! repo))))
 
 (defn listen!
   []

+ 10 - 15
src/main/frontend/components/block.cljs

@@ -211,9 +211,9 @@
                         *exist? (::exist? state)]
                     (when (and sync-on? asset-file? (false? @*exist?))
                       (let [sync-state (state/sub [:file-sync/sync-state (state/get-current-repo)])
-                            _downloading-files (:current-remote->local-files sync-state)
-                            contain-url? (and (seq _downloading-files)
-                                              (some #(string/ends-with? src %) _downloading-files))]
+                            downloading-files (:current-remote->local-files sync-state)
+                            contain-url? (and (seq downloading-files)
+                                              (some #(string/ends-with? src %) downloading-files))]
                         (cond
                           (and (not @*loading?) contain-url?)
                           (reset! *loading? true)
@@ -251,8 +251,8 @@
   (let [images (js/document.querySelectorAll ".asset-container img")
         images (to-array images)
         images (if-not (= (count images) 1)
-                 (let [^js _image (.closest (.-target e) ".asset-container")
-                       image (. _image querySelector "img")]
+                 (let [^js image (.closest (.-target e) ".asset-container")
+                       image (. image querySelector "img")]
                    (->> images
                         (sort-by (juxt #(.-y %) #(.-x %)))
                         (split-with (complement #{image}))
@@ -1896,10 +1896,7 @@
                  (not= "nil" marker))
         {:class (str (string/lower-case marker))})
       (when bg-color
-        {:style {:background-color bg-color
-                 :padding-left 6
-                 :padding-right 6
-                 :color "#FFFFFF"}
+        {:style {:background-color bg-color}
          :class "with-bg-color"}))
      (remove-nils
       (concat
@@ -2635,7 +2632,7 @@
         block (if ref?
                 (merge block (db/pull-block (:db/id block)))
                 block)
-        {:block/keys [uuid children pre-block? top? refs heading-level level type format content]} block
+        {:block/keys [uuid children pre-block? top? refs heading-level level format content properties]} block
         config (if navigated? (assoc config :id (str navigating-block)) config)
         block (merge block (block/parse-title-and-body uuid format pre-block? content))
         blocks-container-id (:blocks-container-id config)
@@ -2644,7 +2641,7 @@
         config (if (nil? (:query-result config))
                  (assoc config :query-result (atom nil))
                  config)
-        heading? (or (= type :heading) (and heading-level (<= heading-level 6)))
+        heading? (or (:heading properties) (and heading-level (<= heading-level 6)))
         *control-show? (get state ::control-show?)
         db-collapsed? (util/collapsed? block)
         collapsed? (cond
@@ -2933,8 +2930,7 @@
 
 (defn built-in-custom-query?
   [title]
-  (let [repo (state/get-current-repo)
-        queries (state/sub [:config repo :default-queries :journals])]
+  (let [queries (get-in (state/sub-config) [:default-queries :journals])]
     (when (seq queries)
       (boolean (some #(= % title) (map :title queries))))))
 
@@ -3010,10 +3006,9 @@
   [state config {:keys [title query view collapsed? children? breadcrumb-show? table-view?] :as q}]
   (let [dsl-query? (:dsl-query? config)
         query-atom (:query-atom state)
-        repo (state/get-current-repo)
         query-time (or (react/get-query-time query)
                        (react/get-query-time q))
-        view-fn (if (keyword? view) (state/sub [:config repo :query/views view]) view)
+        view-fn (if (keyword? view) (get-in (state/sub-config) [:query/views view]) view)
         current-block-uuid (or (:block/uuid (:block config))
                                (:block/uuid config))
         current-block (db/entity [:block/uuid current-block-uuid])

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

@@ -304,6 +304,21 @@
   }
 }
 
+.with-bg-color {
+  @apply px-1;
+
+  color: #fff;
+
+  a,
+  .page-reference:not(:hover), {
+    color: #aacece;
+
+    .bracket {
+      color: #aacece;
+    }
+  }
+}
+
 .block-properties {
   margin: 4px 0;
   padding: 4px 8px;

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

@@ -186,7 +186,7 @@
            :on-click (fn [_e]
                        (editor-handler/open-block-in-sidebar! block-id))}
           "Open in sidebar"
-          ["shift" "click"])
+          ["" "click"])
 
          [:hr.menu-separator]
 
@@ -312,7 +312,7 @@
                     block-ref-id
                     :block-ref))}
       "Open in sidebar"
-      ["shift" "click"])
+      ["" "click"])
      (ui/menu-link
       {:key "copy"
        :on-click (fn [] (editor-handler/copy-current-ref block-ref-id))}

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

@@ -509,11 +509,6 @@
          [:span {:id (str "mock-text_" idx)
                  :key idx} c])))])
 
-(rum/defc mock-textarea-wrapper < rum/reactive
-  []
-  (let [content (state/sub-edit-content)]
-    (mock-textarea content)))
-
 (rum/defc animated-modal < rum/reactive
   [modal-name component set-default-width?]
   (when-let [pos (:pos (state/get-editor-action-data))]
@@ -585,7 +580,7 @@
   (shortcut/mixin :shortcut.handler/block-editing-only)
   lifecycle/lifecycle
   [state {:keys [format block]} id _config]
-  (let [content (state/sub-edit-content)
+  (let [content (state/sub-edit-content id)
         heading-class (get-editor-style-class content format)]
     [:div.editor-inner {:class (if block "block-editor" "non-block-editor")}
 
@@ -600,7 +595,7 @@
        :auto-focus        false
        :class             heading-class})
 
-     (mock-textarea-wrapper)
+     (mock-textarea content)
      (modals id format)
 
      (when format

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

@@ -266,11 +266,11 @@
 (defn input-password
   ([repo-url close-fn] (input-password repo-url close-fn {:type :local}))
   ([repo-url close-fn opts]
-   (fn [_close-fn]
+   (fn [close-fn']
      (let [close-fn' (if (fn? close-fn)
                        #(do (close-fn %)
-                            (_close-fn))
-                       _close-fn)]
+                            (close-fn'))
+                       close-fn')]
        (input-password-inner repo-url close-fn' opts)))))
 
 (rum/defcs encryption-setup-dialog-inner

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

@@ -161,8 +161,8 @@
       (ui/button "Cancel" :background "gray" :class "opacity-50" :on-click close-fn)
       (ui/button "Create remote graph" :on-click on-confirm)]]))
 
-(rum/defcs ^:large-vars/cleanup-todo indicator <
-  rum/reactive
+(rum/defcs ^:large-vars/cleanup-todo indicator < rum/reactive
+  < {:key-fn #(identity "file-sync-indicator")}
   {:will-mount   (fn [state]
                    (let [unsub-fn (file-sync-handler/setup-file-sync-event-listeners)]
                      (assoc state ::unsub-events unsub-fn)))
@@ -308,9 +308,9 @@
             (when (and synced-file-graph? queuing?)
               [:div.head-ctls
                (ui/button "Sync now"
-                 :class "block cursor-pointer"
-                 :small? true
-                 :on-click #(async/offer! fs-sync/immediately-local->remote-chan true))])
+                          :class "block cursor-pointer"
+                          :small? true
+                          :on-click #(async/offer! fs-sync/immediately-local->remote-chan true))])
 
                                         ;(when config/dev?
                                         ;  [:strong.debug-status (str status)])
@@ -359,7 +359,7 @@
                                 (p/resolved nil)
                                 (if (util/electron?)
                                   (ipc/ipc :readGraphTxIdInfo root)
-                                  (fs-util/read-graph-txid-info root)))
+                                  (fs-util/read-graphs-txid-info root)))
 
                               (p/then (fn [^js info]
                                         (when (and (not empty-dir?)
@@ -414,7 +414,7 @@
        [:div.p-4 (ui/loading "Loading...")]
        (for [version version-files]
          (let [version-uuid (get-version-key version)
-               _local?      (some? (:relative-path version))]
+               local?      (some? (:relative-path version))]
            [:div.version-list-item {:key version-uuid}
             [:a.item-link.block.fade-link.flex.justify-between
              {:title    version-uuid
@@ -427,7 +427,7 @@
                (or (:CreateTime version)
                    (:create-time version)) nil)]
              [:small.opacity-50.translate-y-1
-              (if _local?
+              (if local?
                 [:<> (ui/icon "git-commit") " local"]
                 [:<> (ui/icon "cloud") " remote"])]]])))]))
 

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

@@ -21,7 +21,9 @@
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
 
-(rum/defc home-button []
+(rum/defc home-button
+  < {:key-fn #(identity "home-button")}
+  []
   (ui/with-shortcut :go/home "left"
     [:button.button.icon.inline
      {:title "Home"
@@ -32,6 +34,7 @@
      (ui/icon "home" {:style {:fontSize ui/icon-size}})]))
 
 (rum/defc login < rum/reactive
+  < {:key-fn #(identity "login-button")}
   []
   (let [_ (state/sub :auth/id-token)
         loading? (state/sub [:ui/loading? :login])
@@ -46,14 +49,16 @@
          [:span.ml-2 (ui/loading "")])])))
 
 (rum/defc left-menu-button < rum/reactive
+  < {:key-fn #(identity "left-menu-toggle-button")}
   [{:keys [on-click]}]
   (ui/with-shortcut :ui/toggle-left-sidebar "bottom"
     [: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" {:style {:fontSize ui/icon-size}})]))
 
 (rum/defc dropdown-menu < rum/reactive
+  < {:key-fn #(identity "repos-dropdown-menu")}
   [{:keys [current-repo t]}]
   (let [page-menu (page-menu/page-menu nil)
         page-menu-and-hr (when (seq page-menu)
@@ -106,6 +111,7 @@
      {})))
 
 (rum/defc back-and-forward
+  < {:key-fn #(identity "nav-history-buttons")}
   []
   [:div.flex.flex-row
 
@@ -206,8 +212,6 @@
       (when plugin-handler/lsp-enabled?
         (plugins/hook-ui-items :toolbar))
 
-
-
       (when (util/electron?)
         (back-and-forward))
 

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

@@ -163,7 +163,7 @@
 (rum/defc today-queries < rum/reactive
   [repo today? sidebar?]
   (when (and today? (not sidebar?))
-    (let [queries (state/sub [:config repo :default-queries :journals])]
+    (let [queries (get-in (state/sub-config repo) [:default-queries :journals])]
       (when (seq queries)
         [:div#today-queries.mt-10
          (for [query queries]
@@ -683,7 +683,7 @@
                    (state/set-search-mode! :global)
                    state)}
   [state]
-  (let [settings (state/sub-graph-config-settings)
+  (let [settings (state/graph-settings)
         theme (state/sub :ui/theme)
         graph (graph-handler/build-global-graph theme settings)
         search-graph-filters (state/sub :search/graph-filters)

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

@@ -67,7 +67,7 @@
           contents? (= page-name "contents")
           properties (:block/properties page)
           public? (true? (:public properties))
-          favorites (:favorites (state/sub-graph-config))
+          favorites (:favorites (state/sub-config))
           favorited? (contains? (set (map util/page-name-sanity-lc favorites))
                                 page-name)
           developer-mode? (state/sub [:ui/developer-mode?])

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

@@ -872,8 +872,8 @@
                             false)}})
    {:trigger-class "toolbar-plugins-manager-trigger"}))
 
-(rum/defcs hook-ui-items <
-  rum/reactive
+(rum/defcs hook-ui-items < rum/reactive
+  < {:key-fn #(identity "plugin-hook-items")}
   "type of :toolbar, :pagebar"
   [_state type]
   (when (state/sub [:plugin/installed-ui-items])

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

@@ -87,18 +87,18 @@
 
 (rum/defc settings-container
   [schema ^js pl]
-  (let [^js _settings (.-settings pl)
+  (let [^js settings (.-settings pl)
         pid (.-id pl)
-        [settings, set-settings] (rum/use-state (bean/->clj (.toJSON _settings)))
-        update-setting! (fn [k v] (.set _settings (name k) (bean/->js v)))]
+        [settings, set-settings] (rum/use-state (bean/->clj (.toJSON settings)))
+        update-setting! (fn [k v] (.set settings (name k) (bean/->js v)))]
 
     (rum/use-effect!
       (fn []
         (let [on-change (fn [^js s]
                           (when-let [s (bean/->clj s)]
                             (set-settings s)))]
-          (.on _settings "change" on-change)
-          #(.off _settings "change" on-change)))
+          (.on settings "change" on-change)
+          #(.off settings "change" on-change)))
       [pid])
 
     (if (seq schema)
@@ -123,4 +123,4 @@
            [:p (str "#Not Handled#" key)]))]
 
       ;; no settings
-      [:h2.font-bold.text-lg.py-4.warning "No Settings Schema!"])))
+      [:h2.font-bold.text-lg.py-4.warning "No Settings Schema!"])))

+ 53 - 16
src/main/frontend/components/right_sidebar.cljs

@@ -159,8 +159,17 @@
     (get-page match)))
 
 (rum/defc sidebar-resizer
-  []
-  (let [el-ref (rum/use-ref nil)]
+  [sidebar-open? sidebar-id handler-position]
+  (let [el-ref (rum/use-ref nil)
+        min-ratio 0.1
+        max-ratio 0.7
+        keyboard-step 5
+        set-width! (fn [ratio element]
+                     (when (and el-ref element)
+                       (let [width (str (* ratio 100) "%")]
+                         (#(.setProperty (.-style element) "width" width)
+                          (.setAttribute (rum/deref el-ref) "aria-valuenow" ratio)
+                          (ui-handler/persist-right-sidebar-width!)))))]
     (rum/use-effect!
      (fn []
        (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
@@ -171,23 +180,51 @@
                 {:move
                  (fn [^js/MouseEvent e]
                    (let [width js/document.documentElement.clientWidth
-                         offset (.-left (.-rect e))
-                         right-el-ratio (- 1 (.toFixed (/ offset width) 6))
-                         right-el-ratio (cond
-                                          (< right-el-ratio 0.2) 0.2
-                                          (> right-el-ratio 0.7) 0.7
-                                          :else right-el-ratio)
-                         right-el (js/document.getElementById "right-sidebar")]
-                     (when right-el
-                       (let [width (str (* right-el-ratio 100) "%")]
-                         (.setProperty (.-style right-el) "width" width)
-                         (ui-handler/persist-right-sidebar-width!)))))}}))
+                         sidebar-el (js/document.getElementById sidebar-id)
+                         offset (.-pageX e)
+                         ratio (.toFixed (/ offset width) 6)
+                         ratio (if (= handler-position :west) (- 1 ratio) ratio)
+                         cursor-class (str "cursor-" (first (name handler-position)) "-resize")]
+                     (if (= (.getAttribute el "data-expanded") "true")
+                       (cond
+                         (< ratio (/ min-ratio 2))
+                         (state/hide-right-sidebar!)
+
+                         (< ratio min-ratio)
+                         (.. js/document.documentElement -classList (add cursor-class))
+
+                         (and (< ratio max-ratio) sidebar-el)
+                         (when sidebar-el
+                           (#(.. js/document.documentElement -classList (remove cursor-class))
+                            (set-width! ratio sidebar-el)))
+                         :else
+                         #(.. 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 "dragend" #(.. js/document.documentElement -classList (remove "is-resizing-buf")))
+             (.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))
+                                      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))))))))
        #())
      [])
-    [:span.resizer {:ref el-ref}]))
+    [:.resizer {:ref el-ref
+                :role "separator"
+                :aria-orientation "vertical"
+                :aria-label (t :right-side-bar/separator)
+                :aria-valuemin (* min-ratio 100)
+                :aria-valuemax (* max-ratio 100)
+                :tabIndex "0"
+                :data-expanded sidebar-open?}]))
 
 (rum/defcs sidebar-inner <
   (rum/local false ::anim-finished?)
@@ -198,7 +235,6 @@
   (let [*anim-finished? (get state ::anim-finished?)]
     [:div.cp__right-sidebar-inner.flex.flex-col.h-full#right-sidebar-container
 
-     (sidebar-resizer)
      [:div.cp__right-sidebar-scrollable
       [:div.cp__right-sidebar-topbar.flex.flex-row.justify-between.items-center.px-2.h-12
        [:div.cp__right-sidebar-settings.hide-scrollbar.gap-1 {:key "right-sidebar-settings"}
@@ -243,5 +279,6 @@
         repo (state/sub :git/current-repo)]
     [:div#right-sidebar.cp__right-sidebar.h-screen
      {:class (if sidebar-open? "open" "closed")}
+     (sidebar-resizer sidebar-open? "right-sidebar" :west)
      (when sidebar-open?
        (sidebar-inner repo t blocks))]))

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

@@ -15,6 +15,7 @@
             [frontend.handler.user :as user-handler]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.file-sync :as file-sync-handler]
+            [frontend.handler.global-config :as global-config-handler]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
             [frontend.state :as state]
@@ -140,10 +141,18 @@
   (row-with-button-action
     {:left-label   (t :settings-page/custom-configuration)
      :button-label (t :settings-page/edit-config-edn)
-     :href         (rfe/href :file {:path (config/get-config-path)})
+     :href         (rfe/href :file {:path (config/get-repo-config-path)})
      :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
      :-for         "config_edn"}))
 
+(defn edit-global-config-edn []
+  (row-with-button-action
+    {:left-label   (t :settings-page/custom-global-configuration)
+     :button-label (t :settings-page/edit-global-config-edn)
+     :href         (rfe/href :file {:path (global-config-handler/global-config-path)})
+     :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
+     :-for         "global_config_edn"}))
+
 (defn edit-custom-css []
   (row-with-button-action
     {:left-label   (t :settings-page/custom-theme)
@@ -551,6 +560,7 @@
      (version-row t version)
      (language-row t preferred-language)
      (theme-modes-row t switch-theme system-theme? dark?)
+     (when (config/global-config-enabled?) (edit-global-config-edn))
      (when current-repo (edit-config-edn))
      (when current-repo (edit-custom-css))
      (when current-repo (edit-export-css))
@@ -611,7 +621,7 @@
         developer-mode? (state/sub [:ui/developer-mode?])
         https-agent-opts (state/sub [:electron/user-cfgs :settings/agent])]
     [:div.panel-wrap.is-advanced
-     (when (and util/mac? (util/electron?)) (app-auto-update-row t))
+     (when (and (or util/mac? util/win32?) (util/electron?)) (app-auto-update-row t))
      (usage-diagnostics-row t instrument-disabled?)
      (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
      (when (util/electron?) (https-user-agent-row https-agent-opts))

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

@@ -79,7 +79,7 @@
          [:th.text-right]]]
        [:tbody
         (map (fn [[k {:keys [binding]}]]
-               [:tr {:key k}
+               [:tr {:key (str k)}
                 [:td.text-left (t (dh/decorate-namespace k))]
                 (shortcut-col k binding configurable? (t (dh/decorate-namespace k)))])
           (dh/binding-by-category name))]]])))

+ 10 - 8
src/main/frontend/components/sidebar.cljs

@@ -140,7 +140,7 @@
       (rfe/push-state :page {:name "Favorites"})
       (util/stop e))}
 
-   (let [favorites (->> (:favorites (state/sub-graph-config))
+   (let [favorites (->> (:favorites (state/sub-config))
                         (remove string/blank?)
                         (filter string?))]
      (when (seq favorites)
@@ -402,14 +402,16 @@
          [:div.mt-20
           [:div.ls-center
            (ui/loading (t :loading))]]
-
+         
          :else
-         [:div {:class (if margin-less-pages? "" (util/hiccup->class "max-w-7xl.mx-auto.pb-24"))
-                :style {:margin-bottom (cond
-                                         margin-less-pages? 0
-                                         onboarding-and-home? -48
-                                         :else 120)
-                        :padding-bottom (when (mobile-util/native-iphone?) "7rem")}}
+         [:div
+          {:class (if margin-less-pages? "" (util/hiccup->class "mx-auto.pb-24"))
+           :style {:margin-bottom (cond
+                                    global-graph-pages? 0
+                                    margin-less-pages? 0
+                                    onboarding-and-home? -48
+                                    :else 120)
+                   :padding-bottom (when (mobile-util/native-iphone?) "7rem")}}
           main-content])
 
        (when onboarding-and-home?

+ 23 - 12
src/main/frontend/components/sidebar.css

@@ -304,7 +304,7 @@
   top: var(--ls-headbar-inner-top-padding);
   left: 0;
   z-index: var(--ls-z-index-level-5);
-  transition: width .5s;
+  transition: width .3s;
 
   a {
     color: var(--ls-primary-text-color);
@@ -443,9 +443,31 @@ html[data-theme='dark'] {
   z-index: var(--ls-z-index-level-1);
   transition: width 0.3s;
   background-color: var(--ls-secondary-background-color, #d8e1e8);
+  position: relative;
+
+  .resizer {
+    @apply absolute top-0 bottom-0;
+    left: 0;
+    width: 4px;
+    user-select: none;
+    cursor: col-resize !important;
+    transition: background-color 300ms;
+    transition-delay: 300ms;
+    z-index: 1000;
+
+    &:hover,
+    &:focus,
+    &:active {
+      background-color: var(--ls-active-primary-color);
+    }
+  }
 
   &.closed {
     width: 0 !important;
+
+    .resizer {
+      left: -4px;
+    }
   }
 
   &.open {
@@ -460,17 +482,6 @@ html[data-theme='dark'] {
 
   &-inner {
     padding-top: 0;
-    position: relative;
-
-    .resizer {
-      position: absolute;
-      top: 0;
-      bottom: 0;
-      left: 0;
-      width: 4px;
-      user-select: none;
-      cursor: col-resize !important;
-    }
   }
 
   &-settings {

+ 11 - 0
src/main/frontend/components/theme.css

@@ -102,4 +102,15 @@ html.is-resizing-buf {
   #right-sidebar {
     transition: none;
   }
+
+  * {
+    cursor: col-resize !important;
+    user-select: none;
+  }
+
+  &.cursor-w-resize {
+    * {
+      cursor: w-resize !important;
+    }
+  }
 }

+ 7 - 3
src/main/frontend/config.cljs

@@ -264,6 +264,10 @@
 
 (def config-default-content (rc/inline "config.edn"))
 
+;; Desktop only as other platforms requires better understanding of their
+;; multi-graph workflows and optimal place for a "global" dir
+(def global-config-enabled? util/electron?)
+
 (defonce idb-db-prefix "logseq-db/")
 (defonce local-db-prefix "logseq_local_")
 (defonce local-handle "handle")
@@ -373,9 +377,9 @@
                         page-name)]
     (get-file-path repo-url (str sub-dir "/" page-basename "." ext))))
 
-(defn get-config-path
+(defn get-repo-config-path
   ([]
-   (get-config-path (state/get-current-repo)))
+   (get-repo-config-path (state/get-current-repo)))
   ([repo]
    (when repo
      (get-file-path repo (str app-name "/" config-file)))))
@@ -430,4 +434,4 @@
 
 (defn get-block-hidden-properties
   []
-  (get-in @state/state [:config (state/get-current-repo) :block-hidden-properties]))
+  (:block-hidden-properties (state/get-config)))

+ 3 - 2
src/main/frontend/context/i18n.cljs

@@ -1,6 +1,7 @@
 (ns frontend.context.i18n
-  "Handles translation for the entire application. The dependencies for this ns
-  must be small since it is used throughout the application."
+  "This ns is a system component that handles translation for the entire
+  application. The ns dependencies for this ns must be small since it is used
+  throughout the application."
   (:require [frontend.dicts :as dicts]
             [frontend.modules.shortcut.dicts :as shortcut-dicts]
             [tongue.core :as tongue]

+ 3 - 6
src/main/frontend/db.cljs

@@ -179,12 +179,9 @@
     (restore-graph-from-text! repo stored)))
 
 (defn restore!
-  [{:keys [repos]} _old-db-schema restore-config-handler]
-  (let [repo (or (state/get-current-repo) (:url (first repos)))]
-    (when repo
-      (p/let [_ (restore-graph! repo)]
-        (restore-config-handler repo)
-        (listen-and-persist! repo)))))
+  [repo]
+  (p/let [_ (restore-graph! repo)]
+    (listen-and-persist! repo)))
 
 (defn run-batch-txs!
   []

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

@@ -65,9 +65,10 @@
                                   result)]
                      (model/with-pages result))
                    result)
-          result-transform-fn (:result-transform q)
-          repo (state/get-current-repo)]
-      (if-let [result-transform (if (keyword? result-transform-fn) (state/sub [:config repo :query/result-transforms result-transform-fn]) result-transform-fn)]
+          result-transform-fn (:result-transform q)]
+      (if-let [result-transform (if (keyword? result-transform-fn)
+                                  (get-in (state/sub-config) [:query/result-transforms result-transform-fn])
+                                  result-transform-fn)]
         (if-let [f (sci/eval-string (pr-str result-transform))]
           (try
             (sci/call-fn f result)

+ 90 - 39
src/main/frontend/dicts.cljc

@@ -70,6 +70,7 @@
         :right-side-bar/flashcards "Flashcards"
         :right-side-bar/new-page "New page"
         :right-side-bar/show-journals "Show Journals"
+        :right-side-bar/separator "Right sidebar resize handler"
         :left-side-bar/journals "Journals"
         :left-side-bar/new-page "New page"
         :left-side-bar/nav-favorites "Favorites"
@@ -155,9 +156,11 @@
         :settings-page/git-switcher-label "Enable Git auto commit"
         :settings-page/git-commit-delay "Git auto commit seconds"
         :settings-page/edit-config-edn "Edit config.edn"
+        :settings-page/edit-global-config-edn "Edit global config.edn"
         :settings-page/edit-custom-css "Edit custom.css"
         :settings-page/edit-export-css "Edit export.css"
         :settings-page/custom-configuration "Custom configuration"
+        :settings-page/custom-global-configuration "Custom global configuration"
         :settings-page/custom-theme "Custom theme"
         :settings-page/export-theme "Export theme"
         :settings-page/show-brackets "Show brackets"
@@ -2392,8 +2395,8 @@
            :settings-page/network-proxy "Nettverksproxy"
            :settings-page/plugin-system "System for utvidelser"}
 
-   :pt-BR {:on-boarding/demo-graph "Esse é um gráfico de demonstração, mudanças não serão salvas enquanto uma pasta local não for aberta."
-           :on-boarding/add-graph "Adicionar gráfico"
+   :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"
            :on-boarding/open-local-dir "Abrir pasta local"
            :on-boarding/new-graph-desc-1 "Logseq funciona com Markdown e Org-mode. Você pode abrir uma pasta existente ou criar uma nova em seu dispositivo. Seus dados serão armazenados apenas neste dispositivo."
            :on-boarding/new-graph-desc-2 "Após abrir sua pasta, três pastas serão criadas nela:"
@@ -2444,7 +2447,7 @@
            :right-side-bar/recent "Recente"
            :right-side-bar/contents "Conteúdo"
            :right-side-bar/favorites "Favoritos"
-           :right-side-bar/page-graph "Gráfico da página"
+           :right-side-bar/page-graph "Grafo da página"
            :right-side-bar/block-ref "Referência de bloco"
            :right-side-bar/flashcards "Flashcards"
            :right-side-bar/new-page "Nova página"
@@ -2541,7 +2544,7 @@
            :settings-page/disable-developer-mode "Desativar modo de desenvolvimento"
            :settings-page/developer-mode-desc "O modo de desenvolvimento ajuda os contribuidores e programadores de extensões a testar as suas integrações com o Logseq de forma eficiente."
            :settings-page/current-version "Versão atual"
-           :settings-page/current-graph "Gráfico atual"
+           :settings-page/current-graph "Grafo atual"
            :settings-page/tab-general "Geral"
            :settings-page/tab-editor "Editor"
            :settings-page/tab-shortcuts "Atalhos"
@@ -2562,16 +2565,16 @@
            :search/publishing "Pesquisar"
            :search "Pesquisar ou Criar Página"
            :page-search "Pesquisar na página atual"
-           :graph-search "Pesquisar gráfico"
+           :graph-search "Pesquisar grafo"
            :new-page "Nova página"
            :new-file "Novo arquivo"
-           :new-graph "Adicionar novo gráfico"
-           :graph "Gráfico"
-           :graph-view "Ver Gráfico"
+           :new-graph "Adicionar novo grafo"
+           :graph "Grafo"
+           :graph-view "Ver Grafo"
            :cards-view "Ver Cartões"
            :publishing "Publicando"
            :export "Exportar"
-           :export-graph "Exportar Gráfico"
+           :export-graph "Exportar Grafo"
            :export-markdown "Exportar como Markdown padrão (sem propriedades de bloco)"
            :export-opml "Exportar como OPML"
            :export-page "Exportar página"
@@ -2581,7 +2584,7 @@
            :export-edn "Exportar como EDN"
            :export-datascript-edn "Exportar datascript EDN"
            :convert-markdown "Converter cabeçalhos Markdown para listas não-ordenadas (# -> -)"
-           :all-graphs "Todos os gráficos"
+           :all-graphs "Todos os grafos"
            :all-pages "Todas as páginas"
            :all-files "Todos os arquivos"
            :all-journals "Todos os diários"
@@ -2610,7 +2613,7 @@
            :white "Claro"
            :dark "Escuro"
            :remove-background "Remover fundo"
-           :re-index-detail "Re-indexar gráfico"
+           :re-index-detail "Re-indexar grafo"
            :open "Abrir"
            :open-a-directory "Abrir uma pasta local"
            :open-new-window "Nova janela"
@@ -2664,13 +2667,13 @@
            :plugin/update-available "Atualização disponível"
            :plugin/updating "Atualizando"
            :right-side-bar/all-pages "Todas as páginas"
-           :right-side-bar/graph-view "Ver gráfico"
+           :right-side-bar/graph-view "Ver grafo"
            :search/page-names "Procurar nome da página"
            :plugin/stars "Estrelas"
            :select/default-prompt "Selecione um"
-           :select.graph/prompt "Selecione um gráfico"
-           :select.graph/add-graph "Sim, adicionar outro gráfico"
-           :select.graph/empty-placeholder-description "Nenhum gráfico encontrado. Deseja adicionar um novo?"
+           :select.graph/prompt "Selecione um grafo"
+           :select.graph/add-graph "Sim, adicionar outro grafo"
+           :select.graph/empty-placeholder-description "Nenhum grafo encontrado. Deseja adicionar um novo?"
            :settings-page/enable-shortcut-tooltip "Habilitar dicas de atalho"
            :tips/all-done "Tudo certo"
            :updater/new-version-install "Uma nova versão foi baixada"
@@ -2683,8 +2686,8 @@
            :settings-page/custom-configuration "Configuração personalizada"
            :settings-page/custom-theme "Tema personalizado"
            :settings-page/edit-custom-css "Editar custom.css"
-           :re-index-multiple-windows-warning "Você precisa fechar as outras janelas antes de reindexar este gráfico."
-           :re-index-discard-unsaved-changes-warning "A reindexação descartará o gráfico atual e processará todos os arquivos novamente conforme estão armazenados no disco. Você perderá as alterações não salvas e pode demorar um pouco. Continuar?"
+           :re-index-multiple-windows-warning "Você precisa fechar as outras janelas antes de reindexar este grafo"
+           :re-index-discard-unsaved-changes-warning "A reindexação descartará o grafo atual e processará todos os arquivos novamente conforme estão armazenados no disco. Você perderá as alterações não salvas e pode demorar um pouco. Continuar?"
            :sync-from-local-changes-detected "Atualizar detecta e processa arquivos modificados em seu disco e que são diferentes do conteúdo atual da página do Logseq. Continuar?"
            :page/open-backup-directory "Abra a listagem de backups de página"
            :save "Salvar"
@@ -2700,8 +2703,8 @@
            :settings-page/plugin-system "Sistema de Plugins"
            :settings-page/network-proxy "Proxy de Rede"
 
-           :file-sync/other-user-graph "O gráfico local atual é obrigado ao gráfico remoto de outro usuário. Portanto, não consigo iniciar a sincronização."
-           :file-sync/graph-deleted "O gráfico remoto atual foi excluído"
+           :file-sync/other-user-graph "O grafo local atual está ligado ao grafo remoto de outro usuário. Portanto, não consigo iniciar a sincronização."
+           :file-sync/graph-deleted "O grafo remoto atual foi excluído"
 
            :page/copy-page-url "Copiar URL da página"
            :plugin/not-installed "Não instalado"
@@ -2709,7 +2712,26 @@
            :tutorial/text "tutorial-en.md"
            :settings-page/edit-export-css "Editar export.css"
            :settings-page/enable-flashcards "Flashcards"
-           :settings-page/export-theme "Exportar Tema"}
+           :settings-page/export-theme "Exportar Tema"
+           
+           :discourse-title "Nosso fórum!"
+           :importing "Importando"
+           :asset/copy "Copiar imagem"
+           :asset/delete "Excluir imagem"
+           :asset/maximize "Expandir imagem"
+
+           :asset/open-in-browser "Abrir imagem no navegador"
+           :asset/show-in-folder "Mostrar imagem na pasta"
+           :graph/all-graphs "Todos os grafos"
+           :graph/local-graphs "Grafos locais"
+           :graph/remote-graphs "Grafos remotos"
+           :help/forum-community "Comunidade do fórum"
+           :linked-references/filter-search "Procurar em páginas vinculadas"
+           :right-side-bar/show-journals "Mostrar registros"
+           :settings-page/custom-global-configuration "Configuração global personalizada"
+           :settings-page/edit-global-config-edn "Editar config.edn global"
+           :settings-page/sync "Sincronizar"
+           :settings-page/tab-features "Recursos"}
 
    :pt-PT {:on-boarding/demo-graph "Isto é um grafo de demonstração, nenhuma mudança será guardada até abrir uma pasta local."
            :on-boarding/add-graph "Adicionar grafo"
@@ -3033,7 +3055,26 @@
         :settings-page/enable-flashcards "Flashcards"
         :settings-page/export-theme "Exportar tema"
         :settings-page/network-proxy "Proxy de rede"
-        :settings-page/plugin-system "Sistema de plugins"}
+        :settings-page/plugin-system "Sistema de plugins"
+        
+        :discourse-title "Nosso fórum!"
+        :importing "Importando"
+        :asset/copy "Copiar imagem"
+        :asset/delete "Excluir imagem"
+        :asset/maximize "Expandir imagem"
+
+        :asset/open-in-browser "Abrir imagem no navegador"
+        :asset/show-in-folder "Mostrar imagem na pasta"
+        :graph/all-graphs "Todos os grafos"
+        :graph/local-graphs "Grafos locais"
+        :graph/remote-graphs "Grafos remotos"
+        :help/forum-community "Comunidade do fórum"
+        :linked-references/filter-search "Procurar em páginas vinculadas"
+        :right-side-bar/show-journals "Mostrar registros"
+        :settings-page/custom-global-configuration "Configuração global personalizada"
+        :settings-page/edit-global-config-edn "Editar config.edn global"
+        :settings-page/sync "Sincronizar"
+        :settings-page/tab-features "Recursos"}
 
    :ru {:on-boarding/demo-graph "Это демонстрационный граф, изменения не будут сохранены, пока вы не откроете локальный файл."
         :on-boarding/add-graph "Добавить новый граф"
@@ -4013,8 +4054,8 @@
                                 :default "tutorial-tr.md")
         :tutorial/dummy-notes #?(:cljs (rc/inline "dummy-notes-tr.md")
                                        :default "dummy-notes-tr.md")
-        :on-boarding/demo-graph "Bu bir demo grafiktir, yerel bir klasör açana kadar değişiklikler kaydedilmeyecektir."
-        :on-boarding/add-graph "Bir grafik ekle"
+        :on-boarding/demo-graph "Bu bir demo çizelgedir, yerel bir klasör açana kadar değişiklikler kaydedilmeyecektir."
+        :on-boarding/add-graph "Bir çizelge ekle"
         :on-boarding/open-local-dir "Yerel bir dizin açın"
         :on-boarding/new-graph-desc-1 "Logseq, hem Markdown hem de Org modunu destekler. Cihazınızda var olan bir dizini (klasörü) açabilir veya yeni bir tane oluşturabilirsiniz. Verileriniz yalnızca bu cihazda saklanacaktır."
         :on-boarding/new-graph-desc-2 "Dizininizi açtıktan sonra, o dizinde üç klasör oluşturacaktır:"
@@ -4067,9 +4108,9 @@
         :right-side-bar/recent "En son"
         :right-side-bar/contents "İçindekiler"
         :right-side-bar/favorites "Sık kullanılanlar"
-        :right-side-bar/page-graph "Sayfa grafiği"
+        :right-side-bar/page-graph "Sayfa çizelgesi"
         :right-side-bar/block-ref "Blok referansı"
-        :right-side-bar/graph-view "Grafik görünümü"
+        :right-side-bar/graph-view "Çizelge görünümü"
         :right-side-bar/all-pages "Bütün sayfalar"
         :right-side-bar/flashcards "Bilgi kartları"
         :right-side-bar/new-page "Yeni sayfa"
@@ -4137,6 +4178,11 @@
         :draw/more-options "Diğer seçenekler"
         :draw/back-to-logseq "Logseq'e geri dön"
         :text/image "Resim"
+        :asset/show-in-folder "Resmi klasörde göster"
+        :asset/open-in-browser "Resmi tarayıcıda aç"
+        :asset/delete "Resmi sil"
+        :asset/copy "Resmi kopyala"
+        :asset/maximize "Resim ekranı kaplasın"
         :asset/confirm-delete "Bu resmi silmek istediğinizden emin misiniz?"
         :asset/physical-delete "Dosyayı da kaldırın (geri getirilemeyeceğine dikkat edin)"
         :content/copy "Kopyala"
@@ -4183,15 +4229,17 @@
         :settings-page/disable-developer-mode "Geliştirici modunu devre dışı bırak"
         :settings-page/developer-mode-desc "Geliştirici modu, katkıda bulunanların ve eklenti geliştiricilerinin Logseq ile entegrasyonlarını daha verimli bir şekilde test etmesine yardımcı olur."
         :settings-page/current-version "Geçerli sürüm"
-        :settings-page/current-graph "Geçerli grafik"
+        :settings-page/current-graph "Geçerli çizelge"
         :settings-page/tab-general "Genel"
         :settings-page/tab-editor "Düzenleyici"
         :settings-page/tab-shortcuts "Kısayollar"
         :settings-page/tab-version-control "Sürüm denetimi"
         :settings-page/tab-advanced "Gelişmiş"
-        :settings-page/plugin-system "Eklenti sistemi"
+        :settings-page/tab-features "Özellikler"
+        :settings-page/plugin-system "Eklentiler"
         :settings-page/enable-flashcards "Bilgi kartları"
         :settings-page/network-proxy "Ağ ara sunucusu"
+        :settings-page/sync "Eşitle"
         :logseq "Logseq"
         :on "AÇIK"
         :more-options "Diğer seçenekler"
@@ -4208,7 +4256,7 @@
         :port "Bağlantı Noktası"
         :re-index "Yeniden dizin oluştur"
         :re-index-detail "Grafiği yeniden oluştur"
-        :re-index-multiple-windows-warning "Bu grafik için yeniden dizin oluşturmadan önce diğer pencereleri kapatmanız gerekiyor."
+        :re-index-multiple-windows-warning "Bu çizelge için yeniden dizin oluşturmadan önce diğer pencereleri kapatmanız gerekiyor."
         :re-index-discard-unsaved-changes-warning "Yeniden dizin oluşturmak mevcut grafiği siler ve ardından tüm dosyaları o anda diskte depolandıkları şekilde yeniden işler. Kaydedilmemiş değişiklikleri kaybedeceksiniz ve bu biraz zaman alabilir. Devam edilsin mi?"
         :open-new-window "Yeni pencere"
         :sync-from-local-files "Yenile"
@@ -4219,21 +4267,24 @@
         :search/publishing "Ara"
         :search "Ara veya sayfa oluştur"
         :page-search "Geçerli sayfada ara"
-        :graph-search "Grafikte ara"
+        :graph-search "Çizelgede ara"
         :new-page "Yeni sayfa"
         :new-file "Yeni dosya"
-        :new-graph "Yeni grafik ekle"
-        :graph "Grafik"
-        :graph-view "Grafiği görüntüle"
+        :new-graph "Yeni çizelge ekle"
+        :graph "Çizelge"
+        :graph-view "Çizelgeyi görüntüle"
         :graph/persist "Logseq dahili durumu senkronize ediyor, lütfen birkaç saniye bekleyin."
         :graph/persist-error "Dahili durum senkronize edilemedi."
         :graph/save "Kaydediliyor..."
         :graph/save-success "Başarıyla Kaydedildi"
         :graph/save-error "Kaydedilemedi"
+        :graph/all-graphs "Tüm çizelgeler"
+        :graph/local-graphs "Yerel çizelgeler"
+        :graph/remote-graphs "Uzak çizelgeler"
         :cards-view "Kartları görüntüle"
         :publishing "Yayımlama"
         :export "Dışarı aktar"
-        :export-graph "Grafiği dışarı aktar"
+        :export-graph "Çizelgeyi dışarı aktar"
         :export-page "Sayfayı dışarı aktar"
         :export-markdown "Standart Markdown olarak dışarı aktar (blok özelliği yok)"
         :export-opml "OPML olarak dışarı aktar"
@@ -4243,7 +4294,7 @@
         :export-edn "EDN olarak dışarı aktar"
         :export-datascript-edn "EDN datascript öğesini dışarı aktar"
         :convert-markdown "Markdown başlıklarını sırasız listelere dönüştürün (# -> -)"
-        :all-graphs "Tüm grafikler"
+        :all-graphs "Tüm çizelgeler"
         :all-pages "Tüm sayfalar"
         :all-files "Tüm dosyalar"
         :remove-orphaned-pages "Yalnız bırakılmış sayfaları kaldır"
@@ -4332,12 +4383,12 @@
 
         :command-palette/prompt "Bir komut yazın"
         :select/default-prompt "Birini seçin"
-        :select.graph/prompt "Bir grafik seçin"
-        :select.graph/empty-placeholder-description "Eşleşen grafik yok. Bir tane daha eklemek ister misin?"
-        :select.graph/add-graph "Evet, başka bir grafik ekle"
+        :select.graph/prompt "Bir çizelge seçin"
+        :select.graph/empty-placeholder-description "Eşleşen çizelge yok. Bir tane daha eklemek ister misin?"
+        :select.graph/add-graph "Evet, başka bir çizelge ekle"
 
-        :file-sync/other-user-graph "Geçerli yerel grafik, diğer kullanıcının uzak grafiğine bağlıdır. Bu yüzden senkronizasyon başlatılamıyor."
-        :file-sync/graph-deleted "Geçerli uzak grafik silindi"}
+        :file-sync/other-user-graph "Geçerli yerel çizelge, diğer kullanıcının uzak çizelgesine bağlıdır. Bu yüzden senkronizasyon başlatılamıyor."
+        :file-sync/graph-deleted "Geçerli uzak çizelge silindi"}
 
    :ko {:tutorial/text #?(:cljs (rc/inline "tutorial-ko.md")
                           :default "tutorial-ko.md")

+ 6 - 4
src/main/frontend/extensions/latex.cljs

@@ -18,10 +18,12 @@
   [state]
   (let [[id s display?] (:rum/args state)]
     (try
-      (js/katex.render s (gdom/getElement id)
-                      #js {:displayMode display?
-                           :throwOnError false
-                           :strict false})
+      (when-let [elem (gdom/getElement id)]
+        (js/katex.render s elem
+                         #js {:displayMode display?
+                              :throwOnError false
+                              :strict false}))
+
       (catch js/Error e
         (js/console.error e)))))
 

+ 1 - 1
src/main/frontend/extensions/zotero/setting.cljs

@@ -19,7 +19,7 @@
 
 (defn sub-zotero-config
   []
-  (:zotero/settings-v2 (get (state/sub-config) (state/get-current-repo))))
+  (:zotero/settings-v2 (state/sub-config)))
 
 (defn all-profiles []
   (let [profiles (-> (sub-zotero-config) keys set)

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

@@ -172,8 +172,8 @@
         result))))
 
 (defn watch-dir!
-  [dir]
-  (protocol/watch-dir! (get-record) dir))
+  ([dir] (watch-dir! dir {}))
+  ([dir options] (protocol/watch-dir! (get-record) dir options)))
 
 (defn unwatch-dir!
   [dir]

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

@@ -34,7 +34,7 @@
     nil)
   (get-files [_this _path-or-handle _ok-handler]
     nil)
-  (watch-dir! [_this _dir]
+  (watch-dir! [_this _dir _options]
     nil)
   (unwatch-dir! [_this _dir]
     nil))

+ 7 - 6
src/main/frontend/fs/capacitor_fs.cljs

@@ -2,6 +2,7 @@
   (:require ["@capacitor/filesystem" :refer [Encoding Filesystem]]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
+            [goog.string :as gstring]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.encrypt :as encrypt]
@@ -126,11 +127,11 @@
 (def backup-dir "logseq/bak")
 (defn- get-backup-dir
   [repo-dir path ext]
-  (let [path (if (string/starts-with? path "file://")
-               (subs path 7)
-               path)
-        relative-path (-> (string/replace path repo-dir "")
-                          (string/replace (str "." ext) ""))]
+  (let [relative-path (-> path
+                          (string/replace (re-pattern (str "^" (gstring/regExpEscape repo-dir)))
+                                          "")
+                          (string/replace (re-pattern (str "(?i)" (gstring/regExpEscape (str "." ext)) "$"))
+                                          ""))]
     (util/safe-path-join repo-dir (str backup-dir "/" relative-path))))
 
 (defn- truncate-old-versioned-files!
@@ -351,7 +352,7 @@
       (into [] (concat [{:path path}] files))))
   (get-files [_this path-or-handle _ok-handler]
     (readdir path-or-handle))
-  (watch-dir! [_this dir]
+  (watch-dir! [_this dir _options]
     (p/do!
      (.unwatch mobile-util/fs-watcher)
      (.watch mobile-util/fs-watcher (clj->js {:path dir}))))

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

@@ -239,5 +239,5 @@
     (utils/getFiles path-or-handle true ok-handler))
 
   ;; TODO:
-  (watch-dir! [_this _dir]
+  (watch-dir! [_this _dir _options]
     nil))

+ 2 - 2
src/main/frontend/fs/node.cljs

@@ -124,7 +124,7 @@
     (open-dir))
   (get-files [_this path-or-handle _ok-handler]
     (ipc/ipc "getFiles" path-or-handle))
-  (watch-dir! [_this dir]
-    (ipc/ipc "addDirWatcher" dir))
+  (watch-dir! [_this dir options]
+    (ipc/ipc "addDirWatcher" dir options))
   (unwatch-dir! [_this dir]
     (ipc/ipc "unwatchDir" dir)))

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

@@ -15,7 +15,7 @@
   (stat [this dir path])
   (open-dir [this ok-handler])
   (get-files [this path-or-handle ok-handler])
-  (watch-dir! [this dir])
+  (watch-dir! [this dir options])
   (unwatch-dir! [this dir])
   ;; Ensure the dir is watched, window agnostic.
   ;; Implementation should handle the actual watcher's construction / destruction.

+ 59 - 49
src/main/frontend/fs/sync.cljs

@@ -702,7 +702,7 @@
           (recur (dec n)))
         r))))
 
-(deftype RSAPI [^:mutable _graph-uuid ^:mutable _private-key ^:mutable _public-key]
+(deftype RSAPI [^:mutable graph-uuid' ^:mutable private-key' ^:mutable public-key']
   IToken
   (<get-token [this]
     (go
@@ -714,15 +714,15 @@
       (state/get-auth-id-token)))
 
   IRSAPI
-  (rsapi-ready? [_ graph-uuid] (and (= graph-uuid _graph-uuid) _private-key _public-key))
+  (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]
     (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)
+    (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)))
   (<get-local-all-files-meta [_ graph-uuid base-path]
     (go
@@ -786,7 +786,7 @@
                                     (js->clj r))))))
 
 
-(deftype ^:large-vars/cleanup-todo CapacitorAPI [^:mutable _graph-uuid ^:mutable _private-key ^:mutable _public-key]
+(deftype ^:large-vars/cleanup-todo CapacitorAPI [^:mutable graph-uuid' ^:mutable private-key ^:mutable public-key']
   IToken
   (<get-token [this]
     (go
@@ -798,15 +798,15 @@
       (state/get-auth-id-token)))
 
   IRSAPI
-  (rsapi-ready? [_ graph-uuid] (and (= graph-uuid _graph-uuid) _private-key _public-key))
+  (rsapi-ready? [_ graph-uuid] (and (= graph-uuid graph-uuid') private-key public-key'))
   (<key-gen [_]
     (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! _graph-uuid graph-uuid)
-    (set! _private-key secret-key)
-    (set! _public-key public-key)
+    (set! graph-uuid' graph-uuid)
+    (set! private-key secret-key)
+    (set! public-key' public-key)
     (p->c (.setEnv mobile-util/file-sync (clj->js {:env (if prod? "prod" "dev")
                                                    :secretKey secret-key
                                                    :publicKey public-key}))))
@@ -1423,7 +1423,7 @@
               r)))))))
 
 (defn apply-filetxns-partitions
-  "won't call update-graph-txid! when *txid is nil"
+  "won't call update-graphs-txid! when *txid is nil"
   [*sync-state user-uuid graph-uuid base-path filetxns-partitions repo *txid *stopped *paused]
   (assert (some? *sync-state))
 
@@ -2356,7 +2356,7 @@
  SyncManager [graph-uuid base-path *sync-state
               ^Local->RemoteSyncer local->remote-syncer ^Remote->LocalSyncer remote->local-syncer remoteapi
               ^:mutable ratelimit-local-changes-chan
-              *txid ^:mutable state ^:mutable _remote-change-chan ^:mutable _*ws *stopped? *paused?
+              *txid ^:mutable state ^:mutable remote-change-chan ^:mutable *ws *stopped? *paused?
               ^:mutable ops-chan
               ;; control chans
               private-full-sync-chan private-stop-sync-chan private-remote->local-sync-chan
@@ -2389,8 +2389,8 @@
 
   (start [this]
     (set! ops-chan (chan (async/dropping-buffer 10)))
-    (set! _*ws (atom nil))
-    (set! _remote-change-chan (ws-listen! graph-uuid _*ws))
+    (set! *ws (atom nil))
+    (set! remote-change-chan (ws-listen! graph-uuid *ws))
     (set! ratelimit-local-changes-chan (<ratelimit local->remote-syncer local-changes-revised-chan))
     (setup-local->remote! local->remote-syncer)
     (async/tap full-sync-mult private-full-sync-chan)
@@ -2406,7 +2406,7 @@
               private-remote->local-sync-chan {:remote->local true}
               private-full-sync-chan {:local->remote-full-sync true}
               private-pause-resume-chan ([v] (if v {:resume true} {:pause true}))
-              _remote-change-chan ([v] (println "remote change:" v) {:remote->local v})
+              remote-change-chan ([v] (println "remote change:" v) {:remote->local v})
               ratelimit-local-changes-chan ([v]
                                             (let [rest-v (util/drain-chan ratelimit-local-changes-chan)
                                                   vs     (cons v rest-v)]
@@ -2655,7 +2655,7 @@
     (go
       (when-not @*stopped?
         (vreset! *stopped? true)
-        (ws-stop! _*ws)
+        (ws-stop! *ws)
         (offer! private-stop-sync-chan true)
         (async/untap full-sync-mult private-full-sync-chan)
         (async/untap stop-sync-mult private-stop-sync-chan)
@@ -2730,54 +2730,64 @@
   [local-graph-uuid]
   {:pre [(util/uuid-string? local-graph-uuid)]}
   (go
-    (let [result (->> (<! (<list-remote-graphs remoteapi))
-                      :Graphs
-                      (mapv :GraphUUID)
-                      set
-                      (#(contains? % local-graph-uuid)))]
+    (let [r (<! (<list-remote-graphs remoteapi))
+          result
+          (or
+           ;; if api call failed, assume this remote graph still exists
+           (instance? ExceptionInfo r)
+           (and
+            (contains? r :Graphs)
+            (->> (:Graphs r)
+                 (mapv :GraphUUID)
+                 set
+                 (#(contains? % local-graph-uuid)))))]
+
       (when-not result
         (notification/show! (t :file-sync/graph-deleted) :warning false))
       result)))
 
+(declare network-online-cursor)
+
 (defn sync-start []
   (let [*sync-state                 (atom (sync-state))
         current-user-uuid           (user/user-uuid)
         repo                        (state/get-current-repo)]
     (go
-      ;; stop previous sync
-      (<! (<sync-stop))
-
-      (<! (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?)
-                   repo
-                   (not (config/demo-graph? repo)))
-          (when-some [sm (sync-manager-singleton current-user-uuid graph-uuid
-                                                 (config/get-repo-dir repo) repo
-                                                 txid *sync-state)]
-            (when (check-graph-belong-to-current-user current-user-uuid user-uuid)
-              (if-not (<! (<check-remote-graph-exists graph-uuid)) ; remote graph has been deleted
-                (clear-graphs-txid! repo)
-                (do
-                  (state/set-file-sync-state repo @*sync-state)
-                  (state/set-file-sync-manager sm)
+      (when @network-online-cursor
+        ;; stop previous sync
+        (<! (<sync-stop))
+
+        (<! (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?)
+                     repo
+                     (not (config/demo-graph? repo)))
+            (when-some [sm (sync-manager-singleton current-user-uuid graph-uuid
+                                                   (config/get-repo-dir repo) repo
+                                                   txid *sync-state)]
+              (when (check-graph-belong-to-current-user current-user-uuid user-uuid)
+                (if-not (<! (<check-remote-graph-exists graph-uuid)) ; remote graph has been deleted
+                  (clear-graphs-txid! repo)
+                  (do
+                    (state/set-file-sync-state repo @*sync-state)
+                    (state/set-file-sync-manager sm)
 
-                  ;; update global state when *sync-state changes
-                  (add-watch *sync-state ::update-global-state
-                             (fn [_ _ _ n]
-                               (state/set-file-sync-state repo n)))
+                    ;; update global state when *sync-state changes
+                    (add-watch *sync-state ::update-global-state
+                               (fn [_ _ _ n]
+                                 (state/set-file-sync-state repo n)))
 
-                  (.start sm)
+                    (.start sm)
 
-                  (offer! remote->local-full-sync-chan true)
-                  (offer! full-sync-chan true))))))))))
+                    (offer! remote->local-full-sync-chan true)
+                    (offer! full-sync-chan true)))))))))))
 
 ;;; ### some add-watches
 
 ;; TOOD: replace this logic by pause/resume state
-(def network-online-cursor (rum/cursor state/state :network/online?))
+(defonce network-online-cursor (rum/cursor state/state :network/online?))
 (add-watch network-online-cursor "sync-manage"
            (fn [_k _r o n]
              (cond
@@ -2790,7 +2800,7 @@
                :else
                nil)))
 
-(def auth-id-token-cursor (rum/cursor state/state :auth/id-token))
+(defonce auth-id-token-cursor (rum/cursor state/state :auth/id-token))
 (add-watch auth-id-token-cursor "sync-manage"
            (fn [_k _r _o n]
              (when (nil? n)

+ 8 - 6
src/main/frontend/fs/watcher_handler.cljs

@@ -45,10 +45,12 @@
     (db/set-file-last-modified-at! repo path mtime)))
 
 (defn handle-changed!
-  [type {:keys [dir path content stat] :as payload}]
+  [type {:keys [dir path content stat global-dir] :as payload}]
   (when dir
     (let [path (gp-util/path-normalize path)
-          repo (config/get-local-repo dir)
+          ;; Global directory events don't know their originating repo so we rely
+          ;; on the client to correctly identify it
+          repo (if global-dir (state/get-current-repo) (config/get-local-repo dir))
           pages-metadata-path (config/get-pages-metadata-path)
           {:keys [mtime]} stat
           db-content (or (db/get-file repo path) "")]
@@ -90,10 +92,10 @@
           (and (= "unlink" type)
                (db/file-exists? repo path))
           (p/let [dir-exists? (fs/file-exists? dir "")]
-            (when dir-exists?
-              (when-let [page-name (db/get-file-page path)]
-                (println "Delete page: " page-name ", file path: " path ".")
-                (page-handler/delete! page-name #() :delete-file? false))))
+                 (when dir-exists?
+                   (when-let [page-name (db/get-file-page path)]
+                     (println "Delete page: " page-name ", file path: " path ".")
+                     (page-handler/delete! page-name #() :delete-file? false))))
 
           (and (contains? #{"add" "change" "unlink"} type)
                (string/ends-with? path "logseq/custom.css"))

+ 82 - 85
src/main/frontend/handler.cljs

@@ -24,6 +24,8 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
+            [frontend.handler.repo-config :as repo-config-handler]
+            [frontend.handler.global-config :as global-config-handler]
             [frontend.idb :as idb]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
@@ -31,14 +33,12 @@
             [frontend.modules.outliner.file :as file]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.state :as state]
-            [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.persist-var :as persist-var]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [promesa.core :as p]
-            [logseq.db.schema :as db-schema]))
+            [promesa.core :as p]))
 
 (defn set-global-error-notification!
   []
@@ -76,51 +76,48 @@
     (state/pub-event! [:instrument {:type :blocks/count
                                     :payload {:total (db/blocks-count)}}])))
 
-(defn store-schema!
-  []
-  (storage/set :db-schema (assoc db-schema/schema
-                                 :db/version db-schema/version)))
-
 (defn restore-and-setup!
-  [repos old-db-schema]
-  (-> (db/restore!
-       {:repos repos}
-       old-db-schema
-       (fn [repo]
-         (file-handler/restore-config! repo)))
-      (p/then
-       (fn []
-         ;; try to load custom css only for current repo
-         (ui-handler/add-style-if-exists!)
-
-         ;; install after config is restored
-         (shortcut/unlisten-all)
-         (shortcut/refresh!)
-
-         (cond
-           (and (not (seq (db/get-files config/local-repo)))
-                ;; Not native local directory
-                (not (some config/local-db? (map :url repos)))
-                (not (mobile-util/native-platform?)))
-           ;; will execute `(state/set-db-restoring! false)` inside
-           (repo-handler/setup-local-repo-if-not-exists!)
-
-           :else
-           (state/set-db-restoring! false))))
-      (p/then
-       (fn []
-         (js/console.log "db restored, setting up repo hooks")
-         (store-schema!)
-
-         (state/pub-event! [:modal/nfs-ask-permission])
-
-         (page-handler/init-commands!)
-
-         (watch-for-date!)
-         (file-handler/watch-for-current-graph-dir!)
-         (state/pub-event! [:graph/restored (state/get-current-repo)])))
-      (p/catch (fn [error]
-                 (log/error :exception error)))))
+  [repos]
+  (when-let [repo (or (state/get-current-repo) (:url (first repos)))]
+    (-> (db/restore! repo)
+        (p/then
+         (fn []
+           ;; try to load custom css only for current repo
+           (ui-handler/add-style-if-exists!)
+
+           (->
+            (p/do! (repo-config-handler/start {:repo repo})
+                   (when (config/global-config-enabled?)
+                        (global-config-handler/start {:repo repo})))
+            (p/finally
+              (fn []
+                ;; install after config is restored
+                (shortcut/unlisten-all)
+                (shortcut/refresh!)
+
+                (cond
+                  (and (not (seq (db/get-files config/local-repo)))
+                       ;; Not native local directory
+                       (not (some config/local-db? (map :url repos)))
+                       (not (mobile-util/native-platform?)))
+                  ;; will execute `(state/set-db-restoring! false)` inside
+                  (repo-handler/setup-local-repo-if-not-exists!)
+
+                  :else
+                  (state/set-db-restoring! false)))))))
+        (p/then
+         (fn []
+           (js/console.log "db restored, setting up repo hooks")
+
+           (state/pub-event! [:modal/nfs-ask-permission])
+
+           (page-handler/init-commands!)
+
+           (watch-for-date!)
+           (file-handler/watch-for-current-graph-dir!)
+           (state/pub-event! [:graph/restored (state/get-current-repo)])))
+        (p/catch (fn [error]
+                   (log/error :exception error))))))
 
 (defn- handle-connection-change
   [e]
@@ -199,44 +196,44 @@
 (defn start!
   [render]
   (set-global-error-notification!)
-  (let [db-schema (storage/get :db-schema)]
-    (register-components-fns!)
-    (state/set-db-restoring! true)
-    (render)
-    (i18n/start)
-    (instrument/init)
-    (set-network-watcher!)
-
-    (util/indexeddb-check?
-     (fn [_error]
-       (notification/show! "Sorry, it seems that your browser doesn't support IndexedDB, we recommend to use latest Chrome(Chromium) or Firefox(Non-private mode)." :error false)
-       (state/set-indexedb-support! false)))
-
-    (react/run-custom-queries-when-idle!)
-
-    (events/run!)
-
-    (-> (p/let [repos (get-repos)]
-          (state/set-repos! repos)
-          (restore-and-setup! repos db-schema))
-        (p/catch (fn [e]
-                   (js/console.error "Error while restoring repos: " e)))
-        (p/finally (fn []
-                     (state/set-db-restoring! false))))
-    (when (mobile-util/native-platform?)
-      (p/do! (mobile-util/hide-splash)))
-
-    (db/run-batch-txs!)
-    (file/<ratelimit-file-writes!)
-
-    (when config/dev?
-      (enable-datalog-console))
-    (when (util/electron?)
-      (el/listen!))
-    (persist-var/load-vars)
-    (user-handler/restore-tokens-from-localstorage)
-    (user-handler/refresh-tokens-loop)
-    (js/setTimeout instrument! (* 60 1000))))
+  (register-components-fns!)
+  (state/set-db-restoring! true)
+  (render)
+  (i18n/start)
+  (instrument/init)
+  (state/set-online! js/navigator.onLine)
+  (set-network-watcher!)
+
+  (util/indexeddb-check?
+   (fn [_error]
+     (notification/show! "Sorry, it seems that your browser doesn't support IndexedDB, we recommend to use latest Chrome(Chromium) or Firefox(Non-private mode)." :error false)
+     (state/set-indexedb-support! false)))
+
+  (react/run-custom-queries-when-idle!)
+
+  (events/run!)
+
+  (-> (p/let [repos (get-repos)]
+        (state/set-repos! repos)
+        (restore-and-setup! repos))
+      (p/catch (fn [e]
+                 (js/console.error "Error while restoring repos: " e)))
+      (p/finally (fn []
+                   (state/set-db-restoring! false))))
+  (when (mobile-util/native-platform?)
+    (p/do! (mobile-util/hide-splash)))
+
+  (db/run-batch-txs!)
+  (file/<ratelimit-file-writes!)
+
+  (when config/dev?
+    (enable-datalog-console))
+  (when (util/electron?)
+    (el/listen!))
+  (persist-var/load-vars)
+  (user-handler/restore-tokens-from-localstorage)
+  (user-handler/refresh-tokens-loop)
+  (js/setTimeout instrument! (* 60 1000)))
 
 (defn stop! []
   (prn "stop!"))

+ 1 - 32
src/main/frontend/handler/common.cljs

@@ -2,16 +2,13 @@
   (:require [cljs-bean.core :as bean]
             [cljs.reader :as reader]
             [clojure.string :as string]
-            [frontend.config :as config]
             [frontend.date :as date]
-            [frontend.db :as db]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util.property :as property]
             [goog.object :as gobj]
             ["ignore" :as Ignore]
-            [lambdaisland.glogi :as log]
-            [borkdude.rewrite-edn :as rewrite]))
+            [lambdaisland.glogi :as log]))
 
 (defn copy-to-clipboard-without-id-property!
   [format raw-text html]
@@ -50,10 +47,6 @@
                 (hidden? path patterns))) files)
     files))
 
-(defn get-config
-  [repo-url]
-  (db/get-file repo-url (config/get-config-path)))
-
 (defn safe-read-string
   [content error-message-or-handler]
   (try
@@ -65,20 +58,6 @@
         (println error-message-or-handler))
       {})))
 
-(defn read-config
-  [content]
-  (safe-read-string content
-                    (fn [_e]
-                      (state/pub-event! [:backup/broken-config (state/get-current-repo) content])
-                      (reader/read-string config/config-default-content))))
-
-(defn reset-config!
-  [repo-url content]
-  (when-let [content (or content (get-config repo-url))]
-    (let [config (read-config content)]
-      (state/set-config! repo-url config)
-      config)))
-
 (defn read-metadata!
   [content]
   (try
@@ -118,16 +97,6 @@
   (let [position [(gobj/get e "clientX") (gobj/get e "clientY")]]
     (state/show-custom-context-menu! context-menu-content position)))
 
-(defn parse-config
-  "Parse configuration from file `content` such as from config.edn."
-  [content]
-  (try
-    (rewrite/parse-string content)
-    (catch :default e
-      (log/error :parse/config-failed e)
-      (state/pub-event! [:backup/broken-config (state/get-current-repo) content])
-      (rewrite/parse-string config/config-default-content))))
-
 (defn listen-to-scroll!
   [element]
   (let [*scroll-timer (atom nil)]

+ 79 - 0
src/main/frontend/handler/common/file.cljs

@@ -0,0 +1,79 @@
+(ns frontend.handler.common.file
+  "Common file related fns for handlers"
+  (:require [frontend.util :as util]
+            [frontend.config :as config]
+            [frontend.state :as state]
+            [frontend.db :as db]
+            ["/frontend/utils" :as utils]
+            [frontend.mobile.util :as mobile-util]
+            [logseq.graph-parser :as graph-parser]
+            [logseq.graph-parser.util :as gp-util]
+            [logseq.graph-parser.config :as gp-config]
+            [lambdaisland.glogi :as log]))
+
+(defn- page-exists-in-another-file
+  "Conflict of files towards same page"
+  [repo-url page file]
+  (when-let [page-name (:block/name page)]
+    (let [current-file (:file/path (db/get-page-file repo-url page-name))]
+      (when (not= file current-file)
+        current-file))))
+
+(defn- get-delete-blocks [repo-url first-page file]
+  (let [delete-blocks (->
+                       (concat
+                        (db/delete-file-blocks! repo-url file)
+                        (when first-page (db/delete-page-blocks repo-url (:block/name first-page))))
+                       (distinct))]
+    (when-let [current-file (page-exists-in-another-file repo-url first-page file)]
+      (when (not= file current-file)
+        (let [error (str "Page already exists with another file: " current-file ", current file: " file)]
+          (state/pub-event! [:notification/show
+                             {:content error
+                              :status :error
+                              :clear? false}]))))
+    delete-blocks))
+
+(defn reset-file!
+  "Main fn for updating a db with the results of a parsed file"
+  ([repo-url file content]
+   (reset-file! repo-url file content {}))
+  ([repo-url file content {:keys [verbose] :as options}]
+   (try
+     (let [electron-local-repo? (and (util/electron?)
+                                     (config/local-db? repo-url))
+           file (cond
+                  (and electron-local-repo?
+                       util/win32?
+                       (utils/win32 file))
+                  file
+
+                  (and electron-local-repo? (or
+                                             util/win32?
+                                             (not= "/" (first file))))
+                  (str (config/get-repo-dir repo-url) "/" file)
+
+                  (and (mobile-util/native-android?) (not= "/" (first file)))
+                  file
+
+                  (and (mobile-util/native-ios?) (not= "/" (first file)))
+                  file
+
+                  :else
+                  file)
+           file (gp-util/path-normalize file)
+           new? (nil? (db/entity [:file/path file]))
+           options (merge (dissoc options :verbose)
+                          {:new? new?
+                           :delete-blocks-fn (partial get-delete-blocks repo-url)
+                           :extract-options (merge
+                                             {:user-config (state/get-config)
+                                              :date-formatter (state/get-date-formatter)
+                                              :page-name-order (state/page-name-order)
+                                              :block-pattern (config/get-block-pattern (gp-util/get-format file))
+                                              :supported-formats (gp-config/supported-formats)}
+                                             (when (some? verbose) {:verbose verbose}))})]
+       (:tx (graph-parser/parse-file (db/get-db repo-url false) file content options)))
+     (catch :default e
+       (prn "Reset file failed " {:file file})
+       (log/error :exception e)))))

+ 30 - 3
src/main/frontend/handler/config.cljs

@@ -1,12 +1,39 @@
 (ns frontend.handler.config
+  "Fns for setting repo config"
   (:require [frontend.state :as state]
             [frontend.handler.file :as file-handler]
-            [frontend.config :as config]))
+            [frontend.handler.repo-config :as repo-config-handler]
+            [frontend.config :as config]
+            [frontend.db :as db]
+            [borkdude.rewrite-edn :as rewrite]
+            [lambdaisland.glogi :as log]))
+
+(defn parse-repo-config
+  "Parse repo configuration file content"
+  [content]
+  (try
+    (rewrite/parse-string content)
+    (catch :default e
+      (log/error :parse/config-failed e)
+      (state/pub-event! [:backup/broken-config (state/get-current-repo) content])
+      (rewrite/parse-string config/config-default-content))))
+
+(defn- repo-config-set-key-value
+  [path k v]
+  (when-let [repo (state/get-current-repo)]
+    (when-let [content (db/get-file path)]
+      (repo-config-handler/read-repo-config repo content)
+      (let [result (parse-repo-config content)
+            ks (if (vector? k) k [k])
+            new-result (rewrite/assoc-in result ks v)
+            new-content (str new-result)]
+        (file-handler/set-file-content! repo path new-content)))))
 
 (defn set-config!
+  "Sets config state for repo-specific config"
   [k v]
-  (let [path (config/get-config-path)]
-    (file-handler/edn-file-set-key-value path k v)))
+  (let [path (config/get-repo-config-path)]
+    (repo-config-set-key-value path k v)))
 
 (defn toggle-ui-show-brackets! []
   (let [show-brackets? (state/show-brackets?)]

+ 26 - 19
src/main/frontend/handler/events.cljs

@@ -32,6 +32,7 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.repo :as repo-handler]
+            [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.search :as search-handler]
             [frontend.handler.ui :as ui-handler]
@@ -72,20 +73,23 @@
   (state/set-state! [:ui/loading? :login] false)
   (async/go
     (let [result (async/<! (sync/<user-info sync/remoteapi))]
-      (when (seq result)
-        (state/set-state! :user/info result)
-
-        (let [status (if (user-handler/alpha-user?) :welcome :unavailable)]
-          (when (= status :welcome)
-            (async/<! (file-sync-handler/load-session-graphs))
-            (p/let [repos (repo-handler/refresh-repos!)]
-              (when-let [repo (state/get-current-repo)]
-                (when (some #(and (= (:url %) repo)
-                                 (vector? (:sync-meta %))
-                                 (util/uuid-string? (first (:sync-meta %)))
-                                 (util/uuid-string? (second (:sync-meta %)))) repos)
-                 (file-sync-restart!)))))
-          (file-sync/maybe-onboarding-show status))))))
+      (cond
+        (instance? ExceptionInfo result)
+        nil
+        (map? result)
+        (do
+          (state/set-state! :user/info result)
+          (let [status (if (user-handler/alpha-user?) :welcome :unavailable)]
+            (when (= status :welcome)
+              (async/<! (file-sync-handler/load-session-graphs))
+              (p/let [repos (repo-handler/refresh-repos!)]
+                (when-let [repo (state/get-current-repo)]
+                  (when (some #(and (= (:url %) repo)
+                                    (vector? (:sync-meta %))
+                                    (util/uuid-string? (first (:sync-meta %)))
+                                    (util/uuid-string? (second (:sync-meta %)))) repos)
+                    (file-sync-restart!)))))
+            (file-sync/maybe-onboarding-show status)))))))
 
 (defmethod handle :user/logout [[_]]
   (file-sync-handler/reset-session-graphs)
@@ -113,7 +117,7 @@
      (do
        (state/set-current-repo! graph)
        ;; load config
-       (common-handler/reset-config! graph nil)
+       (repo-config-handler/restore-repo-config! graph)
        (st/refresh!)
        (when-not (= :draw (state/get-current-route))
          (route-handler/redirect-to-home!))
@@ -166,13 +170,13 @@
    (file-sync/pick-page-histories-panel graph-uuid page-name)
    {:id :page-histories :label "modal-page-histories"}))
 
-(defmethod handle :graph/open-new-window [[ev repo]]
+(defmethod handle :graph/open-new-window [[_ev repo]]
   (p/let [current-repo (state/get-current-repo)
           target-repo (or repo current-repo)
           _ (repo-handler/persist-db! current-repo persist-db-noti-m) ;; FIXME: redundant when opening non-current-graph window
           _ (when-not (= current-repo target-repo)
               (repo-handler/broadcast-persist-db! repo))]
-    (ui-handler/open-new-window! ev repo)))
+    (ui-handler/open-new-window! repo)))
 
 (defmethod handle :graph/migrated [[_ _repo]]
   (js/alert "Graph migrated."))
@@ -451,7 +455,7 @@
               (state/set-current-repo! current-repo)
               (db/listen-and-persist! current-repo)
               (db/persist-if-idle! current-repo)
-              (file-handler/restore-config! current-repo)
+              (repo-config-handler/restore-repo-config! current-repo)
               (.watch mobile-util/fs-watcher #js {:path current-repo-dir})
               (when graph-switch-f (graph-switch-f current-repo true))
               (file-sync-restart!))))
@@ -503,7 +507,7 @@
 
 (defmethod handle :backup/broken-config [[_ repo content]]
   (when (and repo content)
-    (let [path (config/get-config-path)
+    (let [path (config/get-repo-config-path)
           broken-path (string/replace path "/config.edn" "/broken-config.edn")]
       (p/let [_ (fs/write-file! repo (config/get-repo-dir repo) broken-path content {})
               _ (file-handler/alter-file repo path config/config-default-content {:skip-compare? true})]
@@ -524,6 +528,9 @@
 (defmethod handle :rebuild-slash-commands-list [[_]]
   (page-handler/rebuild-slash-commands-list!))
 
+(defmethod handle :shortcut/refresh [[_]]
+  (st/refresh!))
+
 (defn- refresh-cb []
   (page-handler/create-today-journal!)
   (st/refresh!)

+ 51 - 119
src/main/frontend/handler/file.cljs

@@ -1,13 +1,13 @@
 (ns frontend.handler.file
   (:refer-clojure :exclude [load-file])
-  (:require ["/frontend/utils" :as utils]
-            [borkdude.rewrite-edn :as rewrite]
-            [frontend.config :as config]
+  (:require [frontend.config :as config]
             [frontend.db :as db]
             [frontend.fs :as fs]
             [frontend.fs.nfs :as nfs]
             [frontend.fs.capacitor-fs :as capacitor-fs]
-            [frontend.handler.common :as common-handler]
+            [frontend.handler.common.file :as file-common-handler]
+            [frontend.handler.repo-config :as repo-config-handler]
+            [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
             [frontend.util :as util]
@@ -15,9 +15,9 @@
             [electron.ipc :as ipc]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]
-            [frontend.mobile.util :as mobile]
+            [frontend.mobile.util :as mobile-util]
             [logseq.graph-parser.config :as gp-config]
-            [logseq.graph-parser :as graph-parser]))
+            ["path" :as path]))
 
 ;; TODO: extract all git ops using a channel
 
@@ -52,15 +52,6 @@
   [files]
   (keep-formats files (gp-config/img-formats)))
 
-(defn restore-config!
-  ([repo-url]
-   (restore-config! repo-url nil))
-  ([repo-url config-content]
-   (let [config-content (if config-content config-content
-                            (common-handler/get-config repo-url))]
-     (when config-content
-       (common-handler/reset-config! repo-url config-content)))))
-
 (defn load-files-contents!
   [repo-url files ok-handler]
   (let [images (only-image-formats files)
@@ -87,85 +78,16 @@
     (util/electron?)
     (ipc/ipc "backupDbFile" repo-url path db-content content)
 
-    (mobile/native-platform?)
+    (mobile-util/native-platform?)
     (capacitor-fs/backup-file-handle-changed! repo-url path db-content)
 
     :else
     nil))
 
-(defn- page-exists-in-another-file
-  "Conflict of files towards same page"
-  [repo-url page file]
-  (when-let [page-name (:block/name page)]
-    (let [current-file (:file/path (db/get-page-file repo-url page-name))]
-      (when (not= file current-file)
-        current-file))))
-
-(defn- get-delete-blocks [repo-url first-page file]
-  (let [delete-blocks (->
-                       (concat
-                        (db/delete-file-blocks! repo-url file)
-                        (when first-page (db/delete-page-blocks repo-url (:block/name first-page))))
-                       (distinct))]
-    (when-let [current-file (page-exists-in-another-file repo-url first-page file)]
-      (when (not= file current-file)
-        (let [error (str "Page already exists with another file: " current-file ", current file: " file)]
-          (state/pub-event! [:notification/show
-                             {:content error
-                              :status :error
-                              :clear? false}]))))
-    delete-blocks))
-
-(defn reset-file!
-  ([repo-url file content]
-   (reset-file! repo-url file content {}))
-  ([repo-url file content {:keys [verbose] :as options}]
-   (try
-     (let [electron-local-repo? (and (util/electron?)
-                                    (config/local-db? repo-url))
-          file (cond
-                 (and electron-local-repo?
-                      util/win32?
-                      (utils/win32 file))
-                 file
-
-                 (and electron-local-repo? (or
-                                            util/win32?
-                                            (not= "/" (first file))))
-                 (str (config/get-repo-dir repo-url) "/" file)
-
-                 (and (mobile/native-android?) (not= "/" (first file)))
-                 file
-
-                 (and (mobile/native-ios?) (not= "/" (first file)))
-                 file
-
-                 :else
-                 file)
-          file (gp-util/path-normalize file)
-          new? (nil? (db/entity [:file/path file]))]
-      (:tx
-       (graph-parser/parse-file
-        (db/get-db repo-url false)
-        file
-        content
-        (merge (dissoc options :verbose)
-               {:new? new?
-                :delete-blocks-fn (partial get-delete-blocks repo-url)
-                :extract-options (merge
-                                  {:user-config (state/get-config)
-                                   :date-formatter (state/get-date-formatter)
-                                   :page-name-order (state/page-name-order)
-                                   :block-pattern (config/get-block-pattern (gp-util/get-format file))
-                                   :supported-formats (gp-config/supported-formats)}
-                                  (when (some? verbose) {:verbose verbose}))}))))
-     (catch :default e
-       (prn "Reset file failed " {:file file})
-       (log/error :exception e)))))
-
 ;; TODO: Remove this function in favor of `alter-files`
 (defn alter-file
-  [repo path content {:keys [reset? re-render-root? from-disk? skip-compare? new-graph? verbose]
+  [repo path content {:keys [reset? re-render-root? from-disk? skip-compare? new-graph? verbose
+                             skip-db-transact?]
                       :or {reset? true
                            re-render-root? false
                            from-disk? false
@@ -173,31 +95,50 @@
   (let [original-content (db/get-file repo path)
         write-file! (if from-disk?
                       #(p/resolved nil)
-                      #(fs/write-file! repo (config/get-repo-dir repo) path content
-                                       (assoc (when original-content {:old-content original-content})
-                                              :skip-compare? skip-compare?)))
+                      #(let [path-dir (if (= (path/dirname path) (global-config-handler/global-config-dir))
+                                        (global-config-handler/global-config-dir)
+                                        (config/get-repo-dir repo))]
+                         (fs/write-file! repo path-dir path content
+                                        (assoc (when original-content {:old-content original-content})
+                                               :skip-compare? skip-compare?))))
         opts {:new-graph? new-graph?
-              :from-disk? from-disk?}]
-    (if reset?
-      (do
-        (when-let [page-id (db/get-file-page-id path)]
-          (db/transact! repo
-            [[:db/retract page-id :block/alias]
-             [:db/retract page-id :block/tags]]
-            opts))
-        (reset-file! repo path content (merge opts
-                                              (when (some? verbose) {:verbose verbose}))))
-      (db/set-file-content! repo path content opts))
+              :from-disk? from-disk?
+              :skip-db-transact? skip-db-transact?}
+        result (if reset?
+                 (do
+                   (when-not skip-db-transact?
+                     (when-let [page-id (db/get-file-page-id path)]
+                       (db/transact! repo
+                         [[:db/retract page-id :block/alias]
+                          [:db/retract page-id :block/tags]]
+                         opts)))
+                   (file-common-handler/reset-file! repo path content (merge opts
+                                                         (when (some? verbose) {:verbose verbose}))))
+                 (db/set-file-content! repo path content opts))]
     (util/p-handle (write-file!)
                    (fn [_]
-                     (when (= path (config/get-config-path repo))
-                       (restore-config! repo))
-                     (when (= path (config/get-custom-css-path repo))
+                     (cond
+                       (= path (config/get-repo-config-path repo))
+                       (p/let [_ (repo-config-handler/restore-repo-config! repo)]
+                         (state/pub-event! [:shortcut/refresh]))
+
+                       (= path (global-config-handler/global-config-path))
+                       (p/let [_ (global-config-handler/restore-global-config!)]
+                         (state/pub-event! [:shortcut/refresh]))
+
+                       (= path (config/get-custom-css-path repo))
                        (ui-handler/add-style-if-exists!))
+
                      (when re-render-root? (ui-handler/re-render-root!)))
                    (fn [error]
+                     (when (= path (global-config-handler/global-config-path))
+                       (state/pub-event! [:notification/show
+                                         {:content (str "Failed to write to file " path)
+                                          :status :error}]))
+
                      (println "Write file failed, path: " path ", content: " content)
-                     (log/error :write/failed error)))))
+                     (log/error :write/failed error)))
+    result))
 
 (defn set-file-content!
   [repo path new-content]
@@ -251,7 +192,7 @@
     (when update-db?
       (doseq [[path content] files]
         (if reset?
-          (reset-file! repo path content)
+          (file-common-handler/reset-file! repo path content)
           (db/set-file-content! repo path content))))
     (alter-files-handler! repo files opts file->content)))
 
@@ -259,6 +200,8 @@
   []
   (when-let [repo (state/get-current-repo)]
     (when-let [dir (config/get-repo-dir repo)]
+      ;; An unwatch shouldn't be needed on startup. However not having this
+      ;; after an app refresh can cause stale page data to load
       (fs/unwatch-dir! dir)
       (fs/watch-dir! dir))))
 
@@ -271,7 +214,7 @@
     (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
-        (reset-file! repo-url path default-content)))))
+        (file-common-handler/reset-file! repo-url path default-content)))))
 
 (defn create-pages-metadata-file
   [repo-url]
@@ -282,15 +225,4 @@
     (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
-        (reset-file! repo-url path default-content)))))
-
-(defn edn-file-set-key-value
-  [path k v]
-  (when-let [repo (state/get-current-repo)]
-    (when-let [content (db/get-file path)]
-      (common-handler/read-config content)
-      (let [result (common-handler/parse-config content)
-            ks (if (vector? k) k [k])
-            new-result (rewrite/assoc-in result ks v)
-            new-content (str new-result)]
-        (set-file-content! repo path new-content)))))
+        (file-common-handler/reset-file! repo-url path default-content)))))

+ 65 - 0
src/main/frontend/handler/global_config.cljs

@@ -0,0 +1,65 @@
+(ns frontend.handler.global-config
+  "This ns is a system component that encapsulates global config functionality.
+  Unlike repo config, this also manages a directory for configuration. This
+  component depends on a repo."
+  (:require [frontend.fs :as fs]
+            [frontend.handler.common.file :as file-common-handler]
+            [frontend.state :as state]
+            [cljs.reader :as reader]
+            [promesa.core :as p]
+            [shadow.resource :as rc]
+            [electron.ipc :as ipc]
+            ["path" :as path]))
+
+;; Use defonce to avoid broken state on dev reload
+;; Also known as home directory a.k.a. '~'
+(defonce root-dir
+  (atom nil))
+
+(defn global-config-dir
+  []
+  (path/join @root-dir "config"))
+
+(defn global-config-path
+  []
+  (path/join @root-dir "config" "config.edn"))
+
+(defn- set-global-config-state!
+  [content]
+  (let [config (reader/read-string content)]
+    (state/set-global-config! config)
+    config))
+
+(def default-content (rc/inline "global-config.edn"))
+
+(defn- create-global-config-file-if-not-exists
+  [repo-url]
+  (let [config-dir (global-config-dir)
+        config-path (global-config-path)]
+    (p/let [_ (fs/mkdir-if-not-exists config-dir)
+            file-exists? (fs/create-if-not-exists repo-url config-dir config-path default-content)]
+           (when-not file-exists?
+             (file-common-handler/reset-file! repo-url config-path default-content)
+             (set-global-config-state! default-content)))))
+
+(defn restore-global-config!
+  "Sets global config state from config file"
+  []
+  (let [config-dir (global-config-dir)
+        config-path (global-config-path)]
+    (p/let [config-content (fs/read-file config-dir config-path)]
+      (set-global-config-state! config-content))))
+
+(defn start
+  "This component has four responsibilities on start:
+- Fetch root-dir for later use with config paths
+- Manage ui state of global config
+- Create a global config dir and file if it doesn't exist
+- Start a file watcher for global config dir if it's not already started.
+  Watcher ensures client db is seeded with correct file data."
+  [{:keys [repo]}]
+  (p/let [root-dir' (ipc/ipc "getLogseqDotDirRoot")
+          _ (reset! root-dir root-dir')
+          _ (restore-global-config!)
+          _ (create-global-config-file-if-not-exists repo)
+          _ (fs/watch-dir! (global-config-dir) {:global-dir true})]))

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

@@ -249,10 +249,10 @@
         new-tag (if (re-find #"[\s\t]+" new-name)
                   (util/format "#[[%s]]" new-name)
                   (str "#" new-name))]
-    ;; hash tag parsing rules https://github.com/logseq/mldoc/blob/701243eaf9b4157348f235670718f6ad19ebe7f8/test/test_markdown.ml#L631 
+    ;; hash tag parsing rules https://github.com/logseq/mldoc/blob/701243eaf9b4157348f235670718f6ad19ebe7f8/test/test_markdown.ml#L631
     ;; Safari doesn't support look behind, don't use
     ;; TODO: parse via mldoc
-    (string/replace content 
+    (string/replace content
                     (re-pattern (str "(?i)(^|\\s)(" (util/escape-regex-chars old-tag) ")(?=[,\\.]*($|\\s))"))
                     ;;    case_insense^    ^lhs   ^_grp2                       look_ahead^         ^_grp3
                     (fn [[_match lhs _grp2 _grp3]]
@@ -330,7 +330,7 @@
 (defn toggle-favorite! []
   ;; NOTE: in journals or settings, current-page is nil
   (when-let [page-name (state/get-current-page)]
-   (let [favorites  (:favorites (state/sub-graph-config))
+   (let [favorites  (:favorites (state/sub-config))
          favorited? (contains? (set (map string/lower-case favorites))
                                (string/lower-case page-name))]
     (if favorited?

+ 65 - 49
src/main/frontend/handler/repo.cljs

@@ -9,9 +9,12 @@
             [frontend.fs.nfs :as nfs]
             [frontend.handler.common :as common-handler]
             [frontend.handler.file :as file-handler]
+            [frontend.handler.repo-config :as repo-config-handler]
+            [frontend.handler.common.file :as file-common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.metadata :as metadata-handler]
+            [frontend.handler.global-config :as global-config-handler]
             [frontend.idb :as idb]
             [frontend.search :as search]
             [frontend.spec :as spec]
@@ -28,26 +31,13 @@
             [cljs-bean.core :as bean]
             [clojure.core.async :as async]
             [frontend.encrypt :as encrypt]
-            [frontend.mobile.util :as mobile-util]))
+            [frontend.mobile.util :as mobile-util]
+            [medley.core :as medley]))
 
 ;; Project settings should be checked in two situations:
 ;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
 ;; 2. Git pulls the new change (fn: load-files)
 
-(defn create-config-file-if-not-exists
-  [repo-url]
-  (spec/validate :repos/url repo-url)
-  (let [repo-dir (config/get-repo-dir repo-url)
-        app-dir config/app-name
-        dir (str repo-dir "/" app-dir)]
-    (p/let [_ (fs/mkdir-if-not-exists dir)]
-      (let [default-content config/config-default-content
-            path (str app-dir "/" config/config-file)]
-        (p/let [file-exists? (fs/create-if-not-exists repo-url repo-dir (str app-dir "/" config/config-file) default-content)]
-          (when-not file-exists?
-            (file-handler/reset-file! repo-url path default-content)
-            (common-handler/reset-config! repo-url default-content)))))))
-
 (defn create-contents-file
   [repo-url]
   (spec/validate :repos/url repo-url)
@@ -67,7 +57,7 @@
         (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir pages-dir))
                 file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
           (when-not file-exists?
-            (file-handler/reset-file! repo-url path default-content)))))))
+            (file-common-handler/reset-file! repo-url path default-content)))))))
 
 (defn create-custom-theme
   [repo-url]
@@ -79,7 +69,7 @@
     (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir config/app-name))
             file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
       (when-not file-exists?
-        (file-handler/reset-file! repo-url path default-content)))))
+        (file-common-handler/reset-file! repo-url path default-content)))))
 
 (defn create-dummy-notes-page
   [repo-url content]
@@ -89,7 +79,7 @@
         file-path (str "/" path)]
     (p/let [_ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-pages-directory)))
             _file-exists? (fs/create-if-not-exists repo-url repo-dir file-path content)]
-      (file-handler/reset-file! repo-url path content))))
+      (file-common-handler/reset-file! repo-url path content))))
 
 (defn- create-today-journal-if-not-exists
   [repo-url {:keys [content]}]
@@ -123,7 +113,7 @@
                 _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-journals-directory)))
                 file-exists? (fs/file-exists? repo-dir file-path)]
           (when-not file-exists?
-            (p/let [_ (file-handler/reset-file! repo-url path content)]
+            (p/let [_ (file-common-handler/reset-file! repo-url path content)]
               (p/let [_ (fs/create-if-not-exists repo-url repo-dir file-path content)]
                 (when-not (state/editing?)
                   (ui-handler/re-render-root!)))))
@@ -140,7 +130,7 @@
              _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (str config/app-name "/" config/recycle-dir)))
              _ (fs/mkdir-if-not-exists (util/safe-path-join repo-dir (config/get-journals-directory)))
              _ (file-handler/create-metadata-file repo-url encrypted?)
-             _ (create-config-file-if-not-exists repo-url)
+             _ (repo-config-handler/create-config-file-if-not-exists repo-url)
              _ (create-contents-file repo-url)
              _ (create-custom-theme repo-url)]
        (state/pub-event! [:page/create-today-journal repo-url])))))
@@ -185,21 +175,27 @@
         file-paths [path]]
     (load-pages-metadata! repo file-paths files force?)))
 
+(defonce *file-tx (atom nil))
+
 (defn- parse-and-load-file!
-  [repo-url file {:keys [new-graph? verbose]}]
+  [repo-url file {:keys [new-graph? verbose skip-db-transact?]
+                  :or {skip-db-transact? true}}]
   (try
-    (file-handler/alter-file repo-url
-                             (:file/path file)
-                             (:file/content file)
-                             (merge {:new-graph? new-graph?
-                                     :re-render-root? false
-                                     :from-disk? true}
-                                    (when (some? verbose) {:verbose verbose})))
+    (reset! *file-tx
+            (file-handler/alter-file repo-url
+                                     (:file/path file)
+                                     (:file/content file)
+                                     (merge {:new-graph? new-graph?
+                                             :re-render-root? false
+                                             :from-disk? true
+                                             :skip-db-transact? skip-db-transact?}
+                                            (when (some? verbose) {:verbose verbose}))))
     (catch :default e
       (state/set-parsing-state! (fn [m]
                                   (update m :failed-parsing-files conj [(:file/path file) e])))))
   (state/set-parsing-state! (fn [m]
-                              (update m :finished inc))))
+                              (update m :finished inc)))
+  @*file-tx)
 
 (defn- after-parse
   [repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan]
@@ -221,8 +217,11 @@
   (let [supported-files (graph-parser/filter-files files)
         delete-data (->> (concat delete-files delete-blocks)
                          (remove nil?))
-        chan (async/to-chan! supported-files)
-        graph-added-chan (async/promise-chan)]
+        indexed-files (medley/indexed supported-files)
+        chan (async/to-chan! indexed-files)
+        graph-added-chan (async/promise-chan)
+        total (count supported-files)
+        large-graph? (> total 1000)]
     (when (seq delete-data) (db/transact! repo-url delete-data))
     (state/set-current-repo! repo-url)
     (state/set-parsing-state! {:total (count supported-files)})
@@ -231,18 +230,33 @@
       (do
         (doseq [file supported-files]
           (state/set-parsing-state! (fn [m]
-                                      (assoc m :current-parsing-file (:file/path file))))
-          (parse-and-load-file! repo-url file (select-keys opts [:new-graph? :verbose])))
+                                      (assoc m
+                                             :current-parsing-file (:file/path file))))
+          (parse-and-load-file! repo-url file (assoc
+                                               (select-keys opts [:new-graph? :verbose])
+                                               :skip-db-transact? false)))
         (after-parse repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan))
-      (async/go-loop []
-        (if-let [file (async/<! chan)]
-          (do
+      (async/go-loop [tx []]
+        (if-let [item (async/<! chan)]
+          (let [[idx file] item
+                yield-for-ui? (or (not large-graph?)
+                                  (zero? (rem idx 10))
+                                  (<= (- total idx) 10))]
             (state/set-parsing-state! (fn [m]
                                         (assoc m :current-parsing-file (:file/path file))))
-            (async/<! (async/timeout 10))
-            (parse-and-load-file! repo-url file (select-keys opts [:new-graph? :verbose]))
-            (recur))
-          (after-parse repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan))))
+
+            (when yield-for-ui? (async/<! (async/timeout 1)))
+
+            (let [result (parse-and-load-file! repo-url file (select-keys opts [:new-graph? :verbose]))
+                  tx' (concat tx result)
+                  tx' (if (zero? (rem (inc idx) 100))
+                        (do (db/transact! repo-url tx' {:from-disk? true})
+                            [])
+                        tx')]
+              (recur tx')))
+          (do
+            (when (seq tx) (db/transact! repo-url tx {:from-disk? true}))
+            (after-parse repo-url files file-paths db-encrypted? re-render? re-render-opts opts graph-added-chan)))))
     graph-added-chan))
 
 (defn- parse-files-and-create-default-files!
@@ -280,9 +294,9 @@
   (spec/validate :repos/url repo-url)
   (route-handler/redirect-to-home!)
   (state/set-parsing-state! {:graph-loading? true})
-  (let [config (or (when-let [content (some-> (first (filter #(= (config/get-config-path repo-url) (:file/path %)) nfs-files))
+  (let [config (or (when-let [content (some-> (first (filter #(= (config/get-repo-config-path repo-url) (:file/path %)) nfs-files))
                                               :file/content)]
-                     (common-handler/read-config content))
+                     (repo-config-handler/read-repo-config repo-url content))
                    (state/get-config repo-url))
         ;; NOTE: Use config while parsing. Make sure it's the corrent journal title format
         _ (state/set-config! repo-url config)
@@ -368,7 +382,7 @@
                (let [tutorial (t :tutorial/text)
                      tutorial (string/replace-first tutorial "$today" (date/today))]
                  (create-today-journal-if-not-exists repo {:content tutorial})))
-             (create-config-file-if-not-exists repo)
+             (repo-config-handler/create-config-file-if-not-exists repo)
              (create-contents-file repo)
              (create-custom-theme repo)
              (state/set-db-restoring! false)
@@ -390,12 +404,14 @@
   conn, or replace the conn in state with a new one."
   [repo]
   (p/let [_ (state/set-db-restoring! true)
-          _ (db/restore-graph! repo)]
-         (file-handler/restore-config! repo)
-         ;; Don't have to unlisten the old listerner, as it will be destroyed with the conn
-         (db/listen-and-persist! repo)
-         (ui-handler/add-style-if-exists!)
-         (state/set-db-restoring! false)))
+          _ (db/restore-graph! repo)
+          _ (repo-config-handler/restore-repo-config! repo)
+          _ (global-config-handler/restore-global-config!)]
+    ;; Don't have to unlisten the old listener, as it will be destroyed with the conn
+    (db/listen-and-persist! repo)
+    (state/pub-event! [:shortcut/refresh])
+    (ui-handler/add-style-if-exists!)
+    (state/set-db-restoring! false)))
 
 (defn rebuild-index!
   [url]

+ 63 - 0
src/main/frontend/handler/repo_config.cljs

@@ -0,0 +1,63 @@
+(ns frontend.handler.repo-config
+  "This ns is a system component that encapsulates repo config functionality.
+  This component only concerns itself with one user-facing repo config file,
+  logseq/config.edn. In the future it may manage more files. This component
+  depends on a repo."
+  (:require [frontend.db :as db]
+            [frontend.config :as config]
+            [frontend.state :as state]
+            [frontend.handler.common :as common-handler]
+            [frontend.handler.common.file :as file-common-handler]
+            [cljs.reader :as reader]
+            [frontend.fs :as fs]
+            [promesa.core :as p]
+            [frontend.spec :as spec]))
+
+(defn- get-repo-config-content
+  [repo-url]
+  (db/get-file repo-url (config/get-repo-config-path)))
+
+(defn read-repo-config
+  "Converts file content to edn and handles read failure by backing up file and
+  reverting to a default file"
+  [repo content]
+  (common-handler/safe-read-string
+   content
+   (fn [_e]
+     (state/pub-event! [:backup/broken-config repo content])
+     (reader/read-string config/config-default-content))))
+
+(defn set-repo-config-state!
+  "Sets repo config state using given file content"
+  [repo-url content]
+  (let [config (read-repo-config repo-url content)]
+    (state/set-config! repo-url config)
+    config))
+
+(defn create-config-file-if-not-exists
+  "Creates a default logseq/config.edn if it doesn't exist"
+  [repo-url]
+  (spec/validate :repos/url repo-url)
+  (let [repo-dir (config/get-repo-dir repo-url)
+        app-dir config/app-name
+        dir (str repo-dir "/" app-dir)]
+    (p/let [_ (fs/mkdir-if-not-exists dir)]
+           (let [default-content config/config-default-content
+                  path (str app-dir "/" config/config-file)]
+             (p/let [file-exists? (fs/create-if-not-exists repo-url repo-dir (str app-dir "/" config/config-file) default-content)]
+                    (when-not file-exists?
+                      (file-common-handler/reset-file! repo-url path default-content)
+                      (set-repo-config-state! repo-url default-content)))))))
+
+(defn restore-repo-config!
+  "Sets repo config state from db"
+  [repo-url]
+  (let [config-content (get-repo-config-content repo-url)]
+    (set-repo-config-state! repo-url config-content)))
+
+(defn start
+  "This component only has one reponsibility on start, to manage db and ui state
+  from repo config. It does not manage the repo directory, logseq/, as that is
+  loosely done by repo-handler"
+  [{:keys [repo]}]
+  (restore-repo-config! repo))

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

@@ -298,9 +298,9 @@
 (defn open-new-window!
   "Open a new Electron window.
    No db cache persisting ensured. Should be handled by the caller."
-  ([_e]
-   (open-new-window! _e nil))
-  ([_e repo]
+  ([]
+   (open-new-window! nil))
+  ([repo]
    ;; TODO: find out a better way to open a new window with a different repo path. Using local storage for now
    ;; TODO: also write local storage with the current repo state, to make behavior consistent
    ;; then we can remove the `openNewWindowOfGraph` ipcMain call

+ 14 - 12
src/main/frontend/handler/user.cljs

@@ -105,18 +105,20 @@
     (when-let [refresh-token (state/get-auth-refresh-token)]
       (let [resp (<! (http/get (str "https://" config/API-DOMAIN "/auth_refresh_token?refresh_token=" refresh-token)
                                {:with-credentials? false}))]
-        (if (= 400 (:status resp))
-          ;; invalid refresh_token
-          (do
-            (clear-tokens)
-            false)
-          (do
-            (->
-             resp
-             (as-> $ (and (http/unexceptional-status? (:status $)) $))
-             :body
-             (as-> $ (set-tokens! (:id_token $) (:access_token $))))
-            true))))))
+
+        (cond
+          ;; e.g. api return 500, server internal error
+          ;; we shouldn't clear tokens if they aren't expired yet
+          ;; the `refresh-tokens-loop` will retry soon
+          (and (not (http/unexceptional-status? (:status resp)))
+               (not (-> (state/get-auth-id-token) parse-jwt expired?)))
+          nil                           ; do nothing
+
+          (not (http/unexceptional-status? (:status resp)))
+          (clear-tokens)
+
+          :else                         ; ok
+          (set-tokens! (:id_token (:body resp)) (:access_token (:body resp))))))))
 
 (defn restore-tokens-from-localstorage
   "restore id-token, access-token, refresh-token from localstorage,

+ 35 - 25
src/main/frontend/handler/web/nfs.cljs

@@ -11,6 +11,7 @@
             [frontend.fs :as fs]
             [frontend.fs.nfs :as nfs]
             [frontend.handler.common :as common-handler]
+            [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.route :as route-handler]
             [frontend.idb :as idb]
@@ -38,10 +39,10 @@
                                   %) files)]
       (if-let [file (:file/file ignore-file)]
         (p/let [content (.text file)]
-          (when content
-            (let [paths (set (common-handler/ignore-files content (map :file/path files)))]
-              (when (seq paths)
-                (filter (fn [f] (contains? paths (:file/path f))) files)))))
+               (when content
+                 (let [paths (set (common-handler/ignore-files content (map :file/path files)))]
+                   (when (seq paths)
+                     (filter (fn [f] (contains? paths (:file/path f))) files)))))
         (p/resolved files))
       (p/resolved files))))
 
@@ -186,8 +187,8 @@
                                 (assoc file :file/content content))) markup-files))
                 (p/then (fn [result]
                           (p/let [files (map #(dissoc % :file/file) result)
-                                  graph-txid-meta (util-fs/read-graph-txid-info dir-name)
-                                  graph-uuid (and (vector? graph-txid-meta) (second graph-txid-meta))]
+                                  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)]
                               (state/pub-event!
                                [:notification/show
@@ -330,25 +331,34 @@
          (state/set-graph-syncing? true))
        (->
         (p/let [handle (when-not electron? (idb/get-item handle-path))]
-          (when (or handle electron? mobile-native?)   ; electron doesn't store the file handle
-            (p/let [_ (when handle (nfs/verify-permission repo handle true))
-                    files-result (fs/get-files (if nfs? handle
-                                                   (config/get-local-dir repo))
-                                               (fn [path handle]
-                                                 (when nfs?
-                                                   (swap! path-handles assoc path handle))))
-                    new-files (-> (->db-files mobile-native? electron? dir-name files-result)
-                                  (remove-ignore-files dir-name nfs?))
-                    _ (when nfs?
-                        (let [file-paths (set (map :file/path new-files))]
-                          (swap! path-handles (fn [handles]
-                                                (->> handles
-                                                     (filter (fn [[path _handle]]
-                                                               (contains? file-paths
-                                                                          (string/replace-first path (str dir-name "/") ""))))
-                                                     (into {})))))
-                        (set-files! @path-handles))]
-              (handle-diffs! repo nfs? old-files new-files handle-path path-handles re-index?))))
+               (when (or handle electron? mobile-native?)   ; electron doesn't store the file handle
+                 (p/let [_ (when handle (nfs/verify-permission repo handle true))
+                         local-files-result
+                         (fs/get-files (if nfs? handle
+                                         (config/get-local-dir repo))
+                                       (fn [path handle]
+                                         (when nfs?
+                                           (swap! path-handles assoc path handle))))
+                         global-dir (global-config-handler/global-config-dir)
+                         global-files-result (if (config/global-config-enabled?)
+                                               (fs/get-files global-dir (constantly nil))
+                                               [])
+                         new-local-files (-> (->db-files mobile-native? electron? dir-name local-files-result)
+                                             (remove-ignore-files dir-name nfs?))
+                         new-global-files (-> (->db-files mobile-native? electron? global-dir global-files-result)
+                                              (remove-ignore-files global-dir nfs?))
+                         new-files (concat new-local-files new-global-files)
+
+                         _ (when nfs?
+                             (let [file-paths (set (map :file/path new-files))]
+                               (swap! path-handles (fn [handles]
+                                                     (->> handles
+                                                          (filter (fn [[path _handle]]
+                                                                    (contains? file-paths
+                                                                               (string/replace-first path (str dir-name "/") ""))))
+                                                          (into {})))))
+                             (set-files! @path-handles))]
+                        (handle-diffs! repo nfs? old-files new-files handle-path path-handles re-index?))))
         (p/catch (fn [error]
                    (log/error :nfs/load-files-error repo)
                    (log/error :exception error)))

+ 2 - 2
src/main/frontend/modules/instrumentation/posthog.cljs

@@ -1,7 +1,7 @@
 (ns frontend.modules.instrumentation.posthog
   (:require [frontend.config :as config]
             [frontend.util :as util]
-            [frontend.mobile.util :as mobile]
+            [frontend.mobile.util :as mobile-util]
             [frontend.version :refer [version]]
             ["posthog-js" :as posthog]
             [cljs-bean.core :as bean]))
@@ -12,7 +12,7 @@
 (defn register []
   (posthog/register
    (clj->js
-    {:app_type (let [platform (mobile/platform)]
+    {:app_type (let [platform (mobile-util/platform)]
                  (cond
                    (util/electron?)
                    "electron"

+ 1 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -252,7 +252,7 @@
    :go/electron-find-in-page       {:binding "mod+f"
                                     :inactive (not (util/electron?))
                                     :fn      #(search-handler/open-find-in-page!)}
-   
+
    :go/electron-jump-to-the-next {:binding ["enter" "mod+g"]
                                   :inactive (not (util/electron?))
                                   :fn      #(search-handler/loop-find-in-page! false)}

+ 4 - 4
src/main/frontend/modules/shortcut/core.cljs

@@ -1,6 +1,6 @@
 (ns frontend.modules.shortcut.core
   (:require [clojure.string :as str]
-            [frontend.handler.config :as config]
+            [frontend.handler.config :as config-handler]
             [frontend.handler.notification :as notification]
             [frontend.modules.shortcut.data-helper :as dh]
             [frontend.modules.shortcut.config :as shortcut-config]
@@ -224,9 +224,9 @@
      (let [k (first args)
            keystroke (str/trim @local)]
        (when-not (empty? keystroke)
-         (config/set-config! :shortcuts (merge
-                                         (:shortcuts (state/get-config))
-                                         {k keystroke}))))
+         (config-handler/set-config! :shortcuts (merge
+                                                 (:shortcuts (state/get-config))
+                                                 {k keystroke}))))
 
      (when-let [^js handler (::key-record-handler state)]
        (.dispose handler))

+ 5 - 4
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -9,7 +9,8 @@
             [frontend.state :as state]
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
-            [frontend.handler.common :as common-handler])
+            [frontend.handler.repo-config :as repo-config-handler]
+            [frontend.handler.config :as config-handler])
   (:import [goog.ui KeyboardShortcutHandler]))
 
 (defn get-bindings
@@ -138,15 +139,15 @@
 
 (defn remove-shortcut [k]
   (let [repo (state/get-current-repo)
-        path (config/get-config-path)]
+        path (config/get-repo-config-path)]
     (when-let [content (db/get-file path)]
-      (let [result (common-handler/parse-config content)
+      (let [result (config-handler/parse-repo-config content)
             new-result (rewrite/update
                         result
                         :shortcuts
                         #(dissoc (rewrite/sexpr %) k))
             new-content (str new-result)]
-        (common-handler/reset-config! repo new-content)
+        (repo-config-handler/set-repo-config-state! repo new-content)
         (file/set-file-content! repo path new-content)))))
 
 (defn get-group

+ 32 - 20
src/main/frontend/modules/shortcut/dicts.cljc

@@ -219,10 +219,11 @@
              :command.pdf/close                       "关闭当前PDF文档"
              :command.ui/toggle-help                  "显示/关闭帮助"
              :command.git/commit                      "提交消息"
-             :command.go/search                       "全文搜索"
+             :command.go/search                       "搜索页面和块"
              :command.go/backward                     "回退"
              :command.go/forward                      "前进"
-             :command.go/search-in-page               "在当前页面搜索"
+             :command.go/search-in-page               "在当前页面搜索块"
+             :command.go/electron-find-in-page        "在当前页面查找文本"
              :command.ui/toggle-document-mode         "切换文档模式"
              :command.ui/toggle-contents              "打开/关闭目录"
              :command.ui/toggle-theme                 "在暗色/亮色主题之间切换"
@@ -870,15 +871,15 @@
              :command.go/backward                     "Voltar"
              :command.go/flashcards                   "Trocar flashcards"
              :command.go/forward                      "Avançar"
-             :command.go/graph-view                   "Ir para o gráfico"
+             :command.go/graph-view                   "Ir para o grafo"
              :command.go/home                         "Volar para o inicio"
              :command.go/keyboard-shortcuts           "Ir para os atalhos do teclado"
              :command.go/next-journal                 "Ir ao proximo jornal"
              :command.go/prev-journal                 "Ir ao jornal anterior"
              :command.go/tomorrow                     "Ir para amanhã"
-             :command.graph/add                       "Adicionar um gráfico"
-             :command.graph/open                      "Selecionar gráfico para abrir"
-             :command.graph/remove                    "Remover um gráfico"
+             :command.graph/add                       "Adicionar um grafo"
+             :command.graph/open                      "Selecionar grafo para abrir"
+             :command.graph/remove                    "Remover um grafo"
              :command.pdf/next-page                   "Próxima página do atual pdf doc"
              :command.pdf/previous-page               "Página anterior do atual pdf doc"
              :command.sidebar/clear                   "Limpar tudo da barra lateral direita"
@@ -886,9 +887,14 @@
              :command.ui/select-theme-color           "Selecionar as cores do tema disponível"
              :command.ui/toggle-cards                 "Trocar cartões"
              :command.ui/toggle-left-sidebar          "Trocar barra lateral esquerda"
-             :command.graph/save                      "Salvar gráfico atual no computador"
+             :command.graph/save                      "Salvar grafo atual no computador"
              :command.misc/copy                       "Copiar (copiar seleção ou referência do bloco)"
-             :command.ui/goto-plugins                 "Ir para o painel de plugins"}
+             :command.ui/goto-plugins                 "Ir para o painel de plugins"
+             :command.go/all-graphs                   "Ir à todos os grafos"
+             :command.go/electron-find-in-page        "Procurar texto na página"
+             :command.go/electron-jump-to-the-next    "Ir para a próxima correspondência da sua pesquisa"
+             :command.go/electron-jump-to-the-previous "Voltar para a correspondência anterior da sua pesquisa"
+             :command.graph/re-index                  "Reindexar o grafo atual"}
 
    :pt-BR   {:shortcut.category/formatting            "Formatação"
              :shortcut.category/basics                "Básico"
@@ -978,15 +984,15 @@
              :command.go/backward                     "Voltar"
              :command.go/flashcards                   "Trocar flashcards"
              :command.go/forward                      "Avançar"
-             :command.go/graph-view                   "Ir para o gráfico"
+             :command.go/graph-view                   "Ir para o grafo"
              :command.go/home                         "Volar para o inicio"
              :command.go/keyboard-shortcuts           "Ir para os atalhos do teclado"
              :command.go/next-journal                 "Ir ao proximo jornal"
              :command.go/prev-journal                 "Ir ao jornal anterior"
              :command.go/tomorrow                     "Ir para amanhã"
-             :command.graph/add                       "Adicionar um gráfico"
-             :command.graph/open                      "Selecionar gráfico para abrir"
-             :command.graph/remove                    "Remover um gráfico"
+             :command.graph/add                       "Adicionar um grafo"
+             :command.graph/open                      "Selecionar grafo para abrir"
+             :command.graph/remove                    "Remover um grafo"
              :command.pdf/next-page                   "Próxima página do atual pdf doc"
              :command.pdf/previous-page               "Página anterior do atual pdf doc"
              :command.sidebar/clear                   "Limpar tudo da barra lateral direita"
@@ -999,7 +1005,7 @@
              :command.editor/copy-current-file        "Copiar o arquivo atual"
              :command.editor/open-file-in-default-app "Abra o arquivo no aplicativo padrão"
              :command.editor/open-file-in-directory   "Abra o arquivo na pasta"
-             :command.graph/save                      "Salvar gráfico atual no computador"
+             :command.graph/save                      "Salvar grafo atual no computador"
              :command.misc/copy                       "Copiar (copiar seleção ou referência do bloco)"
              :command.ui/goto-plugins                 "Ir para o painel de plugins"
              ;;  :command.ui/open-new-window              "Abra uma nova janela"
@@ -1007,7 +1013,12 @@
              :command.editor/select-up                "Selecione o conteúdo acima"
              :command.editor/copy-embed               "Copiar uma incorporação do bloco, apontando para o bloco atual"
              :command.editor/copy-text                "Copiar seleção como texto"
-             :command.pdf/close                       "Fechar visualização do PDF"}
+             :command.pdf/close                       "Fechar visualização do PDF"
+             :command.go/all-graphs                   "Ir à todos os grafos"
+             :command.go/electron-find-in-page        "Localizar texto na página"
+             :command.go/electron-jump-to-the-next    "Ir para a próxima correspondência da sua pesquisa"
+             :command.go/electron-jump-to-the-previous "Voltar para a correspondência anterior da sua pesquisa"
+             :command.graph/re-index                  "Reindexar o grafo atual"}
 
    :ja      {:shortcut.category/formatting            "フォーマット"
              :shortcut.category/basics                "基本操作"
@@ -1334,15 +1345,16 @@
              :command.sidebar/clear                  "Sağ kenar çubuğundaki herşeyi temizle"
              :command.misc/copy                      "mod+c"
              :command.command-palette/toggle         "Komut paletini aç"
-             :command.graph/open                     "Açılacak grafiği seçin"
-             :command.graph/remove                   "Bir grafiği kaldır"
-             :command.graph/add                      "Grafik ekle"
-             :command.graph/save                     "Mevcut grafiği diske kaydet"
-             :command.graph/re-index                 "Mevcut grafiği yeniden oluştur"
+             :command.graph/open                     "Açılacak çizelgeyi seçin"
+             :command.graph/remove                   "Bir çizelgeyi kaldır"
+             :command.graph/add                      "Çizelge ekle"
+             :command.graph/save                     "Mevcut çizelgeyi diske kaydet"
+             :command.graph/re-index                 "Mevcut çizelgeyi yeniden oluştur"
              :command.command/run                    "Git komutunu çalıştır"
              :command.go/home                        "Ana sayfaya git"
+             :command.go/all-graphs                  "Bütün çizelgelere git"
              :command.go/all-pages                   "Bütün sayfalara git"
-             :command.go/graph-view                  "Grafik görünümüne git"
+             :command.go/graph-view                  "Çizelge görünümüne git"
              :command.go/keyboard-shortcuts          "Klavye kısayollarına git"
              :command.go/tomorrow                    "Yarının günlüğüne git"
              :command.go/next-journal                "Sonraki günlüğe git"

+ 3 - 2
src/main/frontend/search.cljs

@@ -96,7 +96,7 @@
       (when-not (string/blank? q)
         (protocol/query engine q option)))))
 
-(defn transact-blocks!
+(defn- transact-blocks!
   [repo data]
   (when-let [engine (get-engine repo)]
     (protocol/transact-blocks! engine data)))
@@ -237,7 +237,8 @@
                 blocks-to-add (->> (filter (fn [block]
                                              (contains? blocks-to-add-set (:db/id block)))
                                            blocks-result)
-                                   (map search-db/block->index))
+                                   (map search-db/block->index)
+                                   (remove nil?))
                 blocks-to-remove-set (->> (remove :added blocks)
                                           (map :e)
                                           (set))]

+ 1 - 3
src/main/frontend/spec/storage.cljc

@@ -3,7 +3,6 @@
   #?(:cljs (:require [cljs.spec.alpha :as s])
      :default (:require [clojure.spec.alpha :as s])))
 
-(s/def ::db-schema map?)
 (s/def ::ls-right-sidebar-state map?)
 (s/def ::ls-right-sidebar-width string?)
 (s/def ::ls-left-sidebar-open? boolean?)
@@ -36,8 +35,7 @@
 (s/def ::local-storage
   ;; All these keys are optional since we usually only validate one key at a time
   (s/keys
-   :opt-un [::db-schema
-            ::ls-right-sidebar-state
+   :opt-un [::ls-right-sidebar-state
             ::ls-right-sidebar-width
             ::ls-left-sidebar-open?
             :ui/theme

+ 284 - 261
src/main/frontend/state.cljs

@@ -16,6 +16,7 @@
             [logseq.graph-parser.config :as gp-config]
             [frontend.mobile.util :as mobile-util]))
 
+;; Stores main application state
 (defonce ^:large-vars/data-var state
   (let [document-mode? (or (storage/get :document/mode?) false)
         current-graph  (let [graph (storage/get :git/current-repo)]
@@ -251,6 +252,9 @@
      :graph/importing-state                 {}
      })))
 
+;; Block ast state
+;; ===============
+
 ;; block uuid -> {content(String) -> ast}
 (def blocks-ast-cache (atom {}))
 (defn add-block-ast-cache!
@@ -267,68 +271,53 @@
   (when (and block-uuid content)
     (get-in @blocks-ast-cache [block-uuid content])))
 
-(defn sub
-  [ks]
-  (if (coll? ks)
-    (util/react (rum/cursor-in state ks))
-    (util/react (rum/cursor state ks))))
-
-(defn get-route-match
-  []
-  (:route-match @state))
-
-(defn get-current-route
-  []
-  (get-in (get-route-match) [:data :name]))
-
-(defn home?
-  []
-  (= :home (get-current-route)))
-
-(defn setups-picker?
-  []
-  (= :repo-add (get-current-route)))
-
-(defn get-current-page
-  []
-  (when (= :page (get-current-route))
-    (get-in (get-route-match)
-            [:path-params :name])))
-
-(defn route-has-p?
-  []
-  (get-in (get-route-match) [:query-params :p]))
-
-(defn set-state!
-  [path value]
-  (if (vector? path)
-    (swap! state assoc-in path value)
-    (swap! state assoc path value)))
-
-(defn update-state!
-  [path f]
-  (if (vector? path)
-    (swap! state update-in path f)
-    (swap! state update path f)))
-
-(defn get-current-repo
-  []
-  (or (:git/current-repo @state)
-      (when-not (mobile-util/native-platform?)
-        "local")))
+;; User configuration getters under :config (and sometimes :me)
+;; ========================================
+;; TODO: Refactor default config values to be data driven. Currently they are all
+;;  buried in getters
+;; TODO: Refactor our access to be more data driven. Currently each getter
+;;  (re-)fetches get-current-repo needlessly
+;; TODO: Add consistent validation. Only a few config options validate at get time
 
 (def default-config
   "Default config for a repo-specific, user config"
   {:feature/enable-search-remove-accents? true
    :default-arweave-gateway "https://arweave.net"})
 
+;; State that most user config is dependent on
+(declare get-current-repo)
+
+(defn merge-configs
+  "Merges user configs in given orders. All values are overriden except for maps
+  which are merged."
+  [& configs]
+  (apply merge-with
+    (fn merge-config [current new]
+      (if (and (map? current) (map? new))
+        (merge current new)
+        new))
+    configs))
+
 (defn get-config
-  "User config for the given repo or current repo if none given"
+  "User config for the given repo or current repo if none given. All config fetching
+should be done through this fn in order to get global config and config defaults"
   ([]
    (get-config (get-current-repo)))
   ([repo-url]
-   (merge default-config
-          (get-in @state [:config repo-url]))))
+   (merge-configs
+    default-config
+    (get-in @state [:config ::global-config])
+    (get-in @state [:config repo-url]))))
+
+(defonce publishing? (atom nil))
+
+(defn publishing-enable-editing?
+  []
+  (and @publishing? (:publishing/enable-editing? (get-config))))
+
+(defn enable-editing?
+  []
+  (or (not @publishing?) (:publishing/enable-editing? (get-config))))
 
 (defn get-arweave-gateway
   []
@@ -343,10 +332,6 @@
     built-in-macros
     (:macros (get-config))))
 
-(defn sub-config
-  []
-  (sub :config))
-
 (defn get-custom-css-link
   []
   (:custom-css-url (get-config)))
@@ -367,93 +352,10 @@
         value (if (some? value) value (:all-pages-public? (get-config)))]
     (true? value)))
 
-(defn enable-grammarly?
-  []
-  (true? (:feature/enable-grammarly?
-           (get (sub-config) (get-current-repo)))))
-
-;; (defn store-block-id-in-file?
-;;   []
-;;   (true? (:block/store-id-in-file? (get-config))))
-
-(defn scheduled-deadlines-disabled?
-  []
-  (true? (:feature/disable-scheduled-and-deadline-query?
-           (get (sub-config) (get-current-repo)))))
-
-(defn enable-timetracking?
-  []
-  (not (false? (:feature/enable-timetracking?
-                 (get (sub-config) (get-current-repo))))))
-
-(defn enable-journals?
-  ([]
-   (enable-journals? (get-current-repo)))
-  ([repo]
-   (not (false? (:feature/enable-journals?
-                 (get (sub-config) repo))))))
-
-(defn enable-flashcards?
-  ([]
-   (enable-flashcards? (get-current-repo)))
-  ([repo]
-   (not (false? (:feature/enable-flashcards?
-                 (get (sub-config) repo))))))
-
-(defn user-groups
-  []
-  (set (sub [:user/info :UserGroups])))
-
-(defn enable-sync?
-  []
-  (sub :feature/enable-sync?))
-
-(defn export-heading-to-list?
-  []
-  (not (false? (:export/heading-to-list?
-                 (get (sub-config) (get-current-repo))))))
-
-(defn enable-git-auto-push?
-  [repo]
-  (not (false? (:git-auto-push
-                 (get (sub-config) repo)))))
-
-(defn enable-block-timestamps?
-  []
-  (true? (:feature/enable-block-timestamps?
-           (get (sub-config) (get-current-repo)))))
-
-(defn enable-whiteboards?
-  ([]
-   (enable-whiteboards? (get-current-repo)))
-  ([repo]
-   (and
-    (util/electron?)
-    (true? (:feature/enable-whiteboards?
-            (get (sub-config) repo))))))
-
-(defn sub-graph-config
-  []
-  (get (sub-config) (get-current-repo)))
-
-(defn sub-graph-config-settings
-  []
-  (:graph/settings (sub-graph-config)))
-
-;; Enable by default
-(defn show-brackets?
-  []
-  (not (false? (:ui/show-brackets?
-                 (get (sub-config) (get-current-repo))))))
-
 (defn get-default-home
   []
   (:default-home (get-config)))
 
-(defn sub-default-home-page
-  []
-  (get-in (sub-config) [(get-current-repo) :default-home :page] ""))
-
 (defn custom-home-page?
   []
   (some? (:page (get-default-home))))
@@ -536,6 +438,241 @@
   []
   (:page-name-order (get-config)))
 
+(defn get-date-formatter
+  []
+  (gp-config/get-date-formatter (get-config)))
+
+(defn shortcuts []
+  (:shortcuts (get-config)))
+
+(defn get-commands
+  []
+  (:commands (get-config)))
+
+(defn get-scheduled-future-days
+  []
+  (let [days (:scheduled/future-days (get-config))]
+    (or (when (int? days) days) 0)))
+
+(defn get-start-of-week
+  []
+  (or (:start-of-week (get-config))
+      (get-in @state [:me :settings :start-of-week])
+      6))
+
+(defn get-ref-open-blocks-level
+  []
+  (or
+    (when-let [value (:ref/default-open-blocks-level (get-config))]
+      (when (integer? value)
+        value))
+    2))
+
+(defn get-linked-references-collapsed-threshold
+  []
+  (or
+    (when-let [value (:ref/linked-references-collapsed-threshold (get-config))]
+      (when (integer? value)
+        value))
+    100))
+
+(defn get-export-bullet-indentation
+  []
+  (case (get (get-config) :export/bullet-indentation :tab)
+    :eight-spaces
+    "        "
+    :four-spaces
+    "    "
+    :two-spaces
+    "  "
+    :tab
+    "\t"))
+
+(defn enable-search-remove-accents?
+  []
+  (:feature/enable-search-remove-accents? (get-config)))
+
+;; State cursor fns for use with rum components
+;; ============================================
+
+(declare document-mode?)
+
+(defn sub
+  "Creates a rum cursor, https://github.com/tonsky/rum#cursors, for use in rum components.
+Similar to re-frame subscriptions"
+  [ks]
+  (if (coll? ks)
+    (util/react (rum/cursor-in state ks))
+    (util/react (rum/cursor state ks))))
+
+(defn sub-config
+  "Sub equivalent to get-config which should handle all sub user-config access"
+  ([] (sub-config (get-current-repo)))
+  ([repo]
+   (let [config (sub :config)]
+     (merge-configs default-config
+                    (get config ::global-config)
+                    (get config repo)))))
+
+(defn enable-grammarly?
+  []
+  (true? (:feature/enable-grammarly? (sub-config))))
+
+(defn scheduled-deadlines-disabled?
+  []
+  (true? (:feature/disable-scheduled-and-deadline-query? (sub-config))))
+
+(defn enable-timetracking?
+  []
+  (not (false? (:feature/enable-timetracking? (sub-config)))))
+
+(defn enable-journals?
+  ([]
+   (enable-journals? (get-current-repo)))
+  ([repo]
+   (not (false? (:feature/enable-journals? (sub-config repo))))))
+
+(defn enable-flashcards?
+  ([]
+   (enable-flashcards? (get-current-repo)))
+  ([repo]
+   (not (false? (:feature/enable-flashcards? (sub-config repo))))))
+
+(defn enable-sync?
+  []
+  (sub :feature/enable-sync?))
+
+(defn export-heading-to-list?
+  []
+  (not (false? (:export/heading-to-list? (sub-config)))))
+
+(defn enable-git-auto-push?
+  [repo]
+  (not (false? (:git-auto-push (sub-config repo)))))
+
+(defn enable-block-timestamps?
+  []
+  (true? (:feature/enable-block-timestamps? (sub-config))))
+
+(defn graph-settings
+  []
+  (:graph/settings (sub-config)))
+
+;; Enable by default
+(defn show-brackets?
+  []
+  (not (false? (:ui/show-brackets? (sub-config)))))
+
+(defn sub-default-home-page
+  []
+  (get-in (sub-config) [:default-home :page] ""))
+
+(defn sub-edit-content
+  [id]
+  (sub [:editor/content id]))
+
+(defn- get-selected-block-ids
+  [blocks]
+  (->> blocks
+       (keep #(when-let [id (dom/attr % "blockid")]
+                (uuid id)))
+       (distinct)))
+
+(defn sub-block-selected?
+  [block-uuid]
+  (rum/react
+   (rum/derived-atom [state] [::select-block block-uuid]
+     (fn [state]
+       (contains? (set (get-selected-block-ids (:selection/blocks state)))
+                  block-uuid)))))
+
+(defn block-content-max-length
+  [repo]
+  (or (:block/content-max-length (sub-config repo)) 5000))
+
+(defn mobile?
+  []
+  (or (util/mobile?) (mobile-util/native-platform?)))
+
+(defn enable-tooltip?
+  []
+  (if (mobile?)
+    false
+    (get (sub-config) :ui/enable-tooltip? true)))
+
+(defn show-command-doc?
+  []
+  (get (sub-config) :ui/show-command-doc? true))
+
+(defn logical-outdenting?
+  []
+  (:editor/logical-outdenting? (sub-config)))
+
+(defn enable-encryption?
+  [repo]
+  (:feature/enable-encryption? (sub-config repo)))
+
+(defn doc-mode-enter-for-new-line?
+  []
+  (and (document-mode?)
+       (not (:shortcut/doc-mode-enter-for-new-block? (get-config)))))
+
+(defn user-groups
+  []
+  (set (sub [:user/info :UserGroups])))
+
+;; State mutation helpers
+;; ======================
+
+(defn set-state!
+  [path value]
+  (if (vector? path)
+    (swap! state assoc-in path value)
+    (swap! state assoc path value)))
+
+(defn update-state!
+  [path f]
+  (if (vector? path)
+    (swap! state update-in path f)
+    (swap! state update path f)))
+
+;; State getters and setters
+;; =========================
+;; These fns handle any key except :config.
+;; Some state is also stored in local storage and/or sent to electron's main process
+
+(defn get-route-match
+  []
+  (:route-match @state))
+
+(defn get-current-route
+  []
+  (get-in (get-route-match) [:data :name]))
+
+(defn home?
+  []
+  (= :home (get-current-route)))
+
+(defn setups-picker?
+  []
+  (= :repo-add (get-current-route)))
+
+(defn get-current-page
+  []
+  (when (= :page (get-current-route))
+    (get-in (get-route-match)
+            [:path-params :name])))
+
+(defn route-has-p?
+  []
+  (get-in (get-route-match) [:query-params :p]))
+
+(defn get-current-repo
+  []
+  (or (:git/current-repo @state)
+      (when-not (mobile-util/native-platform?)
+        "local")))
+
 (defn get-remote-repos
   []
   (get-in @state [:file-sync/remote-graphs :graphs]))
@@ -627,10 +764,6 @@
   []
   (get (:editor/content @state) (get-edit-input-id)))
 
-(defn sub-edit-content
-  []
-  (sub [:editor/content (get-edit-input-id)]))
-
 (defn get-cursor-range
   []
   (:cursor-range @state))
@@ -684,13 +817,16 @@
     (do
       (set-editor-action! nil)
       (set-editor-action-data! nil))))
+
 (defn get-editor-show-input
   []
   (when (= (get-editor-action) :input)
     (get @state :editor/action-data)))
+
 (defn set-editor-show-commands!
   []
   (when-not (get-editor-action) (set-editor-action! :commands)))
+
 (defn set-editor-show-block-commands!
   []
   (when-not (get-editor-action) (set-editor-action! :block-commands)))
@@ -745,25 +881,10 @@
   []
   (:selection/blocks @state))
 
-(defn- get-selected-block-ids
-  [blocks]
-  (->> blocks
-       (keep #(when-let [id (dom/attr % "blockid")]
-                (uuid id)))
-       (distinct)))
-
 (defn get-selection-block-ids
   []
   (get-selected-block-ids (get-selection-blocks)))
 
-(defn sub-block-selected?
-  [block-uuid]
-  (rum/react
-   (rum/derived-atom [state] [::select-block block-uuid]
-     (fn [state]
-       (contains? (set (get-selected-block-ids (:selection/blocks state)))
-                  block-uuid)))))
-
 (defn get-selection-start-block-or-first
   []
   (or (get-selection-start-block)
@@ -894,20 +1015,6 @@
        :container       (gobj/get container "id")
        :pos             (cursor/pos (gdom/getElement edit-input-id))})))
 
-(defonce publishing? (atom nil))
-
-(defn publishing-enable-editing?
-  []
-  (and @publishing? (:publishing/enable-editing? (get-config))))
-
-(defn enable-editing?
-  []
-  (or (not @publishing?) (:publishing/enable-editing? (get-config))))
-
-(defn block-content-max-length
-  [repo]
-  (or (:block/content-max-length (get (sub-config) repo)) 5000))
-
 (defn clear-edit!
   []
   (swap! state merge {:editor/editing? nil
@@ -1060,13 +1167,6 @@
   [value]
   (set-state! :today value))
 
-(defn get-date-formatter
-  []
-  (gp-config/get-date-formatter (get-config)))
-
-(defn shortcuts []
-  (get-in @state [:config (get-current-repo) :shortcuts]))
-
 (defn get-me
   []
   (:me @state))
@@ -1200,11 +1300,6 @@
   []
   (get @state :document/mode?))
 
-(defn doc-mode-enter-for-new-line?
-  []
-  (and (document-mode?)
-       (not (:shortcut/doc-mode-enter-for-new-block? (sub-graph-config)))))
-
 (defn toggle-document-mode!
   []
   (let [mode (document-mode?)]
@@ -1221,28 +1316,15 @@
     (set-state! :ui/shortcut-tooltip? (not mode))
     (storage/set :ui/shortcut-tooltip? (not mode))))
 
-(defn mobile?
-  []
-  (or (util/mobile?) (mobile-util/native-platform?)))
-
-(defn enable-tooltip?
-  []
-  (if (mobile?)
-    false
-    (get (get (sub-config) (get-current-repo))
-         :ui/enable-tooltip?
-         true)))
-
-(defn show-command-doc?
-  []
-  (get (get (sub-config) (get-current-repo))
-       :ui/show-command-doc?
-       true))
-
 (defn set-config!
   [repo-url value]
   (set-state! [:config repo-url] value))
 
+(defn set-global-config!
+  [value]
+  ;; Placed under :config so cursors can work seamlessly
+  (set-config! ::global-config value))
+
 (defn get-wide-mode?
   []
   (:ui/wide-mode? @state))
@@ -1255,10 +1337,6 @@
   [value]
   (set-state! :network/online? value))
 
-(defn get-commands
-  []
-  (:commands (get-config)))
-
 (defn get-plugins-commands
   []
   (mapcat seq (flatten (vals (:plugin/installed-slash-commands @state)))))
@@ -1310,11 +1388,6 @@
     true))
 
 
-(defn get-scheduled-future-days
-  []
-  (let [days (:scheduled/future-days (get-config))]
-    (or (when (int? days) days) 0)))
-
 (defn set-graph-syncing?
   [value]
   (set-state! :graph/syncing? value))
@@ -1437,30 +1510,6 @@
   []
   @editor-op)
 
-(defn get-start-of-week
-  []
-  (or
-    (when-let [repo (get-current-repo)]
-      (get-in @state [:config repo :start-of-week]))
-    (get-in @state [:me :settings :start-of-week])
-    6))
-
-(defn get-ref-open-blocks-level
-  []
-  (or
-    (when-let [value (:ref/default-open-blocks-level (get-config))]
-      (when (integer? value)
-        value))
-    2))
-
-(defn get-linked-references-collapsed-threshold
-  []
-  (or
-    (when-let [value (:ref/linked-references-collapsed-threshold (get-config))]
-      (when (integer? value)
-        value))
-    100))
-
 (defn get-events-chan
   []
   (:system/events @state))
@@ -1511,27 +1560,10 @@
   [value]
   (set-state! :block/component-editing-mode? value))
 
-(defn logical-outdenting?
-  []
-  (:editor/logical-outdenting?
-    (get (sub-config) (get-current-repo))))
-
 (defn get-editor-args
   []
   (:editor/args @state))
 
-(defn get-export-bullet-indentation
-  []
-  (case (get (get-config) :export/bullet-indentation :tab)
-    :eight-spaces
-    "        "
-    :four-spaces
-    "    "
-    :two-spaces
-    "  "
-    :tab
-    "\t"))
-
 (defn set-page-blocks-cp!
   [value]
   (set-state! [:view/components :page-blocks] value))
@@ -1757,11 +1789,6 @@
     (when (every? not-empty (vals agent-opts))
       (str (:protocol agent-opts) "://" (:host agent-opts) ":" (:port agent-opts)))))
 
-(defn enable-encryption?
-  [repo]
-  (:feature/enable-encryption?
-   (get (sub-config) repo)))
-
 (defn set-mobile-app-state-change
   [is-active?]
   (set-state! :mobile/app-state-change
@@ -1782,10 +1809,6 @@
   [dir]
   (contains? (:file/unlinked-dirs @state) dir))
 
-(defn enable-search-remove-accents?
-  []
-  (:feature/enable-search-remove-accents? (get-config)))
-
 (defn get-file-rename-event-chan
   []
   (:file/rename-event-chan @state))

+ 32 - 0
src/main/frontend/state_test.cljs

@@ -0,0 +1,32 @@
+(ns frontend.state-test
+  (:require [clojure.test :refer [deftest is]]
+            [frontend.state :as state]))
+
+(deftest merge-configs
+  (let [global-config
+        {:shortcuts {:ui/toggle-theme "t z"}
+         :hidden []
+         :ui/enable-tooltip? true
+         :preferred-workflow :todo
+         :git-pull-secs 60}
+        local-config {:hidden ["foo" "bar"]
+                      :ui/enable-tooltip? false
+                      :preferred-workflow :now
+                      :git-pull-secs 120}]
+    (is (= local-config
+           (dissoc (state/merge-configs global-config local-config) :shortcuts))
+        "Later config overrides all non-map values")
+    (is (= {:start-of-week 6 :shortcuts {:ui/toggle-theme "t z"}}
+           (select-keys (state/merge-configs {:start-of-week 6}
+                                             global-config
+                                             local-config)
+                        [:start-of-week :shortcuts]))
+        "Earlier configs set default values"))
+
+  (is (= {:shortcuts {:ui/toggle-theme "t z"
+                      :ui/toggle-brackets "t b"
+                      :editor/up ["ctrl+p" "up"]}}
+         (state/merge-configs {:shortcuts {:ui/toggle-theme "t z"}}
+                              {:shortcuts {:ui/toggle-brackets "t b"}}
+                              {:shortcuts {:editor/up ["ctrl+p" "up"]}}))
+      "Map values get merged across configs"))

+ 16 - 9
src/main/frontend/ui.cljs

@@ -7,6 +7,7 @@
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.rum :as r]
             [frontend.state :as state]
+            [frontend.storage :as storage]
             [frontend.ui.date-picker]
             [frontend.util :as util]
             [frontend.util.cursor :as cursor]
@@ -85,14 +86,14 @@
     (textarea props)))
 
 (rum/defc dropdown-content-wrapper
-  < {:did-mount    (fn [_state]
+  < {:did-mount    (fn [state]
                      (let [k    (inc (count (state/sub :modal/dropdowns)))
-                           args (:rum/args _state)]
+                           args (:rum/args state)]
                        (state/set-state! [:modal/dropdowns k] (second args))
-                       (assoc _state ::k k)))
-     :will-unmount (fn [_state]
-                     (state/update-state! :modal/dropdowns #(dissoc % (::k _state)))
-                     _state)}
+                       (assoc state ::k k)))
+     :will-unmount (fn [state]
+                     (state/update-state! :modal/dropdowns #(dissoc % (::k state)))
+                     state)}
   [dropdown-state _close-fn content class]
   (let [class (or class
                   (util/hiccup->class "origin-top-right.absolute.right-0.mt-2"))]
@@ -287,8 +288,8 @@
   (let [time-fn (fn []
                   (try
                     (util/time-ago input)
-                    (catch js/Error _e
-                      (js/console.error _e)
+                    (catch js/Error e
+                      (js/console.error e)
                       input)))
         [time set-time] (rum/use-state (time-fn))]
 
@@ -337,7 +338,12 @@
     (when (mobile-util/native-iphone-without-notch?) (.add cl "is-native-iphone-without-notch"))
     (when (mobile-util/native-ipad?) (.add cl "is-native-ipad"))
     (when (util/electron?)
-      (js/window.apis.on "full-screen" #(js-invoke cl (if (= % "enter") "add" "remove") "is-fullscreen"))
+      (doseq [[event function]
+              [["persist-zoom-level" #(storage/set :zoom-level %)]
+               ["restore-zoom-level" #(when-let [zoom-level (storage/get :zoom-level)] (js/window.apis.setZoomLevel zoom-level))]
+               ["full-screen" #(js-invoke cl (if (= % "enter") "add" "remove") "is-fullscreen")]]]
+        (.on js/window.apis event function))
+
       (p/then (ipc/ipc :getAppBaseInfo) #(let [{:keys [isFullScreen]} (js->clj % :keywordize-keys true)]
                                            (and isFullScreen (.add cl "is-fullscreen")))))))
 
@@ -925,6 +931,7 @@
               (dissoc opts :class :extension?))]))
 
 (rum/defc with-shortcut < rum/reactive
+  < {:key-fn (fn [key pos] (str "shortcut-" key pos))}
   [shortcut-key position content]
   (let [tooltip? (state/sub :ui/shortcut-tooltip?)]
     (if tooltip?

+ 5 - 1
src/main/frontend/ui.css

@@ -117,8 +117,12 @@
         padding: 2rem;
         width: auto;
 
-        .ls-card, .ls-search {
+        .ls-card,
+        .ls-search {
           width: 740px;
+        }
+
+        .ls-card {
           min-height: 60vh;
         }
 

+ 2 - 2
src/main/frontend/util/cursor.cljs

@@ -39,8 +39,8 @@
                  (util/nth-safe pos)
                  mock-char-pos
                  (assoc :rect rect))
-         (catch :default _e
-           (js/console.log "index error" _e)
+         (catch :default e
+           (js/console.log "index error" e)
            {:pos pos
             :rect rect
             :left js/Number.MAX_SAFE_INTEGER

+ 25 - 25
src/main/frontend/util/fs.cljs

@@ -17,44 +17,44 @@
   "Ignore path for ls-dir-files-with-handler! and reload-dir!"
   [dir path]
   (let [ignores ["." ".recycle" "node_modules" "logseq/bak"
-                    "logseq/version-files" "logseq/graphs-txid.edn"]]
+                 "logseq/version-files" "logseq/graphs-txid.edn"]]
     (when (string? path)
-     (or
-      (some #(string/starts-with? path (str dir "/" %)) ignores)
-      (some #(string/includes? path (str "/" % "/")) ignores)
-      (some #(string/ends-with? path %)
-            [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"])
+      (or
+       (some #(string/starts-with? path (str dir "/" %)) ignores)
+       (some #(string/includes? path (str "/" % "/")) ignores)
+       (some #(string/ends-with? path %)
+             [".DS_Store" "logseq/graphs-txid.edn" "logseq/broken-config.edn"])
       ;; hidden directory or file
-      (let [relpath (path/relative dir path)]
-        (or (re-find #"/\.[^.]+" relpath)
-            (re-find #"^\.[^.]+" relpath)))
-      (let [path (string/lower-case path)]
-        (and
-         (not (string/blank? (path/extname path)))
-         (not
-          (some #(string/ends-with? path %)
-                [".md" ".markdown" ".org" ".js" ".edn" ".css"]))))))))
+       (let [relpath (path/relative dir path)]
+         (or (re-find #"/\.[^.]+" relpath)
+             (re-find #"^\.[^.]+" relpath)))
+       (let [path (string/lower-case path)]
+         (and
+          (not (string/blank? (path/extname path)))
+          (not
+           (some #(string/ends-with? path %)
+                 [".md" ".markdown" ".org" ".js" ".edn" ".css"]))))))))
 
-(defn read-graph-txid-info
+(defn read-graphs-txid-info
   [root]
   (when (string? root)
     (-> (p/let [txid-str (fs/read-file root "logseq/graphs-txid.edn")
                 txid-meta (and txid-str (reader/read-string txid-str))]
-               txid-meta)
+          txid-meta)
         (p/catch
-          (fn [^js e]
-            (js/console.error "[fs read txid data error]" e))))))
+         (fn [^js e]
+           (js/console.error "[fs read txid data error]" e))))))
 
 (defn inflate-graphs-info
   [graphs]
   (if (seq graphs)
     (p/all (for [{:keys [root] :as graph} graphs]
-             (p/let [sync-meta (read-graph-txid-info root)]
-                    (if sync-meta
-                      (assoc graph
-                             :sync-meta sync-meta
-                             :GraphUUID (second sync-meta))
-                      graph))))
+             (p/let [sync-meta (read-graphs-txid-info root)]
+               (if sync-meta
+                 (assoc graph
+                        :sync-meta sync-meta
+                        :GraphUUID (second sync-meta))
+                 graph))))
     []))
 
 (defn read-repo-file

+ 23 - 18
src/main/frontend/util/persist_var.cljs

@@ -27,36 +27,41 @@
 
   ILoad
   (-load [_]
-    (when-not (config/demo-graph?)
+    (if (config/demo-graph?)
+      (p/resolved nil)
       (let [repo (state/get-current-repo)
             dir (config/get-repo-dir repo)
             path (load-path location)]
-        (p/let [stat (p/catch (fs/stat dir path)
-                              (constantly nil))
-                content (when stat
-                          (p/catch
-                           (fs/read-file dir path)
-                           (constantly nil)))]
-          (when-let [content (and (some? content)
-                                  (try (cljs.reader/read-string content)
-                                       (catch js/Error e
-                                         (println (util/format "load persist-var failed: %s"  (load-path location)))
-                                         (js/console.dir e))))]
-            (swap! *value (fn [o]
-                            (-> o
-                                (assoc-in [repo :loaded?] true)
-                                (assoc-in [repo :value] content)))))))))
+        (-> (p/chain (fs/stat dir path)
+                     (fn [stat]
+                       (when stat
+                         (fs/read-file dir path)))
+                     (fn [content]
+                       (when (not-empty content)
+                         (try (cljs.reader/read-string content)
+                              (catch js/Error e
+                                (println (util/format "read persist-var failed: %s" (load-path location)))
+                                (js/console.dir e)))))
+                     (fn [value]
+                       (when (some? value)
+                         (swap! *value (fn [o]
+                                         (-> o
+                                             (assoc-in [repo :loaded?] true)
+                                             (assoc-in [repo :value] value)))))))
+            (p/catch (fn [e]
+                       (println (util/format "load persist-var failed: %s: %s" (load-path location) e))))))))
   (-loaded? [_]
     (get-in @*value [(state/get-current-repo) :loaded?]))
 
   ISave
   (-save [_]
-    (when-not (config/demo-graph?)
+    (if (config/demo-graph?)
+      (p/resolved nil)
       (let [path (load-path location)
             repo (state/get-current-repo)
             content (str (get-in @*value [repo :value]))
             dir (config/get-repo-dir repo)]
-        (fs/write-file! repo dir path content nil))))
+        (fs/write-file! repo dir path content {:skip-compare? true}))))
 
   IDeref
   (-deref [_this]

+ 3 - 3
src/main/logseq/api.cljs

@@ -113,7 +113,7 @@
 
 (def ^:export get_current_graph_configs
   (fn []
-    (some-> (get (:config @state/state) (state/get-current-repo))
+    (some-> (state/get-config)
             (normalize-keyword-for-json)
             (bean/->js))))
 
@@ -852,9 +852,9 @@
 
 (defn ^:export exper_request
   [pid ^js options]
-  (when-let [^js _pl (plugin-handler/get-plugin-inst pid)]
+  (when-let [^js pl (plugin-handler/get-plugin-inst pid)]
     (let [req-id (vreset! *request-k (inc @*request-k))
-          req-cb #(plugin-handler/request-callback _pl req-id %)]
+          req-cb #(plugin-handler/request-callback pl req-id %)]
       (-> (ipc/ipc :httpRequest req-id options)
           (p/then #(req-cb %))
           (p/catch #(req-cb %)))

+ 12 - 0
templates/global-config.edn

@@ -0,0 +1,12 @@
+;; This global config file is used by all graphs.
+;; Your graph's logseq/config.edn overrides config keys in this file
+;; except for maps which are merged.
+;; As an example of merging, the following global and local configs:
+;;   {:shortcuts {:ui/toggle-theme "t z"}}
+;;   {:shortcuts {:ui/toggle-brackets "t b"}}
+;;
+;;  would result in the final config:
+;;   {:shortcuts {:ui/toggle-theme "t z"
+;;                :ui/toggle-brackets "t b"}}
+
+{}

+ 21 - 8
yarn.lock

@@ -14,6 +14,14 @@
   dependencies:
     "@jridgewell/trace-mapping" "^0.3.0"
 
+"@axe-core/playwright@^4.4.4":
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/@axe-core/playwright/-/playwright-4.4.4.tgz#3786c5f6bba38d1991b608584b00ae2744544573"
+  integrity sha512-VA7MR1WCqW5tFcUGCXDaaqV9pJUCdOGIR4DiZJrOxGjeRYxz3VwyMc1MDg/yiJ5fQA/QYMx+w0mvqYEr3CPx7w==
+  dependencies:
+    axe-core "^4.4.2"
+    playwright ">= 1.0.0"
+
 "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789"
@@ -1226,18 +1234,11 @@ autoprefixer@^9.8.6:
     postcss "^7.0.32"
     postcss-value-parser "^4.1.0"
 
-axe-core@^4.0.1:
+axe-core@^4.4.2:
   version "4.4.3"
   resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
   integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
 
-axe-playwright@^1.1.11:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/axe-playwright/-/axe-playwright-1.1.11.tgz#e57638f08d29b58d157a2aeb34cf81730eab2cff"
-  integrity sha512-YHmUouvF/dFNxoFFwbCjPFmEPwoJSzPgZsD0KZs3xjsR03Rf2mAh771ugre950MaBYuiyxYDlurH5BOEJBK34Q==
-  dependencies:
-    axe-core "^4.0.1"
-
 bach@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880"
@@ -5565,6 +5566,18 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.24.2.tgz#47bc5adf3dcfcc297a5a7a332449c9009987db26"
   integrity sha512-zfAoDoPY/0sDLsgSgLZwWmSCevIg1ym7CppBwllguVBNiHeixZkc1AdMuYUPZC6AdEYc4CxWEyLMBTw2YcmRrA==
 
[email protected]:
+  version "1.25.2"
+  resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.25.2.tgz#ea4baa398a4d45fcdfe48799482b599e3d0f033f"
+  integrity sha512-0yTbUE9lIddkEpLHL3u8PoCL+pWiZtj5A/j3U7YoNjcmKKDGBnCrgHJMzwd2J5vy6l28q4ki3JIuz7McLHhl1A==
+
+"playwright@>= 1.0.0":
+  version "1.25.2"
+  resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.25.2.tgz#0fc67e4385a52a51371ff9114bf68e3ad50a7f41"
+  integrity sha512-RwMB5SFRV/8wSfK+tK8ycpqdzORvoqUNz9DUeRfSgZFrZej5uuBl9wFjWcc+OkXFEtaPmx1acAVGG7hA4IJ1kg==
+  dependencies:
+    playwright-core "1.25.2"
+
 playwright@^1.24.2:
   version "1.24.2"
   resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.24.2.tgz#51e60f128b386023e5ee83deca23453aaf73ba6d"