Przeglądaj źródła

Merge branch 'master' into dev/malli-schema&kondo-config

Junyi Du 2 lat temu
rodzic
commit
414fef6933
53 zmienionych plików z 1246 dodań i 685 usunięć
  1. 3 1
      .clj-kondo/config.edn
  2. 0 13
      .github/workflows/stale-issues.yml
  3. 2 0
      .gitignore
  4. 132 8
      CODE_OF_CONDUCT.md
  5. 6 1
      README.md
  6. 2 1
      deps.edn
  7. 4 0
      deps/graph-parser/.carve/ignore
  8. 1 1
      deps/graph-parser/src/logseq/graph_parser.cljs
  9. 1 1
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  10. 1 1
      deps/graph-parser/src/logseq/graph_parser/extract.cljc
  11. 8 2
      deps/graph-parser/src/logseq/graph_parser/text.cljs
  12. 12 1
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  13. 26 8
      docs/dev-practices.md
  14. 2 2
      e2e-tests/plugins.spec.ts
  15. 1 1
      e2e-tests/whiteboards.spec.ts
  16. 1 1
      libs/src/LSPlugin.ts
  17. 1 1
      package.json
  18. 12 12
      resources/package.json
  19. 12 9
      src/electron/electron/handler.cljs
  20. 4 2
      src/electron/electron/search.cljs
  21. 1 0
      src/electron/electron/window.cljs
  22. 13 4
      src/main/frontend/components/block.cljs
  23. 1 1
      src/main/frontend/components/query_table.cljs
  24. 2 0
      src/main/frontend/components/search.cljs
  25. 8 0
      src/main/frontend/components/settings.cljs
  26. 5 1
      src/main/frontend/components/sidebar.css
  27. 9 3
      src/main/frontend/db/query_custom.cljs
  28. 1 2
      src/main/frontend/db/utils.cljs
  29. 185 4
      src/main/frontend/dicts.cljc
  30. 20 19
      src/main/frontend/extensions/pdf/assets.cljs
  31. 62 44
      src/main/frontend/extensions/pdf/highlights.cljs
  32. 151 109
      src/main/frontend/fs/sync.cljs
  33. 1 0
      src/main/frontend/handler.cljs
  34. 4 0
      src/main/frontend/handler/config.cljs
  35. 5 1
      src/main/frontend/handler/editor/keyboards.cljs
  36. 16 17
      src/main/frontend/handler/file_sync.cljs
  37. 33 1
      src/main/frontend/handler/page.cljs
  38. 1 2
      src/main/frontend/handler/repo.cljs
  39. 1 0
      src/main/frontend/handler/search.cljs
  40. 11 7
      src/main/frontend/modules/file/core.cljs
  41. 34 29
      src/main/frontend/modules/outliner/file.cljs
  42. 1 1
      src/main/frontend/modules/outliner/pipeline.cljs
  43. 21 3
      src/main/frontend/modules/shortcut/dicts.cljc
  44. 3 0
      src/main/frontend/publishing.cljs
  45. 75 0
      src/main/frontend/pubsub.cljc
  46. 1 0
      src/main/frontend/schema/handler/common_config.cljc
  47. 3 0
      src/main/frontend/search.cljs
  48. 4 0
      src/main/frontend/state.cljs
  49. 20 1
      src/main/frontend/util.cljc
  50. 12 0
      src/test/frontend/db/query_custom_test.cljs
  51. 283 346
      static/yarn.lock
  52. 3 0
      templates/config.edn
  53. 25 24
      yarn.lock

+ 3 - 1
.clj-kondo/config.edn

@@ -22,7 +22,9 @@
                              frontend.util/node-path.dirname
                              frontend.util/node-path.join
                              frontend.util/node-path.extname
-                             frontend.util/node-path.name]}
+                             frontend.util/node-path.name
+                             ;; frontend.pubsub/def-mult-or-pub generate vars clj-kondo cannot resolve
+                             frontend.pubsub]}
 
   :consistent-alias
   {:aliases {cljs.reader reader

+ 0 - 13
.github/workflows/stale-issues.yml

@@ -48,13 +48,6 @@ jobs:
 
             Thanks for your contributions to Logseq! If you have any other [**issues**](https://github.com/logseq/logseq/issues/new/choose) or [**feature requests**](https://discuss.logseq.com/c/feature-requests/), please don't hesitate to [let us know](https://github.com/logseq/logseq/issues/new/choose). We always welcome pull requests too!
 
-      - name: 'Print outputs'
-        run: |
-          for key in steps.stale_issues.outputs
-            do
-              printf '%s: %s\n' "${key}" "${steps.stale_issues.outputs[key]}"
-            done
-
       - name: '🧹 Close stale awaiting response issues'
         id: awaiting_issues
         uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b  # v7
@@ -83,9 +76,3 @@ jobs:
 
             Thanks for your contributions to Logseq! If you have any other [**issues**](https://github.com/logseq/logseq/issues/new/choose) or [**feature requests**](https://discuss.logseq.com/c/feature-requests/), please don't hesitate to [let us know](https://github.com/logseq/logseq/issues/new/choose). We always welcome pull requests too!
 
-      - name: 'Print outputs'
-        run: |
-          for key in steps.awaiting_issues.outputs
-            do
-              printf '%s: %s\n' "${key}" "${steps.awaiting_issues.outputs[key]}"
-            done

+ 2 - 0
.gitignore

@@ -55,3 +55,5 @@ android/app/src/main/assets/capacitor.config.json
 
 *.sublime-*
 /public/static
+.yarn/
+.yarnrc.yml

+ 132 - 8
CODE_OF_CONDUCT.md

@@ -1,16 +1,140 @@
 # Code of Conduct
 
-## Purpose
+## Our Pledge
+
+In the interest of fostering a safe and welcoming environment, we as
+the Logseq team pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.  Logseq is not merely a note-taking tool, but can potentially become a community for collaboration and learning.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+  community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+  any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+  without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Enforcement Responsibilities
+
+Logseq team is responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Logseq team has the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
 
-Logseq aims to be a project inclusive to the largest number of people. It is also not merely a note-taking tool, but can potentially become a community for collaboration and learning. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status and religion (or lack thereof).
 
 ## Scope
 
-This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
+This Code of Conduct applies to all Logseq communication channels - online or in person,
+and it also applies when an individual is representing the project or its community in
+public spaces. Examples of representing a project or community include using an official
+project e-mail address, posting via an official social media account, or acting
+as an appointed representative at an online or offline event. Representation of
+a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by by contacting the moderators on the [Forum](https://discuss.logseq.com), [Discord](https://discord.gg/KpN4eHY) channel, Tienson Qin (@tiensonqin) on GitHub, or the official [Twitter](https://twitter.com/logseq). We will work with you to resolve the issue promptly. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The Logseq team
+will maintain confidentiality with regard to the reporter of an incident.
+Enforcement may result in an indefinite ban from all official Logseq communication
+channels, or other actions as deemed appropriate by the Logseq team.
+
+Logseq maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Enforcement Guidelines
+
+Logseq team and community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
 
-## Policy
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
 
-- Please respect each other. Do not dismiss, abuse, harass, attack, insult, or discriminate against others.
-- Likewise, any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
-- Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
-- If you feel being harassed or made uncomfortable by a community member, please report the incident(s) either by contacting the moderators on the [Forum](https://discuss.logseq.com), [Discord](https://discord.gg/KpN4eHY) channel, Tienson Qin (@tiensonqin) on GitHub, or the official [Twitter](https://twitter.com/logseq). We will work with you to resolve the issue promptly.
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations

+ 6 - 1
README.md

@@ -90,7 +90,12 @@ There are more guides in [docs/](docs/), e.g. the [Guide for contributing to tra
 ## How to contribute with a PR
 If you would like to contribute by solving an open issue, please fork this repository and then create a branch for the fix.
 
-Once you push your code to your fork, you'll be able to open a PR into Logseq repository. For more info you can follow this guide from [GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
+Once you push your code to your fork, you'll be able to open a PR into Logseq repository. For more info you can follow this guide from [GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). 
+
+Enabling "allow edits from maintainers" for PR is highly appreciated!
+
+There's a nice [project board](https://github.com/orgs/logseq/projects/5/views/1?pane=info
+) listing items that easy for contributors to catch-up
 
 And here a list of some [good first issues](https://github.com/logseq/logseq/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)!
 

+ 2 - 1
deps.edn

@@ -32,7 +32,8 @@
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
-                                cider/cider-nrepl                {:mvn/version "0.28.4"}
+                                org.clojure/tools.namespace      {:mvn/version "0.2.11"}
+                                cider/cider-nrepl                {:mvn/version "0.29.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 

+ 4 - 0
deps/graph-parser/.carve/ignore

@@ -38,3 +38,7 @@ logseq.graph-parser/get-blocks-to-delete
 logseq.graph-parser.property/colons-org
 ;; API
 logseq.graph-parser.util.db/resolve-input
+;; TODO: use fast-remove-nils instead
+logseq.graph-parser.util/remove-nils
+;; API
+logseq.graph-parser.text/get-file-basename

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

@@ -122,7 +122,7 @@ Options available:
                          new?
                          ;; TODO: use file system timestamp?
                          (assoc :file/created-at (date-time-util/time-ms)))])
-        tx' (gp-util/remove-nils tx)
+        tx' (gp-util/fast-remove-nils tx)
         result (if skip-db-transact?
                  tx'
                  (d/transact! conn tx' (select-keys options [:new-graph? :from-disk?])))]

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

@@ -376,7 +376,7 @@
   [blocks]
   (map (fn [block]
          (if (map? block)
-           (block-keywordize (gp-util/remove-nils block))
+           (block-keywordize (gp-util/remove-nils-non-nested block))
            block))
        blocks))
 

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

@@ -109,7 +109,7 @@
         invalid-properties (set (->> (map (comp name first) *invalid-properties)
                                      (concat invalid-properties)))
         page-m (->
-                (gp-util/remove-nils
+                (gp-util/remove-nils-non-nested
                  (assoc
                   (gp-block/page-name->map page false db true date-formatter
                                            :from-page from-page)

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

@@ -10,9 +10,15 @@
             [logseq.graph-parser.util.page-ref :as page-ref]))
 
 (defn get-file-basename
+  "Returns the basename of a file path. e.g. /a/b/c.md -> c.md"
+  [path]
+  (when-not (string/blank? path)
+    (.-base (path/parse (string/replace path "+" "/")))))
+
+(defn get-file-rootname
+  "Returns the rootname of a file path. e.g. /a/b/c.md -> c"
   [path]
   (when-not (string/blank? path)
-    ;; Same as util/node-path.name
     (.-name (path/parse (string/replace path "+" "/")))))
 
 (def page-ref-re-0 #"\[\[(.*)\]\]")
@@ -28,7 +34,7 @@
        (or (when-let [[_ label _path] (re-matches markdown-page-ref-re s)]
              (string/trim label))
            (when-let [[_ path _label] (re-matches org-page-ref-re s)]
-             (some-> (get-file-basename path)
+             (some-> (get-file-rootname path)
                      (string/replace "." "/")))
            (-> (re-matches page-ref-re-0 s)
                second))))

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

@@ -28,7 +28,8 @@
   (.normalize s "NFC"))
 
 (defn remove-nils
-  "remove pairs of key-value that has nil value from a (possibly nested) map."
+  "remove pairs of key-value that has nil value from a (possibly nested) map or
+  coll of maps."
   [nm]
   (walk/postwalk
    (fn [el]
@@ -37,6 +38,16 @@
        el))
    nm))
 
+(defn remove-nils-non-nested
+  "remove pairs of key-value that has nil value from a map (nested not supported)."
+  [nm]
+  (into {} (remove (comp nil? second)) nm))
+
+(defn fast-remove-nils
+  "remove pairs of key-value that has nil value from a coll of maps."
+  [nm]
+  (keep (fn [m] (if (map? m) (remove-nils-non-nested m) m)) nm))
+
 (defn split-first [pattern s]
   (when-let [first-index (string/index-of s pattern)]
     [(subs s 0 first-index)

+ 26 - 8
docs/dev-practices.md

@@ -11,8 +11,8 @@ this section, run `bb dev:lint`.
 ### Clojure code
 
 To lint:
-```
-clojure -M:clj-kondo --lint src
+```sh
+clojure -M:clj-kondo --parallel --lint src --cache false
 ```
 
 We lint our Clojure(Script) code with https://github.com/clj-kondo/clj-kondo/. If you need to configure specific linters, see [this documentation](https://github.com/clj-kondo/clj-kondo/blob/master/doc/linters.md). Where possible, a global linting configuration is used and namespace specific configuration is avoided.
@@ -27,7 +27,7 @@ There are outstanding linting items that are currently ignored to allow linting
 We use https://github.com/borkdude/carve to detect unused vars in our codebase.
 
 To run this linter:
-```
+```sh
 bb lint:carve
 ```
 
@@ -35,7 +35,7 @@ By default, the script runs in CI mode which prints unused vars if they are
 found. The script can be run in an interactive mode which prompts for keeping
 (ignoring) an unused var or removing it. Run this mode with:
 
-```
+```sh
 bb lint:carve '{:interactive true}'
 ```
 
@@ -46,7 +46,7 @@ why a var is ignored to help others understand why it's unused.
 
 Large vars have a lot of complexity and make it hard for the team to maintain
 and understand them. To run this linter:
-```
+```sh
 bb lint:large-vars
 ```
 
@@ -55,7 +55,7 @@ To configure the linter, see the `[:tasks/config :large-vars]` path of bb.edn.
 ### Document namespaces
 
 Documentation helps teams share their knowledge and enables more individuals to contribute to the codebase. Documenting our namespaces is a good first step to improving our documentation. To run this linter:
-```
+```sh
 bb lint:ns-docstrings
 ```
 
@@ -83,7 +83,7 @@ We have unit and end to end tests.
 
 To run end to end tests
 
-``` bash
+```sh
 yarn electron-watch
 # in another shell
 yarn e2e-test # or npx playwright test
@@ -91,8 +91,9 @@ yarn e2e-test # or npx playwright test
 
 If e2e failed after first running:
 - `rm -rdf ~/.logseq`
+- `rm -rdf ~/.config/Logseq`
 - `rm -rdf <repo dir>/tmp/`  
-- `rm -rdf <appData dir>/Electron`  (Reference: https://www.electronjs.org/de/docs/latest/api/app#appgetpathname)
+- Windows: `rmdir /s %APPDATA%/Electron`  (Reference: https://www.electronjs.org/de/docs/latest/api/app#appgetpathname)
 
 If e2e tests fail, they can be debugged by examining a trace dump with [the
 playwright trace
@@ -212,3 +213,20 @@ Currently the codebase is not formatted/indented consistently. We loosely follow
 There are some babashka tasks under `nbb:` which are useful for inspecting
 database changes in realtime. See [these
 docs](https://github.com/logseq/bb-tasks#logseqbb-tasksnbbwatch) for more info.
+
+## FAQ
+
+If dev app launch failed after electron upgrade:
+```sh
+yarn
+yarn watch
+```
+In another window:
+```sh
+cd static
+yarn
+cd ..
+yarn dev-electron-app
+``` 
+and kill all electron process
+Then a normal start happens via `yarn dev-electron-app`

+ 2 - 2
e2e-tests/plugins.spec.ts

@@ -1,7 +1,7 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
 
-test('enabled plugin system default', async ({ page }) => {
+test.skip('enabled plugin system default', async ({ page }) => {
   const callAPI = callPageAPI.bind(null, page)
 
   const pluginEnabled = await callAPI('get_state_from_store', 'plugin/enabled')
@@ -14,7 +14,7 @@ test('enabled plugin system default', async ({ page }) => {
   expect(Object.keys(currentGraph)).toEqual(['url', 'name', 'path'])
 })
 
-test('play a plugin<logseq-journals-calendar> from the Marketplace', async ({ page }) => {
+test.skip('play a plugin<logseq-journals-calendar> from the Marketplace', async ({ page }) => {
   await page.keyboard.press('t+p')
   const searchInput = page.locator('.search-ctls .form-input')
   await searchInput.type('journals')

+ 1 - 1
e2e-tests/whiteboards.spec.ts

@@ -105,7 +105,7 @@ test('zoom out', async ({ page }) => {
   await page.keyboard.press('Shift+0')
   await page.waitForTimeout(1000)
   await page.click('#tl-zoom-out')
-  await expect(page.locator('#tl-zoom')).toContainText('100%')
+  await expect(page.locator('#tl-zoom')).toContainText('80%')
 })
 
 test('open context menu', async ({ page }) => {

+ 1 - 1
libs/src/LSPlugin.ts

@@ -317,7 +317,7 @@ export interface IPluginSearchServiceHooks {
 
   onIndiceInit: (graph: string) => Promise<SearchIndiceInitStatus>
   onIndiceReset: (graph: string) => Promise<void>
-  onBlocksChanged: (graph: string, changes: { added: Array<SearchBlockItem>, removed: Array<BlockEntity> }) => Promise<void>
+  onBlocksChanged: (graph: string, changes: { added: Array<SearchBlockItem>, removed: Array<EntityID> }) => Promise<void>
   onGraphRemoved: (graph: string, opts?: {}) => Promise<any>
 }
 

+ 1 - 1
package.json

@@ -105,7 +105,7 @@
         "d3-force": "3.0.0",
         "diff": "5.0.0",
         "dompurify": "2.4.0",
-        "electron": "19.1.8",
+        "electron": "20.3.8",
         "electron-dl": "3.3.0",
         "fs": "0.0.1-security",
         "fs-extra": "9.1.0",

+ 12 - 12
resources/package.json

@@ -13,14 +13,14 @@
     "electron:make": "electron-forge make",
     "electron:make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
     "electron:publish:github": "electron-forge publish",
-    "rebuild:all": "electron-rebuild -v 19.1.8 -f",
+    "rebuild:all": "electron-rebuild -v 20.3.8 -f",
     "postinstall": "install-app-deps"
   },
   "config": {
     "forge": "./forge.config.js"
   },
   "dependencies": {
-    "better-sqlite3": "7.4.5",
+    "better-sqlite3": "8.0.1",
     "chokidar": "^3.5.1",
     "dugite": "1.108.0",
     "electron-dl": "3.3.0",
@@ -44,19 +44,19 @@
     "command-exists": "1.2.9"
   },
   "devDependencies": {
-    "@electron-forge/cli": "^6.0.0-beta.57",
-    "@electron-forge/maker-deb": "^6.0.0-beta.57",
-    "@electron-forge/maker-dmg": "^6.0.0-beta.57",
-    "@electron-forge/maker-rpm": "^6.0.0-beta.57",
-    "@electron-forge/maker-squirrel": "^6.0.0-beta.57",
-    "@electron-forge/maker-zip": "^6.0.0-beta.57",
-    "electron": "19.1.8",
+    "@electron-forge/cli": "^6.0.4",
+    "@electron-forge/maker-deb": "^6.0.4",
+    "@electron-forge/maker-dmg": "^6.0.4",
+    "@electron-forge/maker-rpm": "^6.0.4",
+    "@electron-forge/maker-squirrel": "^6.0.4",
+    "@electron-forge/maker-zip": "^6.0.4",
+    "@electron/rebuild": "3.2.10",
+    "electron": "20.3.8",
     "electron-builder": "^22.11.7",
-    "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git",
-    "electron-rebuild": "3.2.5"
+    "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git"
   },
   "resolutions": {
-    "**/electron": "19.1.8",
+    "**/electron": "20.3.8",
     "**/node-gyp": "9.0.0"
   }
 }

+ 12 - 9
src/electron/electron/handler.cljs

@@ -38,21 +38,24 @@
 (defmethod handle :mkdir-recur [_window [_ dir]]
   (fs/mkdirSync dir #js {:recursive true}))
 
-;; {encoding: 'utf8', withFileTypes: true}
 (defn- readdir
-  [dir]
+  "Read directory recursively, return all filenames"
+  [root-dir]
   (->> (tree-seq
-        (fn [^js fpath]
-          (.isDirectory (fs/statSync fpath)))
-        (fn [dir]
-          (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))]
+        (fn [[is-dir _fpath]]
+          is-dir)
+        (fn [[_is-dir dir]]
+          (let [files (fs/readdirSync dir #js {:withFileTypes true})]
             (->> files
                  (remove #(.isSymbolicLink ^js %))
                  (remove #(string/starts-with? (.-name ^js %) "."))
-                 (map #(.join path dir (.-name %))))))
-        dir)
+                 (map #(do
+                         [(.isDirectory %)
+                          (.join path dir (.-name %))])))))
+        [true root-dir])
+       (filter (complement first))
+       (map second)
        (map utils/fix-win-path!)
-       (doall)
        (vec)))
 
 (defmethod handle :readdir [_window [_ dir]]

+ 4 - 2
src/electron/electron/search.cljs

@@ -169,7 +169,8 @@
   [repo pages]
   (if-let [db (get-db repo)]
     ;; TODO: what if a CONFLICT on uuid
-    (let [insert (prepare db "INSERT INTO pages (id, uuid, content) VALUES (@id, @uuid, @content) ON CONFLICT (id) DO UPDATE SET content = @content")
+    ;; Should update all values on id conflict
+    (let [insert (prepare db "INSERT INTO pages (id, uuid, content) VALUES (@id, @uuid, @content) ON CONFLICT (id) DO UPDATE SET (uuid, content) = (@uuid, @content)")
           insert-many (.transaction ^object db
                                     (fn [pages]
                                       (doseq [page pages]
@@ -190,7 +191,8 @@
   [repo blocks]
   (if-let [db (get-db repo)]
     ;; TODO: what if a CONFLICT on uuid
-    (let [insert (prepare db "INSERT INTO blocks (id, uuid, content, page) VALUES (@id, @uuid, @content, @page) ON CONFLICT (id) DO UPDATE SET content = @content")
+    ;; Should update all values on id conflict
+    (let [insert (prepare db "INSERT INTO blocks (id, uuid, content, page) VALUES (@id, @uuid, @content, @page) ON CONFLICT (id) DO UPDATE SET (uuid, content, page) = (@uuid, @content, @page)")
           insert-many (.transaction ^object db
                                     (fn [blocks]
                                       (doseq [block blocks]

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

@@ -34,6 +34,7 @@
                      {:plugins                 true ; pdf
                       :nodeIntegration         false
                       :nodeIntegrationInWorker false
+                      :sandbox                 false
                       :webSecurity             (not dev?)
                       :contextIsolation        true
                       :spellcheck              ((fnil identity true) (cfgs/get-item :spell-check))

+ 13 - 4
src/main/frontend/components/block.cljs

@@ -2245,7 +2245,7 @@
         plugin-slotted? (and config/lsp-enabled? (state/slot-hook-exist? uuid))
         block-ref? (:block-ref? config)
         stop-events? (:stop-events? config)
-        block-ref-with-title? (and block-ref? (seq title))
+        block-ref-with-title? (and block-ref? (not (state/show-full-blocks?)) (seq title))
         block-type (or (:ls-type properties) :default)
         content (if (string? content) (string/trim content) "")
         mouse-down-key (if (util/ios?)
@@ -2611,14 +2611,23 @@
    (editor-handler/unhighlight-blocks!)))
 
 (defn- block-drop
-  [event uuid target-block *move-to]
+  [^js event uuid target-block *move-to]
   (util/stop event)
   (when-not (dnd-same-block? uuid)
     (let [block-uuids (state/get-selection-block-ids)
           lookup-refs (map (fn [id] [:block/uuid id]) block-uuids)
           selected (db/pull-many (state/get-current-repo) '[*] lookup-refs)
-          blocks (if (seq selected) selected [@*dragging-block])]
-      (dnd/move-blocks event blocks target-block @*move-to)))
+          blocks (if (seq selected) selected [@*dragging-block])
+          blocks (remove-nils blocks)]
+      (if-not (seq blocks)
+        (when-let [text (.getData (.-dataTransfer event) "text/plain")]
+          (editor-handler/api-insert-new-block!
+           text
+           {:block-uuid  uuid
+            :edit-block? false
+            :sibling?    (= @*move-to :sibling)
+            :before?     (= @*move-to :top)}))
+        (dnd/move-blocks event blocks target-block @*move-to))))
   (block-drag-end event *move-to))
 
 (defn- block-mouse-over

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

@@ -170,7 +170,7 @@
                               [:string (when-let [updated-at (:block/updated-at item)]
                                          (date/int->local-time-2 updated-at))]
 
-                              [:string (get-in item [:block/properties column])])]
+                              [:string (get-in item [:block/properties-text-values column])])]
                   [:td.whitespace-nowrap {:on-mouse-down (fn [] (reset! select? false))
                                           :on-mouse-move (fn [] (reset! select? true))
                                           :on-mouse-up (fn []

+ 2 - 0
src/main/frontend/components/search.cljs

@@ -331,6 +331,8 @@
        nil)]))
 
 (rum/defc search-auto-complete
+  "has-more? - if the result is truncated
+   all? - if true, in show-more mode"
   [{:keys [engine pages files pages-content blocks has-more?] :as result} search-q all?]
   (let [pages (when-not all? (map (fn [page]
                                     (let [alias (model/get-redirect-page-name page)]

+ 8 - 0
src/main/frontend/components/settings.cljs

@@ -366,6 +366,12 @@
           logical-outdenting?
           config-handler/toggle-logical-outdenting!))
 
+(defn showing-full-blocks [t show-full-blocks?]
+  (toggle "show_full_blocks"
+          (t :settings-page/show-full-blocks)
+          show-full-blocks?
+          config-handler/toggle-show-full-blocks!))
+
 (defn preferred-pasting-file [t preferred-pasting-file?]
   (toggle "preferred_pasting_file"
           (t :settings-page/preferred-pasting-file)
@@ -607,6 +613,7 @@
         enable-timetracking? (state/enable-timetracking?)
         enable-all-pages-public? (state/all-pages-public?)
         logical-outdenting? (state/logical-outdenting?)
+        show-full-blocks? (state/show-full-blocks?)
         preferred-pasting-file? (state/preferred-pasting-file?)
         enable-tooltip? (state/enable-tooltip?)
         enable-shortcut-tooltip? (state/sub :ui/shortcut-tooltip?)
@@ -622,6 +629,7 @@
 
      (when (util/electron?) (switch-spell-check-row t))
      (outdenting-row t logical-outdenting?)
+     (showing-full-blocks t show-full-blocks?)
      (preferred-pasting-file t preferred-pasting-file?)
      (when-not (or (util/mobile?) (mobile-util/native-platform?))
        (shortcut-tooltip-row t enable-shortcut-tooltip?))

+ 5 - 1
src/main/frontend/components/sidebar.css

@@ -223,9 +223,13 @@
         padding: 0;
         margin: 0;
 
+        li {
+          margin: 0;
+        }
+        
         a {
           width: 100%;
-          padding: 2px 24px;
+          padding: 4px 24px;
           transition: background-color .3s;
 
           .page-title {

+ 9 - 3
src/main/frontend/db/query_custom.cljs

@@ -28,7 +28,7 @@
   (let [{:keys [where in]} (datalog-util/query-vec->map query)
         rules-found (datalog-util/find-rules-in-where where (-> rules/query-dsl-rules keys set))]
     (if (seq rules-found)
-      (if (= '% (last in))
+      (if (and (= '% (last in)) (vector? (last (:inputs query-m))))
         ;; Add to existing :inputs rules
         (update query-m
                 :inputs
@@ -46,9 +46,15 @@
             (update :query
                     (fn [q]
                       (if (contains? (set q) :in)
-                        (datalog-util/add-to-end-of-query-section q :in ['%])
+                        ;; only add '% if not already present
+                        (if (not (contains? (set q) '%))
+                          (datalog-util/add-to-end-of-query-section q :in ['%])
+                          q)
                         (into q [:in '$ '%]))))
-            (assoc :rules (mapv rules/query-dsl-rules rules-found))))
+            (update :rules
+                    (fn [rules]
+                      (into (or rules [])
+                            (mapv rules/query-dsl-rules rules-found))))))
       query-m)))
 
 (defn custom-query

+ 1 - 2
src/main/frontend/db/utils.cljs

@@ -93,8 +93,7 @@
    (transact! repo-url tx-data nil))
   ([repo-url tx-data tx-meta]
    (when-not config/publishing?
-     (let [tx-data (->> (gp-util/remove-nils tx-data)
-                        (remove nil?))]
+     (let [tx-data (gp-util/fast-remove-nils tx-data)]
        (when (seq tx-data)
          (when-let [conn (conn/get-db repo-url false)]
            (if tx-meta

+ 185 - 4
src/main/frontend/dicts.cljc

@@ -218,8 +218,9 @@
         :settings-page/spell-checker "Spell checker"
         :settings-page/auto-updater "Auto updater"
         :settings-page/disable-sentry "Send usage data and diagnostics to Logseq"
-        :settings-page/disable-sentry-desc "Logseq will never collect your local graph database or sell your data." 
+        :settings-page/disable-sentry-desc "Logseq will never collect your local graph database or sell your data."
         :settings-page/preferred-outdenting "Logical outdenting"
+        :settings-page/show-full-blocks "Show all lines of a block reference"
         :settings-page/custom-date-format "Preferred date format"
         :settings-page/custom-date-format-warning "Re-index required! Existing journal references would be broken!"
         :settings-page/preferred-file-format "Preferred file format"
@@ -1449,6 +1450,20 @@
         :user/delete-account-notice "Toutes vos pages publiées sur logseq.com seront supprimées"
         :user/delete-your-account "Supprimer votre compte"
         :whiteboard/link-whiteboard-or-block "Lier un tablau blanc/page/bloc"
+
+        :file/validate-existing-file-error "La page existe déjà avec un autre fichier: {1}, fichier actuel: {2}. Veuillez n'en garder qu'un et réindexer votre graphique."
+        :notification/clear-all "Tout effacer"
+        :on-boarding/closed-feature "Fermé {1}"
+        :on-boarding/tour-whiteboard-home "{1} Commencez pour vos tableaux blancs"
+        :on-boarding/tour-whiteboard-home-description "Les tableaux blancs ont leur propre section dans l'application où vous pouvez les voir en un coup d'œil, en créer de nouveaux ou les supprimer facilement."
+        :on-boarding/tour-whiteboard-new "{1} Créer un nouveau tableau blanc"
+        :on-boarding/tour-whiteboard-new-description "Il existe plusieurs façons de créer un nouveau tableau blanc. L'un d'eux est toujours ici dans le tableau de bord."
+        :on-boarding/welcome-whiteboard-modal-description "Les tableaux blancs sont un excellent outil de brainstorming et d'organisation. Vous pouvez maintenant placer n'importe laquelle de vos pensées de la base de connaissances ou de nouvelles à côté l'une de l'autre sur une toile spatiale pour vous connecter, vous associer et comprendre de nouvelles façons."
+        :on-boarding/welcome-whiteboard-modal-skip "Sauter"
+        :on-boarding/welcome-whiteboard-modal-start "Démarrer le tableau blanc"
+        :on-boarding/welcome-whiteboard-modal-title "Un nouveau cadre pour vos pensées."
+        :settings-page/clear-cache-warning "Vider le cache supprimera les graphiques ouverts. Vous perdrez les modifications non enregistrées."
+        :settings-page/disable-sentry-desc "Logseq ne collectera jamais votre base de données de graphes locale ni ne vendra vos données."
      }
 
    :zh-CN {:accessibility/skip-to-main-content "跳转到主内容"
@@ -1586,7 +1601,7 @@
            :file/name "文件名"
            :file/file "文件:"
            :file/last-modified-at "最后更改于"
-           :file/no-data "没有数据" 
+           :file/no-data "没有数据"
            :file/validate-existing-file-error "页面已存在另一个文件: {1}, 当前文件: {2}. 请保留其中一个文件,然后重建当前图谱的索引。"
            :file-rn/re-index "重命名文件后,如果其他设备同步了改文件,强烈建议在同步成功后重新建立索引。"
            :file-rn/need-action "建议执行文件重命名操作以匹配新格式。当重命名的文件被同步后,请在所有设备上重新建立索引。"
@@ -2346,6 +2361,7 @@
         :settings-page/auto-updater "Auto actualizador"
         :settings-page/disable-sentry "Enviar datos de uso y diagnósticos a Logseq"
         :settings-page/preferred-outdenting "Disminución lógica de sangría"
+        :settings-page/show-full-blocks "Mostrar todas las líneas de una referencia a bloque"
         :settings-page/custom-date-format "Formato de fecha preferido"
         :settings-page/preferred-file-format "Formato de archivo preferido"
         :settings-page/preferred-workflow "Flujo de trabajo preferido"
@@ -3187,7 +3203,89 @@
            :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"}
+           :settings-page/tab-features "Recursos"
+           :all-whiteboards "Todos os quadros brancos"
+           :auto-heading "Título automático"
+           :go-to-whiteboard "Ir para o quadro branco"
+           :heading "Título {1}"
+           :new-whiteboard "Novo quadro branco"
+           :not-available-in-mode "Não disponível no modo {1}"
+           :remove-heading "Remover título"
+           :untitled "Sem título"
+           :accessibility/skip-to-main-content "Ir para o conteúdo principal"
+           :color/blue "Azul"
+           :color/gray "Cinza"
+           :color/green "Verde"
+           :color/pink "Rosa"
+           :color/purple "Roxo"
+           :color/red "Vermelho"
+           :color/yellow "Amarelo"
+           :conversion/non-desktop "O diretório Graph em versões antigas precisa ser convertido para o novo formato. Por favor, use o aplicativo de desktop para fazer a conversão."
+           :conversion/write-filename-format "Aplicar formato para arquivos recebidos"
+           :file/validate-existing-file-error "A página já existe com outro arquivo: {1}, arquivo atual: {2}. Por favor, mantenha apenas um deles e reindexe seu gráfico."
+           :file-rn/affected-pages "Páginas afetadas após a alteração do formato"
+           :file-rn/all-action "Aplicar todas as ações!"
+           :file-rn/apply-rename "Aplicar a operação de renomeação de arquivo"
+           :file-rn/close-panel "Fechar o painel"
+           :file-rn/confirm-proceed "Atualizar formato!"
+           :file-rn/filename-desc-1 "Esta configuração define como uma página é armazenada em um arquivo. Logseq armazena uma página em um arquivo com o mesmo nome."
+           :file-rn/filename-desc-2 "Alguns caracteres como \"/\" ou \"?\" são inválidos para um nome de arquivo."
+           :file-rn/filename-desc-3 "Logseq substitui caracteres inválidos por seu URL codificado equivalente para torná-los válidos (por exemplo, \"?\" torna-se \"%3F\")."
+           :file-rn/filename-desc-4 "O separador de espaço de nomes \"/\" também é substituído por \"___\" (sublinhado triplo) para consideração estética."
+           :file-rn/format-deprecated "Você está usando um formato desatualizado. A atualização para o formato mais recente é altamente recomendada. Faça backup de seus dados e feche os clientes Logseq em outros dispositivos antes da operação."
+           :file-rn/instruct-1 "É um processo de 2 etapas para atualizar o formato do nome do arquivo:"
+           :file-rn/instruct-2 "1. Clique em "
+           :file-rn/instruct-3 "2. Siga as instruções abaixo para renomear os arquivos para o novo formato:"
+           :file-rn/legend "🟢 Ações opcionais de renomeação; 🟡 Ação de renomeação necessária para evitar mudança de título; 🔴 Mudança de última hora."
+           :file-rn/need-action "As ações de renomeação de arquivo são sugeridas para corresponder ao novo formato. A reindexação é necessária em todos os dispositivos quando os arquivos renomeados são sincronizados."
+           :file-rn/no-action "Bom trabalho! Nenhuma ação adicional necessária."
+           :file-rn/optional-rename "Sugestão: "
+           :file-rn/or-select-actions " ou renomear individualmente os arquivos abaixo, então "
+           :file-rn/or-select-actions-2 ". Essas ações não estarão disponíveis depois que você fechar este painel."
+           :file-rn/otherwise-breaking "Ou o título se tornará"
+           :file-rn/re-index "A reindexação é fortemente recomendada após a renomeação dos arquivos e em outros dispositivos após a sincronização."
+           :file-rn/rename "renomeie o arquivo \"{1}\" para \"{2}\""
+           :file-rn/select-confirm-proceed "Dev: formato de gravação"
+           :file-rn/select-format "(Opção do modo de desenvolvedor, perigoso!) Selecione o formato do nome do arquivo"
+           :file-rn/suggest-rename "Ação necessária: "
+           :file-rn/unreachable-title "Aviso! O nome da página se tornará {1} no formato de nome de arquivo atual, a menos que a propriedade `title::` seja definida manualmente"
+           :left-side-bar/create "Criar"
+           :left-side-bar/new-whiteboard "Novo quadro branco"
+           :notification/clear-all "Limpar tudo"
+           :on-boarding/closed-feature "Fechado {1}"
+           :on-boarding/tour-whiteboard-home "{1} Início para seus quadros brancos"
+           :on-boarding/tour-whiteboard-home-description "Os quadros brancos têm sua própria seção no aplicativo, onde você pode vê-los rapidamente, criar novos ou excluí-los facilmente."
+           :on-boarding/tour-whiteboard-new "{1} Criar novo quadro branco"
+           :on-boarding/tour-whiteboard-new-description "Existem várias maneiras de criar um novo quadro branco. Um deles está sempre aqui no painel."
+           :on-boarding/welcome-whiteboard-modal-description "Quadros brancos são uma ótima ferramenta para brainstorming e organização. Agora você pode colocar qualquer um dos seus pensamentos da base de conhecimento ou novos próximos uns dos outros em uma tela espacial para conectar, associar e entender de novas maneiras."
+           :on-boarding/welcome-whiteboard-modal-skip "Pular"
+           :on-boarding/welcome-whiteboard-modal-start "Iniciar quadro branco"
+           :on-boarding/welcome-whiteboard-modal-title "Um novo quadro para seus pensamentos."
+           :page/show-whiteboards "Mostrar quadros brancos"
+           :pdf/doc-metadata "Metadados do documento"
+           :pdf/hl-block-colored "Rótulo colorido para bloco de destaque"
+           :plugin.install-from-file/menu-title "Instalar de plugins.edn"
+           :plugin.install-from-file/notice "Os seguintes plugins substituirão seus plugins:"
+           :plugin.install-from-file/success "Todos os plugins instalados"
+           :plugin.install-from-file/title "Instalar plugins de plugins.edn"
+           :right-side-bar/separator "Manipulador de redimensionamento da barra lateral direita"
+           :right-side-bar/whiteboards "Quadros brancos"
+           :search-item/block "Bloco"
+           :search-item/file "Arquivo"
+           :search-item/page "Página"
+           :search-item/whiteboard "Quadro branco"
+           :settings-page/alpha-features "Recursos em fase Alfa"
+           :settings-page/beta-features "Recursos em fase Beta"
+           :settings-page/clear-cache-warning "A limpeza do cache descartará os gráficos abertos. Você perderá as alterações não salvas."
+           :settings-page/custom-date-format-warning "É necessário reindexar! As referências de periódicos existentes seriam quebradas!"
+           :settings-page/disable-sentry-desc "A Logseq nunca coletará seu banco de dados gráfico local ou venderá seus dados."
+           :settings-page/edit-setting "Editar"
+           :settings-page/enable-whiteboards "Quadros brancos"
+           :settings-page/filename-format "Formato dos nomes de arquivos"
+           :settings-page/login-prompt "Para acessar novos recursos antes de qualquer outra pessoa, você deve ser um Patrocinador Coletivo Aberto ou Apoiador do Logseq e, portanto, fazer o login primeiro."
+           :settings-page/preferred-pasting-file "Arquivo preferência para colar"
+           :settings-page/tab-assets "Recursos"
+           :whiteboard/link-whiteboard-or-block "Vincular quadro branco/página/bloco"}
 
    :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"
@@ -3529,7 +3627,90 @@
         :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"}
+        :settings-page/tab-features "Recursos"
+
+         :all-whiteboards "Todos os quadros brancos"
+         :auto-heading "Título automático"
+         :go-to-whiteboard "Ir para o quadro branco"
+         :heading "Título {1}"
+         :not-available-in-mode "Não disponível no modo {1}"
+         :remove-heading "Remover título"
+         :untitled "Sem título"
+         :accessibility/skip-to-main-content "Ir para o conteúdo principal"
+         :color/blue "Azul"
+         :color/gray "Cinza"
+         :color/green "Verde"
+         :color/pink "Rosa"
+         :color/purple "Roxo"
+         :color/red "Vermelho"
+         :color/yellow "Amarelo"
+         :conversion/non-desktop "O diretório Graph em versões antigas precisa ser convertido para o novo formato. Por favor, use o aplicativo de desktop para fazer a conversão."
+         :conversion/write-filename-format "Aplicar formato para arquivos recebidos"
+         :file/validate-existing-file-error "A página já existe com outro arquivo: {1}, arquivo atual: {2}. Por favor, mantenha apenas um deles e reindexe seu gráfico."
+         :file-rn/affected-pages "Páginas afetadas após a alteração do formato"
+         :file-rn/all-action "Aplicar todas as ações!"
+         :file-rn/apply-rename "Aplicar a operação de renomeação de arquivo"
+         :file-rn/close-panel "Fechar o painel"
+         :file-rn/confirm-proceed "Atualizar formato!"
+         :file-rn/filename-desc-1 "Esta configuração define como uma página é armazenada em um arquivo. Logseq armazena uma página em um arquivo com o mesmo nome."
+         :file-rn/filename-desc-2 "Alguns caracteres como \"/\" ou \"?\" são inválidos para um nome de arquivo."
+         :file-rn/filename-desc-3 "Logseq substitui caracteres inválidos por seu URL codificado equivalente para torná-los válidos (por exemplo, \"?\" torna-se \"%3F\")."
+         :file-rn/filename-desc-4 "O separador de espaço de nomes \"/\" também é substituído por \"___\" (sublinhado triplo) para consideração estética."
+         :file-rn/format-deprecated "Você está usando um formato desatualizado. A atualização para o formato mais recente é altamente recomendada. Faça backup de seus dados e feche os clientes Logseq em outros dispositivos antes da operação."
+         :file-rn/instruct-1 "É um processo de 2 etapas para atualizar o formato do nome do arquivo:"
+         :file-rn/instruct-2 "1. Clique em "
+         :file-rn/instruct-3 "2. Siga as instruções abaixo para renomear os arquivos para o novo formato:"
+         :file-rn/legend "🟢 Ações opcionais de renomeação; 🟡 Ação de renomeação necessária para evitar mudança de título; 🔴 Mudança de última hora."
+         :file-rn/need-action "As ações de renomeação de arquivo são sugeridas para corresponder ao novo formato. A reindexação é necessária em todos os dispositivos quando os arquivos renomeados são sincronizados."
+         :file-rn/no-action "Bom trabalho! Nenhuma ação adicional necessária."
+         :file-rn/optional-rename "Sugestão: "
+         :file-rn/or-select-actions " ou renomear individualmente os arquivos abaixo, então "
+         :file-rn/or-select-actions-2 ". Essas ações não estarão disponíveis depois que você fechar este painel."
+         :file-rn/otherwise-breaking "Ou o título se tornará"
+         :file-rn/re-index "A reindexação é fortemente recomendada após a renomeação dos arquivos e em outros dispositivos após a sincronização."
+         :file-rn/rename "renomeie o arquivo \"{1}\" para \"{2}\""
+         :file-rn/select-confirm-proceed "Dev: formato de gravação"
+         :file-rn/select-format "(Opção do modo de desenvolvedor, perigoso!) Selecione o formato do nome do arquivo"
+         :file-rn/suggest-rename "Ação necessária: "
+         :file-rn/unreachable-title "Aviso! O nome da página se tornará {1} no formato de nome de arquivo atual, a menos que a propriedade `title::` seja definida manualmente"
+         :left-side-bar/create "Criar"
+         :left-side-bar/new-whiteboard "Novo quadro branco"
+         :notification/clear-all "Limpar tudo"
+         :on-boarding/closed-feature "Fechado {1}"
+         :on-boarding/tour-whiteboard-home "{1} Início para seus quadros brancos"
+         :on-boarding/tour-whiteboard-home-description "Os quadros brancos têm sua própria seção no aplicativo, onde você pode vê-los rapidamente, criar novos ou excluí-los facilmente."
+         :on-boarding/tour-whiteboard-new "{1} Criar novo quadro branco"
+         :on-boarding/tour-whiteboard-new-description "Existem várias maneiras de criar um novo quadro branco. Um deles está sempre aqui no painel."
+         :on-boarding/welcome-whiteboard-modal-description "Quadros brancos são uma ótima ferramenta para brainstorming e organização. Agora você pode colocar qualquer um dos seus pensamentos da base de conhecimento ou novos próximos uns dos outros em uma tela espacial para conectar, associar e entender de novas maneiras."
+         :on-boarding/welcome-whiteboard-modal-skip "Pular"
+         :on-boarding/welcome-whiteboard-modal-start "Iniciar quadro branco"
+         :on-boarding/welcome-whiteboard-modal-title "Um novo quadro para seus pensamentos."
+         :page/show-whiteboards "Mostrar quadros brancos"
+         :pdf/doc-metadata "Metadados do documento"
+         :pdf/hl-block-colored "Rótulo colorido para bloco de destaque"
+         :plugin.install-from-file/menu-title "Instalar de plugins.edn"
+         :plugin.install-from-file/notice "Os seguintes plugins substituirão seus plugins:"
+         :plugin.install-from-file/success "Todos os plugins instalados"
+         :plugin.install-from-file/title "Instalar plugins de plugins.edn"
+         :right-side-bar/separator "Manipulador de redimensionamento da barra lateral direita"
+         :right-side-bar/whiteboards "Quadros brancos"
+         :search-item/block "Bloco"
+         :search-item/file "Arquivo"
+         :search-item/page "Página"
+         :search-item/whiteboard "Quadro branco"
+         :settings-page/alpha-features "Recursos em fase Alfa"
+         :settings-page/beta-features "Recursos em fase Beta"
+         :settings-page/clear-cache-warning "A limpeza do cache descartará os gráficos abertos. Você perderá as alterações não salvas."
+         :settings-page/custom-date-format-warning "É necessário reindexar! As referências de periódicos existentes seriam quebradas!"
+         :settings-page/disable-sentry-desc "A Logseq nunca coletará seu banco de dados gráfico local ou venderá seus dados."
+         :settings-page/edit-setting "Editar"
+         :settings-page/enable-whiteboards "Quadros brancos"
+         :settings-page/filename-format "Formato dos nomes de arquivos"
+         :settings-page/login-prompt "Para acessar novos recursos antes de qualquer outra pessoa, você deve ser um Patrocinador Coletivo Aberto ou Apoiador do Logseq e, portanto, fazer o login primeiro."
+         :settings-page/preferred-pasting-file "Arquivo preferência para colar"
+         :settings-page/tab-assets "Recursos"
+         :whiteboard/link-whiteboard-or-block "Vincular quadro branco/página/bloco"
+         :new-whiteboard "Novo quadro branco"}
 
    :ru {:on-boarding/demo-graph "Это демонстрационный граф, изменения не будут сохранены, пока вы не откроете локальный файл."
         :on-boarding/add-graph "Добавить новый граф"

+ 20 - 19
src/main/frontend/extensions/pdf/assets.cljs

@@ -184,25 +184,26 @@
   ([pdf hl] (ensure-ref-block! pdf hl nil))
   ([pdf-current {:keys [id content page properties]} insert-opts]
    (when-let [ref-page (and pdf-current (resolve-ref-page pdf-current))]
-     (if-let [ref-block (db-model/query-block-by-uuid id)]
-       (do
-         (println "[existed ref block]" ref-block)
-         ref-block)
-       (let [text       (:text content)
-             wrap-props #(if-let [stamp (:image content)]
-                           (assoc % :hl-type "area" :hl-stamp stamp) %)]
-
-         (when (string? text)
-           (editor-handler/api-insert-new-block!
-            text (merge {:page        (:block/name ref-page)
-                         :custom-uuid id
-                         :properties  (wrap-props
-                                       {:ls-type  "annotation"
-                                        :hl-page  page
-                                        :hl-color (:color properties)
-                                        ;; force custom uuid
-                                        :id       (str id)})}
-                        insert-opts))))))))
+     (let [ref-block (db-model/query-block-by-uuid id)]
+       (if-not (nil? (:block/content ref-block))
+         (do
+           (println "[existed ref block]" ref-block)
+           ref-block)
+         (let [text       (:text content)
+               wrap-props #(if-let [stamp (:image content)]
+                             (assoc % :hl-type "area" :hl-stamp stamp) %)]
+
+           (when (string? text)
+             (editor-handler/api-insert-new-block!
+              text (merge {:page        (:block/name ref-page)
+                           :custom-uuid id
+                           :properties  (wrap-props
+                                         {:ls-type  "annotation"
+                                          :hl-page  page
+                                          :hl-color (:color properties)
+                                          ;; force custom uuid
+                                          :id       (str id)})}
+                          insert-opts)))))))))
 
 (defn del-ref-block!
   [{:keys [id]}]

+ 62 - 44
src/main/frontend/extensions/pdf/highlights.cljs

@@ -2,6 +2,7 @@
   (:require [cljs-bean.core :as bean]
             [clojure.string :as string]
             [frontend.components.svg :as svg]
+            [frontend.components.block :as block]
             [frontend.context.i18n :refer [t]]
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.extensions.pdf.utils :as pdf-utils]
@@ -93,11 +94,11 @@
   "The contextual menu which appears over a text selection and allows e.g. creating a highlight."
   [^js viewer
    {:keys [highlight point ^js selection]}
-   {:keys [clear-ctx-tip! add-hl! upd-hl! del-hl!]}]
+   {:keys [clear-ctx-menu! add-hl! upd-hl! del-hl!]}]
 
   (rum/use-effect!
    (fn []
-     (let [cb #(clear-ctx-tip!)]
+     (let [cb #(clear-ctx-menu!)]
        (js/setTimeout #(js/document.addEventListener "click" cb))
        #(js/document.removeEventListener "click" cb)))
    [])
@@ -158,7 +159,7 @@
 
                               (reset! *highlight-last-color (keyword action)))))
 
-                        (and clear? (js/setTimeout #(clear-ctx-tip!) 68))))]
+                        (and clear? (js/setTimeout #(clear-ctx-menu!) 68))))]
 
     (rum/use-effect!
      (fn []
@@ -208,43 +209,58 @@
      ]))
 
 (rum/defc pdf-highlights-text-region
-  [^js viewer vw-hl hl
-   {:keys [show-ctx-tip!]}]
+  [^js viewer vw-hl hl {:keys [show-ctx-menu!]}]
 
-  (let [{:keys [rects]} (:position vw-hl)
+  (let [{:keys [id]} hl
+        {:keys [rects]} (:position vw-hl)
         {:keys [color]} (:properties hl)
-        open-tip! (fn [^js/MouseEvent e]
-                    (.preventDefault e)
-                    (let [x (.-clientX e)
-                          y (.-clientY e)]
 
-                      (show-ctx-tip! viewer hl {:x x :y y})))]
+        open-ctx-menu!
+        (fn [^js/MouseEvent e]
+          (.preventDefault e)
+          (let [x (.-clientX e)
+                y (.-clientY e)]
+
+            (show-ctx-menu! viewer hl {:x x :y y})))
+
+        dragstart-handle!
+        (fn [^js e]
+          (when-let [^js dt (and id (.-dataTransfer e))]
+            (reset! block/*dragging? true)
+            (pdf-assets/ensure-ref-block! (state/get-current-pdf) hl)
+            (.setData dt "text/plain" (str "((" id "))"))))]
 
     [:div.extensions__pdf-hls-text-region
-     {:on-click        open-tip!
-      :on-context-menu open-tip!}
+     {:on-click        open-ctx-menu!
+      :on-context-menu open-ctx-menu!}
 
      (map-indexed
       (fn [idx rect]
         [:div.hls-text-region-item
-         {:key        idx
-          :style      rect
-          :data-color color}])
+         {:key           idx
+          :style         rect
+          :draggable     "true"
+          :on-drag-start dragstart-handle!
+          :data-color    color}])
       rects)]))
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-region
-  [^js viewer vw-hl hl
-   {:keys [show-ctx-tip! upd-hl!]}]
+  [^js viewer vw-hl hl {:keys [show-ctx-menu! upd-hl!]}]
+
+  (let [{:keys [id]} hl
+        *el    (rum/use-ref nil)
+        *dirty (rum/use-ref nil)
+        open-ctx-menu! (fn [^js/MouseEvent e]
+                         (.preventDefault e)
+                         (when-not (rum/deref *dirty)
+                           (let [x (.-clientX e)
+                                 y (.-clientY e)]
 
-  (let [*el       (rum/use-ref nil)
-        *dirty    (rum/use-ref nil)
-        open-tip! (fn [^js/MouseEvent e]
-                    (.preventDefault e)
-                    (when-not (rum/deref *dirty)
-                      (let [x (.-clientX e)
-                            y (.-clientY e)]
+                             (show-ctx-menu! viewer hl {:x x :y y}))))
 
-                        (show-ctx-tip! viewer hl {:x x :y y}))))]
+        dragstart-handle! (fn [^js e]
+                            (when-let [^js dt (and id (.-dataTransfer e))]
+                              (.setData dt "text/plain" (str "((" id "))"))))]
 
     ;; resizable
     (rum/use-effect!
@@ -331,8 +347,10 @@
          {:ref             *el
           :style           vw-bounding
           :data-color      color
-          :on-click        open-tip!
-          :on-context-menu open-tip!}]))))
+          :draggable       "true"
+          :on-drag-start   dragstart-handle!
+          :on-click        open-ctx-menu!
+          :on-context-menu open-ctx-menu!}]))))
 
 (rum/defc pdf-highlights-region-container
   "Displays the highlights over a pdf document."
@@ -349,7 +367,7 @@
        ))])
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-selection
-  [^js viewer {:keys [show-ctx-tip!]}]
+  [^js viewer {:keys [show-ctx-menu!]}]
 
   (let [^js viewer-clt          (.. viewer -viewer -classList)
         ^js cnt-el              (.-container viewer)
@@ -439,7 +457,7 @@
                                                      :properties {}}]
 
                                     ;; ctx tips
-                                    (show-ctx-tip! viewer hl point {:reset-fn #(reset-coords)})
+                                    (show-ctx-menu! viewer hl point {:reset-fn #(reset-coords)})
 
                                     ;; export area highlight
                                     ;;(dd "[selection end] :start"
@@ -481,17 +499,17 @@
         *mounted       (rum/use-ref false)
         [sel-state, set-sel-state!] (rum/use-state {:selection nil :range nil :collapsed nil :point nil})
         [highlights, set-highlights!] (rum/use-state initial-hls)
-        [tip-state, set-tip-state!] (rum/use-state {:highlight nil :vw-pos nil :selection nil :point nil :reset-fn nil})
+        [ctx-menu-state, set-ctx-menu-state!] (rum/use-state {:highlight nil :vw-pos nil :selection nil :point nil :reset-fn nil})
 
-        clear-ctx-tip! (rum/use-callback
-                        #(let [reset-fn (:reset-fn tip-state)]
-                           (set-tip-state! {})
+        clear-ctx-menu! (rum/use-callback
+                        #(let [reset-fn (:reset-fn ctx-menu-state)]
+                           (set-ctx-menu-state! {})
                            (and (fn? reset-fn) (reset-fn)))
-                        [tip-state])
+                        [ctx-menu-state])
 
-        show-ctx-tip!  (fn [^js viewer hl point & ops]
+        show-ctx-menu!  (fn [^js viewer hl point & ops]
                          (let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
-                           (set-tip-state! (apply merge (list* {:highlight hl :vw-pos vw-pos :point point} ops)))))
+                           (set-ctx-menu-state! (apply merge (list* {:highlight hl :vw-pos vw-pos :point point} ops)))))
 
         add-hl!        (fn [hl] (when (:id hl)
                                   ;; fix js object
@@ -603,7 +621,7 @@
 
            ;; show ctx menu
            (js/setTimeout (fn []
-                            (set-tip-state! {:highlight hl-fn
+                            (set-ctx-menu-state! {:highlight hl-fn
                                              :selection selection
                                              :point     point})))) 0))
 
@@ -621,7 +639,7 @@
 
                (rum/mount
                 (pdf-highlights-region-container
-                 viewer page-hls {:show-ctx-tip! show-ctx-tip!
+                 viewer page-hls {:show-ctx-menu! show-ctx-menu!
                                   :upd-hl!       upd-hl!})
 
                 hls-layer)))))
@@ -633,10 +651,10 @@
     [:div.extensions__pdf-highlights-cnt
 
      ;; hl context tip menu
-     (when-let [_hl (:highlight tip-state)]
+     (when-let [_hl (:highlight ctx-menu-state)]
        (js/ReactDOM.createPortal
-        (pdf-highlights-ctx-menu viewer tip-state
-                                 {:clear-ctx-tip! clear-ctx-tip!
+        (pdf-highlights-ctx-menu viewer ctx-menu-state
+                                 {:clear-ctx-menu! clear-ctx-menu!
                                   :add-hl!        add-hl!
                                   :del-hl!        del-hl!
                                   :upd-hl!        upd-hl!})
@@ -660,8 +678,8 @@
      ;; area selection container
      (pdf-highlight-area-selection
       viewer
-      {:clear-ctx-tip! clear-ctx-tip!
-       :show-ctx-tip!  show-ctx-tip!
+      {:clear-ctx-menu! clear-ctx-menu!
+       :show-ctx-menu!  show-ctx-menu!
        :add-hl!        add-hl!
        })]))
 

+ 151 - 109
src/main/frontend/fs/sync.cljs

@@ -27,6 +27,7 @@
             [frontend.db :as db]
             [frontend.fs :as fs]
             [frontend.encrypt :as encrypt]
+            [frontend.pubsub :as pubsub]
             [logseq.graph-parser.util :as gp-util]
             [medley.core :refer [dedupe-by]]
             [rum.core :as rum]
@@ -604,28 +605,29 @@
   keep this `FileMetadata` in result"
   [s1 s2]
   (reduce
-   (fn [result item]
-     (let [path (:path item)
-           encrypted-path (:encrypted-path item)
-           checksum (:etag item)
-           last-modified (:last-modified item)]
-       (if (some
-            #(cond
-               (not= encrypted-path (:encrypted-path %))
-               false
-               (= checksum (:etag %))
-               true
-               (>= last-modified (:last-modified %))
-               false
-               ;; these special files have higher priority in s1
-               (contains? higher-priority-remote-files path)
-               false
-               (< last-modified (:last-modified %))
-               true)
-            s2)
-         result
-         (conj result item))))
-   #{} s1))
+     (fn [result item]
+       (let [path (:path item)
+             lower-case-path (some-> path string/lower-case)
+             ;; encrypted-path (:encrypted-path item)
+             checksum (:etag item)
+             last-modified (:last-modified item)]
+         (if (some
+              #(cond
+                 (not= lower-case-path (some-> (:path %) string/lower-case))
+                 false
+                 (= checksum (:etag %))
+                 true
+                 (>= last-modified (:last-modified %))
+                 false
+                 ;; these special files have higher priority in s1
+                 (contains? higher-priority-remote-files path)
+                 false
+                 (< last-modified (:last-modified %))
+                 true)
+              s2)
+           result
+           (conj result item))))
+     #{} s1))
 
 (comment
  (defn map->FileMetadata [m]
@@ -1179,6 +1181,36 @@
     (let [encrypted-paths-to-drop (set (persistent! *encrypted-paths-to-drop))]
       (filterv #(not (contains? encrypted-paths-to-drop (:encrypted-path %))) file-meta-list))))
 
+(defn- filter-case-different-same-files
+  "filter case-different-but-same-name files, last-modified one wins"
+  [file-meta-list encrypted-path->path-map]
+  (let [seen (volatile! {})]
+    (loop [result-file-meta-list (transient {})
+           [f & others] file-meta-list]
+      (if f
+        (let [origin-path (get encrypted-path->path-map (:encrypted-path f))
+              _ (assert (some? origin-path) f)
+              path (string/lower-case origin-path)
+              last-modified (:last-modified f)
+              last-modified-seen (get @seen path)]
+          (cond
+            (or (and path (nil? last-modified-seen))
+                (and path (some? last-modified-seen) (> last-modified last-modified-seen)))
+            ;; 1. not found in seen
+            ;; 2. found in seen, but current f wins
+            (do (vswap! seen conj [path last-modified])
+                (recur (conj! result-file-meta-list [path f]) others))
+
+            (and path (some? last-modified-seen) (<= last-modified last-modified-seen))
+            ;; found in seen, and seen-f has more recent last-modified epoch
+            (recur result-file-meta-list others)
+
+            :else
+            (do (println :debug-filter-case-different-same-files:unreachable f path)
+                (recur result-file-meta-list others))))
+        (vals (persistent! result-file-meta-list))))))
+
+
 (extend-type RemoteAPI
   IRemoteAPI
   (<user-info [this]
@@ -1189,28 +1221,28 @@
      (let [file-meta-list      (transient #{})
            encrypted-path-list (transient [])
            exp-r
-                               (<!
-                                (go-loop [continuation-token nil]
-                                  (let [r (<! (.<request this "get_all_files"
-                                                         (into
-                                                          {}
-                                                          (remove (comp nil? second)
-                                                                  {:GraphUUID graph-uuid :ContinuationToken continuation-token}))))]
-                                    (if (instance? ExceptionInfo r)
-                                      r
-                                      (let [next-continuation-token (:NextContinuationToken r)
-                                            objs                    (:Objects r)]
-                                        (apply conj! encrypted-path-list (map (comp remove-user-graph-uuid-prefix :Key) objs))
-                                        (apply conj! file-meta-list
-                                               (map
-                                                #(hash-map :checksum (:checksum %)
-                                                           :encrypted-path (remove-user-graph-uuid-prefix (:Key %))
-                                                           :size (:Size %)
-                                                           :last-modified (:LastModified %)
-                                                           :txid (:Txid %))
-                                                objs))
-                                        (when-not (empty? next-continuation-token)
-                                          (recur next-continuation-token)))))))]
+           (<!
+            (go-loop [continuation-token nil]
+              (let [r (<! (.<request this "get_all_files"
+                                     (into
+                                      {}
+                                      (remove (comp nil? second)
+                                              {:GraphUUID graph-uuid :ContinuationToken continuation-token}))))]
+                (if (instance? ExceptionInfo r)
+                  r
+                  (let [next-continuation-token (:NextContinuationToken r)
+                        objs                    (:Objects r)]
+                    (apply conj! encrypted-path-list (map (comp remove-user-graph-uuid-prefix :Key) objs))
+                    (apply conj! file-meta-list
+                           (map
+                            #(hash-map :checksum (:checksum %)
+                                       :encrypted-path (remove-user-graph-uuid-prefix (:Key %))
+                                       :size (:Size %)
+                                       :last-modified (:LastModified %)
+                                       :txid (:Txid %))
+                            objs))
+                    (when-not (empty? next-continuation-token)
+                      (recur next-continuation-token)))))))]
        (if (instance? ExceptionInfo exp-r)
          exp-r
          (let [file-meta-list*      (persistent! file-meta-list)
@@ -1229,7 +1261,9 @@
                                   true
                                   (:txid %)
                                   nil)
-                 (filter-files-with-unnormalized-path file-meta-list* encrypted-path->path-map))))))))))
+                 (-> file-meta-list*
+                     (filter-files-with-unnormalized-path encrypted-path->path-map)
+                     (filter-case-different-same-files encrypted-path->path-map)))))))))))
 
   (<get-remote-files-meta [this graph-uuid filepaths]
     {:pre [(coll? filepaths)]}
@@ -1286,59 +1320,59 @@
                                                (:Transactions r))
                encrypted-paths           (mapcat :paths txns-with-encrypted-paths)
                encrypted-path->path-map
-                                         (zipmap
-                                          encrypted-paths
-                                          (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
+               (zipmap
+                encrypted-paths
+                (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
                txns
-                                         (mapv
-                                          (fn [txn]
-                                            (assoc txn :paths (mapv #(get encrypted-path->path-map %) (:paths txn))))
-                                          txns-with-encrypted-paths)]
+               (mapv
+                (fn [txn]
+                  (assoc txn :paths (mapv #(get encrypted-path->path-map %) (:paths txn))))
+                txns-with-encrypted-paths)]
            txns)))))
 
   (<get-diff [this graph-uuid from-txid]
-   ;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
+    ;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
     (user/<wrap-ensure-id&access-token
      (let [r (<! (.<request this "get_diff" {:GraphUUID graph-uuid :FromTXId from-txid}))]
        (if (instance? ExceptionInfo r)
          r
          (let [txns-with-encrypted-paths (sort-by :TXId (:Transactions r))
                txns-with-encrypted-paths*
-                                         (mapv
-                                          (fn [txn]
-                                            (assoc txn :TXContent
-                                                   (mapv
-                                                    (fn [[to-path from-path checksum]]
-                                                      [(remove-user-graph-uuid-prefix to-path)
-                                                       (some-> from-path remove-user-graph-uuid-prefix)
-                                                       checksum])
-                                                    (:TXContent txn))))
-                                          txns-with-encrypted-paths)
+               (mapv
+                (fn [txn]
+                  (assoc txn :TXContent
+                         (mapv
+                          (fn [[to-path from-path checksum]]
+                            [(remove-user-graph-uuid-prefix to-path)
+                             (some-> from-path remove-user-graph-uuid-prefix)
+                             checksum])
+                          (:TXContent txn))))
+                txns-with-encrypted-paths)
                encrypted-paths
-                                         (mapcat
-                                          (fn [txn]
-                                            (remove
-                                             #(or (nil? %) (not (string/starts-with? % "e.")))
-                                             (mapcat
-                                              (fn [[to-path from-path _checksum]] [to-path from-path])
-                                              (:TXContent txn))))
-                                          txns-with-encrypted-paths*)
+               (mapcat
+                (fn [txn]
+                  (remove
+                   #(or (nil? %) (not (string/starts-with? % "e.")))
+                   (mapcat
+                    (fn [[to-path from-path _checksum]] [to-path from-path])
+                    (:TXContent txn))))
+                txns-with-encrypted-paths*)
                encrypted-path->path-map
-                                         (zipmap
-                                          encrypted-paths
-                                          (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
+               (zipmap
+                encrypted-paths
+                (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
                txns
-                                         (mapv
-                                          (fn [txn]
-                                            (assoc
-                                              txn :TXContent
-                                              (mapv
-                                               (fn [[to-path from-path checksum]]
-                                                 [(get encrypted-path->path-map to-path to-path)
-                                                  (some->> from-path (get encrypted-path->path-map))
-                                                  checksum])
-                                               (:TXContent txn))))
-                                          txns-with-encrypted-paths*)]
+               (mapv
+                (fn [txn]
+                  (assoc
+                   txn :TXContent
+                   (mapv
+                    (fn [[to-path from-path checksum]]
+                      [(get encrypted-path->path-map to-path to-path)
+                       (some->> from-path (get encrypted-path->path-map))
+                       checksum])
+                    (:TXContent txn))))
+                txns-with-encrypted-paths*)]
            [txns
             (:TXId (last txns))
             (:TXId (first txns))])))))
@@ -2314,7 +2348,9 @@
               {:stop true})
           (let [remote-all-files-meta   remote-all-files-meta-or-exp
                 local-all-files-meta    (<! local-all-files-meta-c)
-                diff-remote-files       (diff-file-metadata-sets remote-all-files-meta local-all-files-meta)
+                {diff-remote-files :result elapsed-time :time}
+                (util/with-time (diff-file-metadata-sets remote-all-files-meta local-all-files-meta))
+                _ (println ::diff-file-metadata-sets-elapsed-time elapsed-time "ms")
                 recent-10-days-range    ((juxt #(tc/to-long (t/minus % (t/days 10))) #(tc/to-long %)) (t/today))
                 sorted-diff-remote-files
                                         (sort-by
@@ -2476,12 +2512,14 @@
                  true)
                (or (string/starts-with? (.-dir e) base-path)
                    (string/starts-with? (str "file://" (.-dir e)) base-path)) ; valid path prefix
-               (not (ignored? e))     ;not ignored
+               (not (ignored? e))       ;not ignored
                ;; download files will also trigger file-change-events, ignore them
-               (when-some [recent-remote->local-file-item
-                           (<! (<file-change-event=>recent-remote->local-file-item
-                                graph-uuid e))]
-                 (not (contains? (:recent-remote->local-files @*sync-state) recent-remote->local-file-item)))))))
+               (if (= "unlink" (:type e))
+                 true
+                 (when-some [recent-remote->local-file-item
+                             (<! (<file-change-event=>recent-remote->local-file-item
+                                  graph-uuid e))]
+                   (not (contains? (:recent-remote->local-files @*sync-state) recent-remote->local-file-item))))))))
 
   (set-remote->local-syncer! [_ s] (set! remote->local-syncer s))
 
@@ -2504,7 +2542,7 @@
        (fn [e]
          (go
            (and (rsapi-ready? rsapi graph-uuid)
-                 (<! (<fast-filter-e-fn e))
+                (<! (<fast-filter-e-fn e))
                 (do
                   (swap! *sync-state sync-state--add-queued-local->remote-files e)
                   (let [v (<! (<filter-local-changes-pred e base-path graph-uuid))]
@@ -2688,7 +2726,7 @@
                ^:mutable ratelimit-local-changes-chan
                *txid *txid-for-get-deletion-log
                ^:mutable state ^:mutable remote-change-chan ^:mutable *ws *stopped? *paused?
-               ^:mutable ops-chan
+               ^:mutable ops-chan ^:mutable app-awake-from-sleep-chan
                ;; control chans
                private-full-sync-chan private-remote->local-sync-chan
                private-remote->local-full-sync-chan private-pause-resume-chan]
@@ -2720,6 +2758,7 @@
 
   (start [this]
     (set! ops-chan (chan (async/dropping-buffer 10)))
+    (set! app-awake-from-sleep-chan (chan (async/sliding-buffer 1)))
     (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))
@@ -2728,23 +2767,26 @@
     (async/tap remote->local-sync-mult private-remote->local-sync-chan)
     (async/tap remote->local-full-sync-mult private-remote->local-full-sync-chan)
     (async/tap pause-resume-mult private-pause-resume-chan)
+    (async/tap pubsub/app-wake-up-from-sleep-mult app-awake-from-sleep-chan)
     (go-loop []
       (let [{:keys [remote->local remote->local-full-sync local->remote-full-sync local->remote resume pause stop]}
             (async/alt!
-             private-remote->local-full-sync-chan {:remote->local-full-sync true}
-             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})
-             ratelimit-local-changes-chan ([v]
-                                           (if (nil? v)
-                                             {:stop true}
-                                             (let [rest-v (util/drain-chan ratelimit-local-changes-chan)
-                                                   vs     (cons v rest-v)]
-                                               (println "local changes:" vs)
-                                               {:local->remote vs})))
-             (timeout (* 20 60 1000)) {:local->remote-full-sync true}
-             :priority true)]
+              private-remote->local-full-sync-chan {:remote->local-full-sync true}
+              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})
+              ratelimit-local-changes-chan ([v]
+                                            (if (nil? v)
+                                              {:stop true}
+                                              (let [rest-v (util/drain-chan ratelimit-local-changes-chan)
+                                                    vs     (cons v rest-v)]
+                                                (println "local changes:" vs)
+                                                {:local->remote vs})))
+              app-awake-from-sleep-chan {:remote->local true}
+              (timeout (* 20 60 1000)) {:local->remote-full-sync true}
+              (timeout (* 10 60 1000)) {:remote->local true}
+              :priority true)]
         (cond
           stop
           nil
@@ -3033,6 +3075,7 @@
         (async/untap remote->local-sync-mult private-remote->local-sync-chan)
         (async/untap remote->local-full-sync-mult private-remote->local-full-sync-chan)
         (async/untap pause-resume-mult private-pause-resume-chan)
+        (async/untap pubsub/app-wake-up-from-sleep-mult app-awake-from-sleep-chan)
         (when ops-chan (async/close! ops-chan))
         (stop-local->remote! local->remote-syncer)
         (stop-remote->local! remote->local-syncer)
@@ -3066,7 +3109,7 @@
     (.set-local->remote-syncer! remote->local-syncer local->remote-syncer)
     (swap! *sync-state sync-state--update-current-syncing-graph-uuid graph-uuid)
     (->SyncManager user-uuid graph-uuid base-path *sync-state local->remote-syncer remote->local-syncer remoteapi-with-stop
-                   nil *txid *txid-for-get-deletion-log nil nil nil *stopped? *paused? nil (chan 1) (chan 1) (chan 1) (chan 1))))
+                   nil *txid *txid-for-get-deletion-log nil nil nil *stopped? *paused? nil nil (chan 1) (chan 1) (chan 1) (chan 1))))
 
 (defn sync-manager-singleton
   [user-uuid graph-uuid base-path repo txid *sync-state]
@@ -3296,5 +3339,4 @@
 (comment
  (def *x (atom nil))
  (add-tap (fn [v] (reset! *x v)))
-
  )

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

@@ -228,6 +228,7 @@
 
   (db/run-batch-txs!)
   (file/<ratelimit-file-writes!)
+  (util/<app-wake-up-from-sleep-loop (atom false))
 
   (when config/dev?
     (enable-datalog-console))

+ 4 - 0
src/main/frontend/handler/config.cljs

@@ -44,6 +44,10 @@
   (let [logical-outdenting? (state/logical-outdenting?)]
     (set-config! :editor/logical-outdenting? (not logical-outdenting?))))
 
+(defn toggle-show-full-blocks! []
+  (let [show-full-blocks? (state/show-full-blocks?)]
+    (set-config! :ui/show-full-blocks? (not show-full-blocks?))))
+
 (defn toggle-ui-enable-tooltip! []
   (let [enable-tooltip? (state/enable-tooltip?)]
     (set-config! :ui/enable-tooltip? (not enable-tooltip?))))

+ 5 - 1
src/main/frontend/handler/editor/keyboards.cljs

@@ -11,7 +11,7 @@
     (mixins/hide-when-esc-or-outside
      state
      :on-hide
-     (fn [_state _e event]
+     (fn [_state e event]
        (cond
          (contains?
           #{:commands :block-commands
@@ -25,6 +25,10 @@
          (= :input (state/get-editor-action))
          nil
 
+         (some-> (.-target e)
+                 (.closest ".ls-keep-editing-when-outside-click"))
+         nil
+
          :else
          (let [{:keys [on-hide value]} (editor-handler/get-state)]
            (when on-hide

+ 16 - 17
src/main/frontend/handler/file_sync.cljs

@@ -145,23 +145,22 @@
                                      (apply path/join base-path))
             version-file-paths (<! (p->c (fs/readdir version-files-dir :path-only? true)))]
         (when-not (instance? ExceptionInfo version-file-paths)
-          (let [version-file-paths (remove #{version-files-dir} version-file-paths)]
-            (when (seq version-file-paths)
-              (->>
-               (mapv
-                (fn [path]
-                  (try
-                    (let [create-time
-                          (-> (path/parse path)
-                              (js->clj :keywordize-keys true)
-                              :name
-                              (#(tf/parse (tf/formatter "yyyy-MM-dd'T'HH_mm_ss.SSSZZ") %)))]
-                      {:create-time create-time :path path :relative-path (string/replace-first path base-path "")})
-                    (catch :default e
-                      (log/error :page-history/parse-format-error e)
-                      nil)))
-                version-file-paths)
-               (remove nil?)))))))))
+          (when (seq version-file-paths)
+            (->>
+             (mapv
+              (fn [path]
+                (try
+                  (let [create-time
+                        (-> (path/parse path)
+                            (js->clj :keywordize-keys true)
+                            :name
+                            (#(tf/parse (tf/formatter "yyyy-MM-dd'T'HH_mm_ss.SSSZZ") %)))]
+                    {:create-time create-time :path path :relative-path (string/replace-first path base-path "")})
+                  (catch :default e
+                    (log/error :page-history/parse-format-error e)
+                    nil)))
+              version-file-paths)
+             (remove nil?))))))))
 
 (defn fetch-page-file-versions [graph-uuid page]
   []

+ 33 - 1
src/main/frontend/handler/page.cljs

@@ -427,6 +427,31 @@
     (doseq [page-id page-ids]
       (outliner-file/sync-to-file page-id))))
 
+(defn- rename-update-namespace!
+  "update :block/namespace of the renamed block"
+  [page old-original-name new-name]
+  (let [old-namespace? (text/namespace-page? old-original-name)
+        new-namespace? (text/namespace-page? new-name)
+        update-namespace! (fn [] (let [namespace (first (gp-util/split-last "/" new-name))]
+                                   (when namespace
+                                     (create! namespace {:redirect? false}) ;; create parent page if not exist, creation of namespace ref is handled in `create!`
+                                     (let [namespace-block (db/pull [:block/name (gp-util/page-name-sanity-lc namespace)])
+                                           repo                (state/get-current-repo)
+                                           page-txs [{:db/id (:db/id page)
+                                                      :block/namespace (:db/id namespace-block)}]]
+                                       (d/transact! (db/get-db repo false) page-txs)))))
+        remove-namespace! (fn []
+                            (db/transact! [[:db/retract (:db/id page) :block/namespace]]))]
+
+    (when old-namespace?
+      (if new-namespace?
+        (update-namespace!)
+        (remove-namespace!)))
+
+    (when-not old-namespace?
+      (when new-namespace?
+        (update-namespace!)))))
+
 (defn- rename-page-aux
   "Only accepts unsanitized page names"
   [old-name new-name redirect?]
@@ -474,6 +499,8 @@
 
         (rename-update-refs! page old-original-name new-name)
 
+        (rename-update-namespace! page old-original-name new-name)
+
         (outliner-file/sync-to-file page))
 
       ;; Redirect to the newly renamed page
@@ -580,7 +607,12 @@
 
       (rename-update-refs! from-page
                            (util/get-page-original-name from-page)
-                           (util/get-page-original-name to-page)))
+                           (util/get-page-original-name to-page))
+
+      (rename-update-namespace! from-page
+                                (util/get-page-original-name from-page)
+                                (util/get-page-original-name to-page)))
+
 
     (delete! from-page-name nil)
 

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

@@ -24,7 +24,6 @@
             [promesa.core :as p]
             [shadow.resource :as rc]
             [frontend.db.persist :as db-persist]
-            [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser :as graph-parser]
             [logseq.graph-parser.config :as gp-config]
             [electron.ipc :as ipc]
@@ -302,7 +301,7 @@
                              [])
               add-or-modify-files (some->>
                                    (concat modify-files add-files)
-                                   (gp-util/remove-nils))
+                                   (remove nil?))
               options {:delete-files (concat delete-files delete-pages)
                        :delete-blocks delete-blocks
                        :re-render? true}]

+ 1 - 0
src/main/frontend/handler/search.cljs

@@ -27,6 +27,7 @@
   (text/remove-level-spaces content format (config/get-block-pattern format)))
 
 (defn search
+  "The aggretation of search results"
   ([q]
    (search (state/get-current-repo) q))
   ([repo q]

+ 11 - 7
src/main/frontend/modules/file/core.cljs

@@ -136,7 +136,7 @@
 (defn- remove-transit-ids [block] (dissoc block :db/id :block/file))
 
 (defn save-tree-aux!
-  [page-block tree]
+  [page-block tree blocks-just-deleted?]
   (let [page-block (db/pull (:db/id page-block))
         file-db-id (-> page-block :block/file :db/id)
         file-path (-> (db-utils/entity file-db-id) :file/path)]
@@ -146,17 +146,21 @@
                            (up/ugly-pr-str {:blocks tree
                                             :pages (list (remove-transit-ids page-block))})
                            (string/triml))
-                          (tree->file-content tree {:init-level init-level}))
-            files [[file-path new-content]]
-            repo (state/get-current-repo)]
-        (file-handler/alter-files-handler! repo files {} {}))
+                          (tree->file-content tree {:init-level init-level}))]
+        (if (and (string/blank? new-content)
+                 (not blocks-just-deleted?))
+          (state/pub-event! [:capture-error {:error (js/Error. "Empty content")
+                                             :payload {:file-path file-path}}])
+          (let [files [[file-path new-content]]
+                repo (state/get-current-repo)]
+            (file-handler/alter-files-handler! repo files {} {}))))
       ;; In e2e tests, "card" page in db has no :file/path
       (js/console.error "File path from page-block is not valid" page-block tree))))
 
 (defn save-tree!
-  [page-block tree]
+  [page-block tree blocks-just-deleted?]
   {:pre [(map? page-block)]}
-  (let [ok-handler #(save-tree-aux! page-block tree)
+  (let [ok-handler #(save-tree-aux! page-block tree blocks-just-deleted?)
         file (or (:block/file page-block)
                  (when-let [page (:db/id (:block/page page-block))]
                    (:block/file (db-utils/entity page))))]

+ 34 - 29
src/main/frontend/modules/outliner/file.cljs

@@ -44,34 +44,37 @@
 
 
 (defn do-write-file!
-  [repo page-db-id]
+  [repo page-db-id outliner-op]
   (let [page-block (db/pull repo '[*] page-db-id)
         page-db-id (:db/id page-block)
         whiteboard? (= "whiteboard" (:block/type page-block))
-        blocks-count (model/get-page-blocks-count repo page-db-id)]
-    (if (or (and (> blocks-count 500)
-                 (not (state/input-idle? repo {:diff 3000}))) ;; long page
-            ;; when this whiteboard page is just being updated
-            (and whiteboard? (not (state/whiteboard-page-idle? repo page-block))))
-      (async/put! (state/get-file-write-chan) [repo page-db-id])
-      (let [pull-keys (if whiteboard? whiteboard-blocks-pull-keys-with-persisted-ids '[*])
-            blocks (model/get-page-blocks-no-cache repo (:block/name page-block) {:pull-keys pull-keys})
-            blocks (if whiteboard? (map cleanup-whiteboard-block blocks) blocks)]
-        (when-not (and (= 1 (count blocks))
-                       (string/blank? (:block/content (first blocks)))
-                       (nil? (:block/file page-block)))
-          (let [tree-or-blocks (if whiteboard? blocks
-                                   (tree/blocks->vec-tree repo blocks (:block/name page-block)))]
-            (if page-block
-              (file/save-tree! page-block tree-or-blocks)
-              (js/console.error (str "can't find page id: " page-db-id)))))))))
+        blocks-count (model/get-page-blocks-count repo page-db-id)
+        blocks-just-deleted? (and (zero? blocks-count)
+                                  (contains? #{:delete-blocks :move-blocks} outliner-op))]
+    (when (or (>= blocks-count 1) blocks-just-deleted?)
+      (if (or (and (> blocks-count 500)
+                   (not (state/input-idle? repo {:diff 3000}))) ;; long page
+              ;; when this whiteboard page is just being updated
+              (and whiteboard? (not (state/whiteboard-page-idle? repo page-block))))
+        (async/put! (state/get-file-write-chan) [repo page-db-id outliner-op])
+        (let [pull-keys (if whiteboard? whiteboard-blocks-pull-keys-with-persisted-ids '[*])
+              blocks (model/get-page-blocks-no-cache repo (:block/name page-block) {:pull-keys pull-keys})
+              blocks (if whiteboard? (map cleanup-whiteboard-block blocks) blocks)]
+          (when-not (and (= 1 (count blocks))
+                         (string/blank? (:block/content (first blocks)))
+                         (nil? (:block/file page-block)))
+            (let [tree-or-blocks (if whiteboard? blocks
+                                     (tree/blocks->vec-tree repo blocks (:block/name page-block)))]
+              (if page-block
+                (file/save-tree! page-block tree-or-blocks blocks-just-deleted?)
+                (js/console.error (str "can't find page id: " page-db-id))))))))))
 
 (defn write-files!
   [pages]
   (when (seq pages)
     (when-not config/publishing?
-      (doseq [[repo page-id] (set pages)]
-        (try (do-write-file! repo page-id)
+      (doseq [[repo page-id outliner-op] (set pages)]
+        (try (do-write-file! repo page-id outliner-op)
              (catch :default e
                (notification/show!
                 [:div
@@ -81,15 +84,17 @@
                (log/error :file/write-file-error {:error e})))))))
 
 (defn sync-to-file
-  [{page-db-id :db/id}]
-  (if (nil? page-db-id)
-    (notification/show!
-     "Write file failed, can't find the current page!"
-     :error)
-    (when-let [repo (state/get-current-repo)]
-      (if (:graph/importing @state/state) ; write immediately
-        (write-files! [[repo page-db-id]])
-        (async/put! (state/get-file-write-chan) [repo page-db-id (tc/to-long (t/now))])))))
+  ([page]
+   (sync-to-file page nil))
+  ([{page-db-id :db/id} outliner-op]
+   (if (nil? page-db-id)
+     (notification/show!
+      "Write file failed, can't find the current page!"
+      :error)
+     (when-let [repo (state/get-current-repo)]
+       (if (:graph/importing @state/state) ; write immediately
+         (write-files! [[repo page-db-id outliner-op]])
+         (async/put! (state/get-file-write-chan) [repo page-db-id outliner-op (tc/to-long (t/now))]))))))
 
 (def *writes-finished? (atom {}))
 

+ 1 - 1
src/main/frontend/modules/outliner/pipeline.cljs

@@ -11,7 +11,7 @@
 (defn updated-page-hook
   [tx-report page]
   (when-not (get-in tx-report [:tx-meta :created-from-journal-template?])
-    (file/sync-to-file page)))
+    (file/sync-to-file page (:outliner-op (:tx-meta tx-report)))))
 
 ;; TODO: it'll be great if we can calculate the :block/path-refs before any
 ;; outliner transaction, this way we can group together the real outliner tx

+ 21 - 3
src/main/frontend/modules/shortcut/dicts.cljc

@@ -565,6 +565,8 @@
              :shortcut.category/navigating  "Navigation"
              :shortcut.category/others  "Autres"
              :shortcut.category/toggle  "Basculer"
+             :command.editor/select-parent "Sélectionnez le bloc parent"
+             :command.sidebar/close-top "Ferme l'élément supérieur dans la barre latérale droite"
              }
 
    :af      {:shortcut.category/formatting           "Formatering"
@@ -1020,7 +1022,14 @@
              :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"}
+             :command.graph/re-index                  "Reindexar o grafo atual"
+             :command.editor/new-whiteboard           "Novo quadro branco"
+             :command.editor/select-parent            "Selecione o bloco pai"
+             :command.go/whiteboards                  "Ir para os quadros brancos"
+             :command.graph/export-as-html            "Exportar páginas de gráficos públicos como html"
+             :command.pdf/find                        "PDF: Pesquisar no documento PDF atual"
+             :command.sidebar/close-top               "Fechar item superior na barra lateral direita"
+             :command.ui/install-plugins-from-file    "Instalar plugins de plugins.edn"}
 
    :pt-BR   {:shortcut.category/formatting            "Formatação"
              :shortcut.category/basics                "Básico"
@@ -1144,7 +1153,15 @@
              :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"}
+             :command.graph/re-index                  "Reindexar o grafo atual"
+             
+             :command.editor/new-whiteboard           "Novo quadro branco"
+             :command.editor/select-parent            "Selecione o bloco pai"
+             :command.go/whiteboards                  "Ir para os quadros brancos"
+             :command.graph/export-as-html            "Exportar páginas de gráficos públicos como html"
+             :command.pdf/find                        "PDF: Pesquisar no documento PDF atual"
+             :command.sidebar/close-top               "Fechar item superior na barra lateral direita"
+             :command.ui/install-plugins-from-file    "Instalar plugins de plugins.edn"}
 
    :ja      {:shortcut.category/formatting            "フォーマット"
              :shortcut.category/basics                "基本操作"
@@ -1522,7 +1539,8 @@
              :command.ui/install-plugins-from-file    "Eklentileri plugins.edn dosyasından yükleyin"
              :command.editor/toggle-open-blocks       "Açık blokları kapat/aç (tüm blokları daralt veya genişlet)"
              :command.ui/toggle-cards                 "Kartları aç/kapat"
-             :command.git/commit                      "Git commit mesajı"}
+             :command.git/commit                      "Git commit mesajı"
+             :command.editor/select-parent            "Ebeveyn bloğunu seçin"}
 
    :ko      {:shortcut.category/formatting             "포맷"
              :shortcut.category/basics                "기본 동작"

+ 3 - 0
src/main/frontend/publishing.cljs

@@ -9,6 +9,7 @@
             [frontend.page :as page]
             [frontend.util :as util]
             [frontend.routes :as routes]
+            [frontend.context.i18n :as i18n]
             [reitit.frontend :as rf]
             [reitit.frontend.easy :as rfe]
             [cljs.reader :as reader]
@@ -79,6 +80,8 @@
   ;; this is called in the index.html and must be exported
   ;; so it is available even in :advanced release builds
   (register-components-fns!)
+  ;; Set :preferred-lang as some components depend on it
+  (i18n/start)
   (restore-from-transit-str!)
   (restore-state!)
   (shortcut/refresh!)

+ 75 - 0
src/main/frontend/pubsub.cljc

@@ -0,0 +1,75 @@
+(ns frontend.pubsub
+  "All mults and pubs are collected to this ns.
+  vars with suffix '-mult' is a/Mult, use a/tap and a/untap on them. used by event subscribers
+  vars with suffix '-pub' is a/Pub, use a/sub and a/unsub on them. used by event subscribers
+  vars with suffix '-ch' is chan used by event publishers."
+  {:clj-kondo/config {:linters {:unresolved-symbol {:level :off}}}}
+  #?(:cljs (:require-macros [frontend.pubsub :refer [def-mult-or-pub chan-of]]))
+  (:require [clojure.core.async :as a :refer [chan mult pub]]
+            [clojure.core.async.impl.protocols :as ap]
+            [malli.core :as m]
+            [malli.dev.pretty :as mdp]
+            [clojure.pprint :as pp]))
+
+;;; helper macro
+(defmacro chan-of [malli-schema malli-schema-validator & chan-args]
+  `(let [ch# (chan ~@chan-args)]
+     (reify
+       ap/ReadPort
+       (~'take! [~'_ fn1-handler#]
+        (ap/take! ch# fn1-handler#))
+       ap/WritePort
+       (~'put! [~'_ val# fn1-handler#]
+        (if (~malli-schema-validator val#)
+          (ap/put! ch# val# fn1-handler#)
+          (do (mdp/explain ~malli-schema val#)
+              (throw (ex-info "validate chan value failed" {:val val#}))))))))
+
+(defmacro def-mult-or-pub
+  "define following vars:
+  - `symbol-name`-ch for event publisher.
+  - `symbol-name`-mult or `symbol-name`-pub for event subscribers.
+  - `symbol-name`-validator is malli schema validator
+  def -pub var when `:topic-fn` exists otherwise -mult var"
+  [symbol-name doc-string malli-schema & {:keys [ch-buffer topic-fn]
+                                          :or   {ch-buffer 1}}]
+  (let [schema-validator-name (symbol (str symbol-name "-validator"))
+        schema-name           (symbol (str symbol-name "-schema"))
+        ch-name               (symbol (str symbol-name "-ch"))
+        mult-or-pub-name      (if topic-fn
+                                (symbol (str symbol-name "-pub"))
+                                (symbol (str symbol-name "-mult")))
+        doc-string*           (str doc-string "\nMalli-schema:\n" (with-out-str (pp/pprint malli-schema)))]
+    `(do
+       (def ~schema-name ~malli-schema)
+       (def ~schema-validator-name (m/validator ~malli-schema))
+       (def ~ch-name ~doc-string* (chan-of ~malli-schema ~schema-validator-name ~ch-buffer))
+       ~(if topic-fn
+          `(def ~mult-or-pub-name ~doc-string* (pub ~ch-name ~topic-fn))
+          `(def ~mult-or-pub-name ~doc-string* (mult ~ch-name))))))
+
+;;; all chan, mult, pub defined here...
+
+(def-mult-or-pub app-wake-up-from-sleep
+  "app wake up from sleep event"
+  [:map
+   [:last-activated-at :int]
+   [:now :int]])
+
+(def-mult-or-pub sync-events
+  "file-sync events"
+  [:map
+   [:event [:enum
+            :created-local-version-file
+            :finished-local->remote
+            :finished-remote->local
+            :start
+            :pause
+            :resume
+            :exception-decrypt-failed
+            :remote->local-full-sync-failed
+            :local->remote-full-sync-failed
+            :get-remote-graph-failed
+            :get-deletion-logs-failed]]
+   [:data :map]]
+  :topic-fn :event)

+ 1 - 0
src/main/frontend/schema/handler/common_config.cljc

@@ -38,6 +38,7 @@
     [:block/content-max-length :int]
     [:ui/show-command-doc? :boolean]
     [:ui/show-empty-bullets? :boolean]
+    [:ui/show-full-blocks? :boolean]
     [:query/views [:map-of
                    :keyword
                    [:sequential any?]]]

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

@@ -111,6 +111,9 @@
     (protocol/transact-blocks! engine data)))
 
 (defn- transact-pages!
+  "Transact pages to search engine
+   :pages-to-remove-set the set of pages to remove (not include those to update)
+   :pages-to-add        the page entities to add"
   [repo data]
   (when-let [engine (get-engine repo)]
     (protocol/transact-pages! engine data)))

+ 4 - 0
src/main/frontend/state.cljs

@@ -670,6 +670,10 @@ Similar to re-frame subscriptions"
   []
   (:editor/logical-outdenting? (sub-config)))
 
+(defn show-full-blocks?
+  []
+  (:ui/show-full-blocks? (sub-config)))
+
 (defn preferred-pasting-file?
   []
   (:editor/preferred-pasting-file? (sub-config)))

+ 20 - 1
src/main/frontend/util.cljc

@@ -28,7 +28,8 @@
             [rum.core :as rum]
             [clojure.core.async :as async]
             [cljs.core.async.impl.channels :refer [ManyToManyChannel]]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.pubsub :as pubsub]))
   (:require
    [clojure.pprint]
    [clojure.string :as string]
@@ -1461,3 +1462,21 @@
           (reset! last-mem ret)
           ret)
         @last-mem))))
+
+#?(:cljs
+   (do
+     (defn <app-wake-up-from-sleep-loop
+       "start a async/go-loop to check the app awake from sleep.
+Use (async/tap `pubsub/app-wake-up-from-sleep-mult`) to receive messages.
+Arg *stop: atom, reset to true to stop the loop"
+       [*stop]
+       (let [*last-activated-at (volatile! (tc/to-epoch (t/now)))]
+         (async/go-loop []
+           (if @*stop
+             (println :<app-wake-up-from-sleep-loop :stop)
+             (let [now-epoch (tc/to-epoch (t/now))]
+               (when (< @*last-activated-at (- now-epoch 10))
+                 (async/>! pubsub/app-wake-up-from-sleep-ch {:last-activated-at @*last-activated-at :now now-epoch}))
+               (vreset! *last-activated-at now-epoch)
+               (async/<! (async/timeout 5000))
+               (recur))))))))

+ 12 - 0
src/test/frontend/db/query_custom_test.cljs

@@ -51,6 +51,18 @@
                                            (not [?b :block/marker _])]]]})))
         "advanced query that uses rule from logseq and rule from :inputs")
 
+    (is (= ["LATER b3"]
+           (map :block/content
+                (custom-query {:query '[:find (pull ?b [*])
+                                        :in $ %
+                                        :where
+                                        (starts-with ?b "LA")
+                                        (task ?b #{"LATER"})]
+                               :rules '[[(starts-with ?b ?substr)
+                                         [?b :block/content ?content]
+                                         [(clojure.string/starts-with? ?content ?substr)]]]})))
+        "advanced query that uses :rules and rules from logseq")
+
     (is (= #{"page1"}
            (set
             (map #(get-in % [:block/page :block/name])

Plik diff jest za duży
+ 283 - 346
static/yarn.lock


+ 3 - 0
templates/config.edn

@@ -27,6 +27,9 @@
  ;; Show brackets around page references
  ;; :ui/show-brackets? true
 
+ ;; Enable showing the body of blocks when referencing them.
+ :ui/show-full-blocks? false
+
  ;; Enable Block timestamp
  :feature/enable-block-timestamps? false
 

+ 25 - 24
yarn.lock

@@ -992,6 +992,13 @@
     "@types/expect" "^1.20.4"
     "@types/node" "*"
 
+"@types/yauzl@^2.9.1":
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
+  integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
+  dependencies:
+    "@types/node" "*"
+
 acorn-node@^1.8.2:
   version "1.8.2"
   resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
@@ -1932,7 +1939,7 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 
-concat-stream@^1.6.0, concat-stream@^1.6.2:
+concat-stream@^1.6.0:
   version "1.6.2"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
   integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -2203,7 +2210,7 @@ dayjs@^1.10.0:
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93"
   integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==
 
-debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
+debug@^2.2.0, debug@^2.3.3:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -2509,14 +2516,14 @@ electron-to-chromium@^1.4.251:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
   integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==
 
-electron@19.1.8:
-  version "19.1.8"
-  resolved "https://registry.yarnpkg.com/electron/-/electron-19.1.8.tgz#3ce19c270ca86d05bbf0df5ceeaea2d23edc7083"
-  integrity sha512-UfPQdFjgKI0xCm1V5sV3iAVOs0kCwAE91xWzV5tI7ij14yOkxTdXp9BqTzFaSbQYLYxn6q1BUUe1nlzjJjzAnw==
+electron@20.3.8:
+  version "20.3.8"
+  resolved "https://registry.yarnpkg.com/electron/-/electron-20.3.8.tgz#94a4b2bb89a3e6bbeb824365cfd0f65658bc1a2c"
+  integrity sha512-P93ZWoJjXEVwsjUJ4q6640KZ4+rmWMZ9O5IB1eq/L9S52ZYsXo0o82afpPXq3j8vpneY0N5J62hgRzC1ZKzYmw==
   dependencies:
     "@electron/get" "^1.14.1"
     "@types/node" "^16.11.26"
-    extract-zip "^1.0.3"
+    extract-zip "^2.0.1"
 
 element-resize-detector@^1.1.14:
   version "1.2.4"
@@ -2802,15 +2809,16 @@ extglob@^2.0.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
-extract-zip@^1.0.3:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
-  integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
+extract-zip@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
+  integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
   dependencies:
-    concat-stream "^1.6.2"
-    debug "^2.6.9"
-    mkdirp "^0.5.4"
+    debug "^4.1.1"
+    get-stream "^5.1.0"
     yauzl "^2.10.0"
+  optionalDependencies:
+    "@types/yauzl" "^2.9.1"
 
 fancy-log@^1.3.2:
   version "1.3.3"
@@ -4642,13 +4650,6 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
-mkdirp@^0.5.4:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
-  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
-  dependencies:
-    minimist "^1.2.6"
-
 mkdirp@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
@@ -4682,9 +4683,9 @@ mute-stdout@^1.0.0:
   integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==
 
 nan@^2.12.1:
-  version "2.16.0"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
-  integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
+  version "2.17.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
+  integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
 
 nanoid@^3.3.4:
   version "3.3.4"

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików