Browse Source

Merge branch 'master' into enhance/mobile

llcc 4 years ago
parent
commit
e03257ef5f
57 changed files with 919 additions and 727 deletions
  1. 4 3
      .github/workflows/build-desktop-release.yml
  2. 1 0
      android/app/src/main/AndroidManifest.xml
  3. 12 1
      android/app/src/main/java/com/logseq/app/FolderPicker.java
  4. 4 4
      e2e-tests/basic.spec.ts
  5. 4 4
      e2e-tests/utils.ts
  6. 1 1
      libs/package.json
  7. 2 2
      libs/src/LSPlugin.core.ts
  8. 64 1
      libs/src/LSPlugin.ts
  9. 2 2
      libs/src/LSPlugin.user.ts
  10. 34 26
      libs/webpack.config.core.js
  11. 1 1
      public/index.html
  12. 3 0
      resources/css/common.css
  13. 1 0
      resources/forge.config.js
  14. 0 0
      resources/js/lsplugin.core.js
  15. 1 1
      resources/package.json
  16. 1 1
      src/electron/electron/core.cljs
  17. 22 20
      src/main/frontend/components/block.cljs
  18. 98 89
      src/main/frontend/components/editor.cljs
  19. 2 2
      src/main/frontend/components/editor.css
  20. 17 10
      src/main/frontend/components/encryption.cljs
  21. 8 4
      src/main/frontend/components/header.cljs
  22. 23 1
      src/main/frontend/components/header.css
  23. 6 3
      src/main/frontend/components/query_table.cljs
  24. 68 56
      src/main/frontend/components/repo.cljs
  25. 68 99
      src/main/frontend/components/sidebar.cljs
  26. 101 83
      src/main/frontend/components/sidebar.css
  27. 0 6
      src/main/frontend/components/theme.cljs
  28. 1 1
      src/main/frontend/components/widgets.cljs
  29. 1 4
      src/main/frontend/db/model.cljs
  30. 1 10
      src/main/frontend/db_schema.cljs
  31. 4 2
      src/main/frontend/dicts.cljs
  32. 1 1
      src/main/frontend/extensions/pdf/pdf.css
  33. 38 28
      src/main/frontend/extensions/srs.cljs
  34. 20 15
      src/main/frontend/format/block.cljs
  35. 2 2
      src/main/frontend/handler.cljs
  36. 1 1
      src/main/frontend/handler/block.cljs
  37. 2 8
      src/main/frontend/handler/command_palette.cljs
  38. 117 101
      src/main/frontend/handler/editor.cljs
  39. 0 2
      src/main/frontend/handler/export.cljs
  40. 9 5
      src/main/frontend/handler/extract.cljs
  41. 16 34
      src/main/frontend/handler/page.cljs
  42. 6 4
      src/main/frontend/modules/file/core.cljs
  43. 1 11
      src/main/frontend/modules/outliner/core.cljs
  44. 20 9
      src/main/frontend/modules/shortcut/config.cljs
  45. 6 6
      src/main/frontend/modules/shortcut/core.cljs
  46. 5 6
      src/main/frontend/modules/shortcut/data_helper.cljs
  47. 3 1
      src/main/frontend/page.cljs
  48. 10 0
      src/main/frontend/state.cljs
  49. 4 2
      src/main/frontend/tools/html_export.cljs
  50. 44 42
      src/main/frontend/ui.cljs
  51. 7 2
      src/main/frontend/util.cljc
  52. 1 1
      src/main/frontend/util/drawer.cljs
  53. 0 2
      src/main/frontend/util/page_property.cljs
  54. 1 1
      src/main/frontend/version.cljs
  55. 48 0
      src/main/logseq/api.cljs
  56. 1 1
      src/test/frontend/handler/block_test.cljs
  57. 1 5
      src/test/frontend/modules/outliner/ds_test.cljs

+ 4 - 3
.github/workflows/build-desktop-release.yml

@@ -215,7 +215,7 @@ jobs:
 
   build-macos:
     needs: [ compile-cljs ]
-    runs-on: macos-latest
+    runs-on: macos-11
 
     steps:
       - name: Download The Static Asset
@@ -269,6 +269,7 @@ jobs:
         env:
           APPLE_ID: ${{ secrets.APPLE_ID_EMAIL }}
           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
+          APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
 
       - name: Save x64 artifacts
         run: |
@@ -277,11 +278,12 @@ jobs:
           mv static/out/make/zip/darwin/x64/*.zip ./builds/Logseq-darwin-x64-${{ steps.ref.outputs.version }}.zip
 
       - name: Build/Release Electron App for arm64
-        run: yarn install && yarn electron:make-macos-arm64
+        run: yarn electron:make-macos-arm64
         working-directory: ./static
         env:
           APPLE_ID: ${{ secrets.APPLE_ID_EMAIL }}
           APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
+          APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
 
       - name: Save arm64 artifacts
         run: |
@@ -351,4 +353,3 @@ jobs:
             ./*.dmg
             ./*.exe
             ./*.AppImage
-

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

@@ -8,6 +8,7 @@
         android:label="@string/app_name"
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
+        android:requestLegacyExternalStorage="true"
         android:theme="@style/AppTheme">
 
         <activity

+ 12 - 1
android/app/src/main/java/com/logseq/app/FolderPicker.java

@@ -5,6 +5,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.Build;
 import android.provider.DocumentsContract;
 import android.provider.Settings;
 
@@ -21,9 +22,19 @@ import com.getcapacitor.PluginMethod;
 
 @CapacitorPlugin(name = "FolderPicker")
 public class FolderPicker extends Plugin {
+    public static boolean FileAccessAllowed()
+        {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+                return true;
+            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                return (Environment.isExternalStorageManager());
+            }
+            return false;
+        }
+
     @PluginMethod()
     public void pickFolder(PluginCall call) {
-        if (Environment.isExternalStorageManager()) {
+        if (FileAccessAllowed()) {
             Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
             i.addCategory(Intent.CATEGORY_DEFAULT);
             startActivityForResult(call, i, "folderPickerResult");

+ 4 - 4
e2e-tests/basic.spec.ts

@@ -15,8 +15,8 @@ test('render app', async ({ page }) => {
 test('open sidebar', async ({ page }) => {
   await openSidebar(page)
 
-  await page.waitForSelector('#sidebar-nav-wrapper a:has-text("New page")', { state: 'visible' })
-  await page.waitForSelector('#sidebar-nav-wrapper >> text=Journals', { state: 'visible' })
+  await page.waitForSelector('#left-sidebar a:has-text("New page")', { state: 'visible' })
+  await page.waitForSelector('#left-sidebar >> text=Journals', { state: 'visible' })
 })
 
 test('search', async ({ page }) => {
@@ -235,12 +235,12 @@ test('auto completion and auto pair', async ({ page }) => {
 
 // FIXME: Electron with filechooser is not working
 test.skip('open directory', async ({ page }) => {
-  await page.click('#sidebar-nav-wrapper >> text=Journals')
+  await page.click('#left-sidebar >> text=Journals')
   await page.waitForSelector('h1:has-text("Open a local directory")')
   await page.click('h1:has-text("Open a local directory")')
 
   // await page.waitForEvent('filechooser')
   await page.keyboard.press('Escape')
 
-  await page.click('#sidebar-nav-wrapper >> text=Journals')
+  await page.click('#left-sidebar >> text=Journals')
 })

+ 4 - 4
e2e-tests/utils.ts

@@ -23,18 +23,18 @@ export async function appFirstLoaded(page: Page) {
 }
 
 export async function openSidebar(page: Page) {
-    let sidebarVisible = await page.isVisible('#sidebar-nav-wrapper .left-sidebar-inner')
+    let sidebarVisible = await page.isVisible('#left-sidebar .left-sidebar-inner')
     if (!sidebarVisible) {
         await page.click('#left-menu.button')
     }
-    await page.waitForSelector('#sidebar-nav-wrapper .left-sidebar-inner', { state: 'visible' })
+    await page.waitForSelector('#left-sidebar .left-sidebar-inner', { state: 'visible' })
 }
 
 export async function createRandomPage(page: Page) {
     const randomTitle = randomString(20)
 
-    // Click #sidebar-nav-wrapper a:has-text("New page")
-    await page.click('#sidebar-nav-wrapper a:has-text("New page")')
+    // Click #left-sidebar a:has-text("New page")
+    await page.click('#search-button')
     // Fill [placeholder="Search or create page"]
     await page.fill('[placeholder="Search or create page"]', randomTitle)
     // Click text=/.*New page: "new page".*/

+ 1 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.0.1-alpha.32",
+  "version": "0.0.1-alpha.33",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",

+ 2 - 2
libs/src/LSPlugin.core.ts

@@ -173,13 +173,13 @@ function initUserSettingsHandlers (pluginLocal: PluginLocal) {
 function initMainUIHandlers (pluginLocal: PluginLocal) {
   const _ = (label: string): any => `main-ui:${label}`
 
-  pluginLocal.on(_('visible'), ({ visible, toggle, cursor }) => {
+  pluginLocal.on(_('visible'), ({ visible, toggle, cursor, autoFocus }) => {
     const el = pluginLocal.getMainUIContainer()
     el?.classList[toggle ? 'toggle' : (visible ? 'add' : 'remove')]('visible')
     // pluginLocal.caller!.callUserModel(LSPMSG, { type: _('visible'), payload: visible })
     // auto focus frame
     if (visible) {
-      if (!pluginLocal.shadow && el) {
+      if (!pluginLocal.shadow && el && (autoFocus !== false)) {
         (el.querySelector('iframe') as HTMLIFrameElement)?.contentWindow?.focus()
       }
     }

+ 64 - 1
libs/src/LSPlugin.ts

@@ -185,6 +185,44 @@ export type SimpleCommandKeybinding = {
   mac?: string // special for Mac OS
 }
 
+export type ExternalCommandType =
+  'logseq.command/run' |
+  'logseq.editor/cycle-todo' |
+  'logseq.editor/down' |
+  'logseq.editor/up' |
+  'logseq.editor/expand-block-children' |
+  'logseq.editor/collapse-block-children' |
+  'logseq.editor/open-file-in-default-app' |
+  'logseq.editor/open-file-in-directory' |
+  'logseq.editor/select-all-blocks' |
+  'logseq.editor/toggle-open-blocks' |
+  'logseq.editor/zoom-in' |
+  'logseq.editor/zoom-out' |
+  'logseq.go/home' |
+  'logseq.go/journals' |
+  'logseq.go/keyboard-shortcuts' |
+  'logseq.go/next-journal' |
+  'logseq.go/prev-journal' |
+  'logseq.go/search' |
+  'logseq.go/search-in-page' |
+  'logseq.go/tomorrow' |
+  'logseq.search/re-index' |
+  'logseq.sidebar/clear' |
+  'logseq.sidebar/open-today-page' |
+  'logseq.ui/goto-plugins' |
+  'logseq.ui/select-theme-color' |
+  'logseq.ui/toggle-brackets' |
+  'logseq.ui/toggle-cards' |
+  'logseq.ui/toggle-contents' |
+  'logseq.ui/toggle-document-mode' |
+  'logseq.ui/toggle-help' |
+  'logseq.ui/toggle-left-sidebar' |
+  'logseq.ui/toggle-right-sidebar' |
+  'logseq.ui/toggle-settings' |
+  'logseq.ui/toggle-theme' |
+  'logseq.ui/toggle-wide-mode' |
+  'logseq.command-palette/toggle'
+
 /**
  * App level APIs
  */
@@ -213,6 +251,24 @@ export interface IAppProxy {
     },
     action: SimpleCommandCallback) => void
 
+  invokeExternalCommand: (
+    type: ExternalCommandType,
+    ...args: Array<any>) => Promise<void>
+
+  /**
+   * Get state from app store
+   * valid state is here
+   * https://github.com/logseq/logseq/blob/master/src/main/frontend/state.cljs#L27
+   *
+   * @example
+   * ```ts
+   * const isDocMode = await logseq.App.getStateFromStore('document/mode?')
+   * ```
+   * @param path
+   */
+  getStateFromStore:
+    <T = any>(path: string | Array<string>) => Promise<T>
+
   // native
   relaunch: () => Promise<void>
   quit: () => Promise<void>
@@ -335,6 +391,8 @@ export interface IEditorProxy extends Record<string, any> {
 
   getCurrentBlock: () => Promise<BlockEntity | null>
 
+  getSelectedBlocks: () => Promise<Array<BlockEntity> | null>
+
   /**
    * get all blocks of the current page as a tree structure
    *
@@ -380,6 +438,11 @@ export interface IEditorProxy extends Record<string, any> {
     opts?: Partial<{ includeChildren: boolean }>
   ) => Promise<BlockEntity | null>
 
+  setBlockCollapsed: (
+    uuid: BlockUUID,
+    opts?: { flag: boolean | 'toggle' }
+  ) => Promise<void>
+
   getPage: (
     srcPage: PageIdentity | EntityID,
     opts?: Partial<{ includeChildren: boolean }>
@@ -580,7 +643,7 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
   /**
    * show the plugin's UI
    */
-  showMainUI (): void
+  showMainUI (opts?: { autoFocus: boolean }): void
 
   /**
    * hide the plugin's UI

+ 2 - 2
libs/src/LSPlugin.user.ts

@@ -358,8 +358,8 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
     this._ui.set(payload.key, payload)
   }
 
-  showMainUI (): void {
-    const payload = { key: KEY_MAIN_UI, visible: true }
+  showMainUI (opts?: { autoFocus: boolean }): void {
+    const payload = { key: KEY_MAIN_UI, visible: true, autoFocus: opts?.autoFocus }
     this.caller.call('main-ui:visible', payload)
     this.emit('ui:visible:changed', payload)
     this._ui.set(payload.key, payload)

+ 34 - 26
libs/webpack.config.core.js

@@ -2,31 +2,39 @@ const webpack = require('webpack')
 const path = require('path')
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
 
-module.exports = {
-  entry: './src/LSPlugin.core.ts',
-  devtool: 'inline-source-map',
-  module: {
-    rules: [
-      {
-        test: /\.tsx?$/,
-        use: 'ts-loader',
-        exclude: /node_modules/,
-      },
+module.exports = (env, argv) => {
+  const config = {
+    entry: './src/LSPlugin.core.ts',
+    devtool: 'eval',
+    module: {
+      rules: [
+        {
+          test: /\.tsx?$/,
+          use: 'ts-loader',
+          exclude: /node_modules/,
+        },
+      ],
+    },
+    resolve: {
+      extensions: ['.tsx', '.ts', '.js'],
+    },
+    plugins: [
+      new webpack.ProvidePlugin({
+        process: 'process/browser',
+      }),
     ],
-  },
-  resolve: {
-    extensions: ['.tsx', '.ts', '.js'],
-  },
-  plugins: [
-    new webpack.ProvidePlugin({
-      process: 'process/browser',
-    }),
-    // new BundleAnalyzerPlugin()
-  ],
-  output: {
-    library: 'LSPlugin',
-    libraryTarget: 'umd',
-    filename: 'lsplugin.core.js',
-    path: path.resolve(__dirname, '../resources/js'),
-  },
+    output: {
+      library: 'LSPlugin',
+      libraryTarget: 'umd',
+      filename: 'lsplugin.core.js',
+      path: path.resolve(__dirname, '../resources/js'),
+    },
+  }
+
+  if (argv.mode === 'production') {
+    delete config.devtool
+    config.plugins.push(new BundleAnalyzerPlugin())
+  }
+
+  return config
 }

+ 1 - 1
public/index.html

@@ -3,7 +3,7 @@
 <head>
   <meta charset="utf-8">
   <meta content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
-  <link rel="stylesheet" href="https://unpkg.com/@tabler/icons@latest/iconfont/tabler-icons.min.css">
+  <link rel="stylesheet" href="/static/css/tabler-icons.min.css">
   <link href="/static/css/style.css" rel="stylesheet" type="text/css">
   <link href="/static/img/logo.png" rel="shortcut icon" type="image/png">
   <link href="/static/img/logo.png" rel="shortcut icon" sizes="192x192">

+ 3 - 0
resources/css/common.css

@@ -9,7 +9,10 @@
   --ls-scrollbar-width: 6px;
   --ls-border-radius-low: 4px;
   --ls-border-radius-medium: 8px;
+  --ls-headbar-height: 3rem;
+  --ls-headbar-inner-top-padding: 0px;
   --ls-left-sidebar-width: 240px;
+  --ls-left-sidebar-sm-width: 70%;
   --ls-left-sidebar-nav-btn-size: 38px;
 }
 

+ 1 - 0
resources/forge.config.js

@@ -14,6 +14,7 @@ module.exports = {
     osxNotarize: {
       appleId: process.env['APPLE_ID'],
       appleIdPassword: process.env['APPLE_ID_PASSWORD'],
+      ascProvider: process.env['APPLE_ASC_PROVIDER']
     },
   },
   makers: [

File diff suppressed because it is too large
+ 0 - 0
resources/js/lsplugin.core.js


+ 1 - 1
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.5.1",
+  "version": "0.5.2",
   "main": "electron.js",
   "author": "Logseq",
   "description": "A privacy-first, open-source platform for knowledge management and collaboration.",

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

@@ -80,7 +80,7 @@
                 (callback #js {:cancel false
                                :requestHeaders requestHeaders}))))))
     (.loadURL win url)
-    (when dev? (.. win -webContents (openDevTools)))
+    ;;(when dev? (.. win -webContents (openDevTools)))
     win))
 
 (defn setup-updater! [^js win]

+ 22 - 20
src/main/frontend/components/block.cljs

@@ -48,7 +48,7 @@
             [frontend.template :as template]
             [frontend.text :as text]
             [frontend.ui :as ui]
-            [frontend.util :as util]
+            [frontend.util :as util :refer [profile]]
             [frontend.util.clock :as clock]
             [frontend.util.property :as property]
             [frontend.util.drawer :as drawer]
@@ -643,13 +643,10 @@
           hl-type (get-in block [:block/properties :hl-type])
           repo (state/get-current-repo)]
       (if block
-        (let [title (let [title (:block/title block)
-                          block-content (block-content (assoc config :block-ref? true)
-                                                       block nil (:block/uuid block)
-                                                       (:slide? config))
-                          class (if (seq title) "block-ref" "block-ref-no-title")]
-                      [:span {:class class}
-                       block-content])
+        (let [title [:span {:class "block-ref"}
+                     (block-content (assoc config :block-ref? true)
+                                    block nil (:block/uuid block)
+                                    (:slide? config))]
               inner (if label
                       (->elem
                        :span.block-ref
@@ -1369,7 +1366,7 @@
                (rum/with-key (block-container config child)
                  (:block/uuid child)))))]))))
 
-(defn block-content-empty?
+(defn- block-content-empty?
   [{:block/keys [properties title body]}]
   (and
    (or
@@ -1813,8 +1810,10 @@
                      summary]]))))])
 
 (rum/defc block-content < rum/reactive
-  [config {:block/keys [uuid title body content children properties scheduled deadline] :as block} edit-input-id block-id slide?]
-  (let [collapsed? (get properties :collapsed)
+  [config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?]
+  (let [{:block/keys [title body] :as block} (if (:block/title block) block
+                                                 (merge block (block/parse-title-and-body format pre-block? content)))
+        collapsed? (get properties :collapsed)
         block-ref? (:block-ref? config)
         block-ref-with-title? (and block-ref? (seq title))
         block-type (or (:ls-type properties) :default)
@@ -1999,12 +1998,13 @@
                               [:page
                                (or page-original-name page-name)])
             parents-props (doall
-                           (for [{:block/keys [uuid title body name] :as block} parents]
+                           (for [{:block/keys [uuid name content] :as block} parents]
                              (when-not name ; not page
-                               [block
-                                (if (seq title)
-                                  (->elem :span (map-inline config title))
-                                  (->elem :div (markup-elements-cp config body)))])))
+                               (let [{:block/keys [title body]} (block/parse-title-and-body (:block/format block) (:block/pre-block? block) content)]
+                                 [block
+                                 (if (seq title)
+                                   (->elem :span (map-inline config title))
+                                   (->elem :div (markup-elements-cp config body)))]))))
             breadcrumb (->> (into [] parents-props)
                             (concat [page-name-props] (when more? [:more]))
                             (filterv identity)
@@ -2159,8 +2159,10 @@
                              (select-keys (second (:rum/args new-state)) compare-keys))
                        (not= (select-keys (first (:rum/args old-state)) config-compare-keys)
                              (select-keys (first (:rum/args new-state)) config-compare-keys)))))}
-  [state config {:block/keys [uuid body repo children pre-block? top? properties refs heading-level level type] :as block}]
-  (let [blocks-container-id (:blocks-container-id config)
+  [state config {:block/keys [uuid repo children pre-block? top? properties refs heading-level level type format content] :as block}]
+  (let [block (merge block (block/parse-title-and-body format pre-block? content))
+        body (:block/body block)
+        blocks-container-id (:blocks-container-id config)
         config (update config :block merge block)
         ;; Each block might have multiple queries, but we store only the first query's result
         config (if (nil? (:query-result config))
@@ -2197,8 +2199,8 @@
       {:id block-id
        :data-refs data-refs
        :data-refs-self data-refs-self
+       :data-collapsed (and collapsed? has-child?)
        :class (str uuid
-                   (when (and collapsed? has-child?) " collapsed")
                    (when pre-block? " pre-block")
                    (when (and card? (not review-cards?)) " shadow-xl"))
        :blockid (str uuid)
@@ -2384,7 +2386,7 @@
                                clocks))]
         [:div.overflow-x-scroll.sm:overflow-auto
          (->elem
-          :table.m-0 
+          :table.m-0
           {:class "logbook-table"
            :border 0
            :style {:width "max-content"}

+ 98 - 89
src/main/frontend/components/editor.cljs

@@ -9,6 +9,7 @@
             [frontend.components.svg :as svg]
             [frontend.mobile.camera :as mobile-camera]
             [frontend.config :as config]
+            [frontend.handler.notification :as notification]
             [frontend.db :as db]
             [frontend.extensions.zotero :as zotero]
             [frontend.handler.editor :as editor-handler :refer [get-state]]
@@ -218,98 +219,106 @@
 
 (rum/defc mobile-bar < rum/reactive
   [parent-state parent-id]
-  [:div#mobile-editor-toolbar.bg-base-2.fix-ios-fixed-bottom
-   [:div.flex.overflow-scroll
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
+  (let [vw-state (state/sub :ui/visual-viewport-state)
+        vw-pending? (state/sub :ui/visual-viewport-pending?)]
+    [:div#mobile-editor-toolbar.bg-base-2
+     {:style {:bottom (if (and vw-state)
+                        (- (.-clientHeight js/document.documentElement)
+                           (:height vw-state)
+                           (:offset-top vw-state))
+                        0)}
+      :class (util/classnames [{:is-vw-pending (boolean vw-pending?)}])}
+     [:div.flex.overflow-scroll
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
                         (util/stop e)
                         (mobile-camera/embed-photo parent-id))}
-      (ui/icon "camera"
+        (ui/icon "camera"
                {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (editor-handler/indent-outdent true))}
-      (ui/icon "arrow-bar-right"
-               {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (editor-handler/indent-outdent false))}
-      (ui/icon "arrow-bar-left"
-               {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (editor-handler/move-up-down true))}
-      (ui/icon "arrow-bar-to-up"
-               {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (editor-handler/move-up-down false))}
-      (ui/icon "arrow-bar-to-down"
-               {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (commands/simple-insert! parent-id "\n"
-                                                 {:forward-pos 1})
-                        ;; TODO: should we add this focus step to `simple-insert!`?
-                        (when-let [input (gdom/getElement parent-id)]
-                          (.focus input)))}
-      (ui/icon "arrow-back"
-               {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (editor-handler/cycle-todo!))}
-      (ui/icon "checkbox"
-               {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (commands/simple-insert!
-                         parent-id "[[]]"
-                         {:backward-pos 2
-                          :check-fn     (fn [_ _ new-pos]
-                                          (reset! commands/*slash-caret-pos new-pos)
-                                          (commands/handle-step [:editor/search-page]))})
-                        (when-let [input (gdom/getElement parent-id)]
-                          (.focus input)))}
-      (ui/icon "brackets"
-               {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (commands/simple-insert!
-                         parent-id "(())"
-                         {:backward-pos 2
-                          :check-fn     (fn [_ _ new-pos]
-                                          (reset! commands/*slash-caret-pos new-pos)
-                                          (commands/handle-step [:editor/search-block]))})
-                        (when-let [input (gdom/getElement parent-id)]
-                          (.focus input)))}
-      (ui/icon "parentheses"
-               {:style {:fontSize ui/icon-size}})]]
-    [:div
-     [:button.bottom-action
-      {:on-mouse-down (fn [e]
-                        (util/stop e)
-                        (commands/simple-insert! parent-id "/" {})
-                        (when-let [input (gdom/getElement parent-id)]
-                          (.focus input)))}
-      (ui/icon "command"
-               {:style {:fontSize ui/icon-size}})]]]])
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          (editor-handler/indent-outdent true))}
+        (ui/icon "arrow-bar-right"
+                 {:style {:fontSize ui/icon-size}})]]
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          (editor-handler/indent-outdent false))}
+        (ui/icon "arrow-bar-left"
+                 {:style {:fontSize ui/icon-size}})]]
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          ((editor-handler/move-up-down true)))}
+        (ui/icon "arrow-bar-to-up"
+                 {:style {:fontSize ui/icon-size}})]]
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          ((editor-handler/move-up-down false)))}
+        (ui/icon "arrow-bar-to-down"
+                 {:style {:fontSize ui/icon-size}})]]
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          (commands/simple-insert! parent-id "\n"
+                                                   {:forward-pos 1})
+                          ;; TODO: should we add this focus step to `simple-insert!`?
+                          (when-let [input (gdom/getElement parent-id)]
+                            (.focus input)))}
+        (ui/icon "arrow-back"
+                 {:style {:fontSize ui/icon-size}})]]
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          (editor-handler/cycle-todo!))}
+        (ui/icon "checkbox"
+                 {:style {:fontSize ui/icon-size}})]]
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          (commands/simple-insert!
+                            parent-id "[[]]"
+                            {:backward-pos 2
+                             :check-fn     (fn [_ _ new-pos]
+                                             (reset! commands/*slash-caret-pos new-pos)
+                                             (commands/handle-step [:editor/search-page]))})
+                          (when-let [input (gdom/getElement parent-id)]
+                            (.focus input)))}
+        (ui/icon "brackets"
+                 {:style {:fontSize ui/icon-size}})]]
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          (commands/simple-insert!
+                            parent-id "(())"
+                            {:backward-pos 2
+                             :check-fn     (fn [_ _ new-pos]
+                                             (reset! commands/*slash-caret-pos new-pos)
+                                             (commands/handle-step [:editor/search-block]))})
+                          (when-let [input (gdom/getElement parent-id)]
+                            (.focus input)))}
+        (ui/icon "parentheses"
+                 {:style {:fontSize ui/icon-size}})]]
+      [:div
+       [:button.bottom-action
+        {:on-mouse-down (fn [e]
+                          (util/stop e)
+                          (commands/simple-insert! parent-id "/" {})
+                          (when-let [input (gdom/getElement parent-id)]
+                            (.focus input)))}
+        (ui/icon "command"
+                 {:style {:fontSize ui/icon-size}})]]]]))
 
 (rum/defcs input < rum/reactive
   (rum/local {} ::input-value)

+ 2 - 2
src/main/frontend/components/editor.css

@@ -1,15 +1,15 @@
 #mobile-editor-toolbar {
   position: fixed;
   bottom: 0;
-  width: 100%;
   left: 0;
+  width: 100%;
   justify-content: center;
   height: 2.5rem;
   display: flex;
   align-items: center;
   z-index: 9999;
 
-  transition: top 0.3s;
+  transition: none;
 
   button {
     padding: 10px;

+ 17 - 10
src/main/frontend/components/encryption.cljs

@@ -31,12 +31,12 @@
                        (when (not @reveal-secret-phrase?)
                          (reset! reveal-secret-phrase? true)))}
           [:div.font-medium "Public Key:"]
-          [:div public-key]
+          [:div.font-mono.select-all.break-all public-key]
           (if @reveal-secret-phrase?
             [:div
              [:div.mt-1.font-medium "Private Key:"]
-             [:div private-key]]
-            [:div "click to view the private key"])]]]
+             [:div.font-mono.select-all.break-all private-key]]
+            [:div.underline "click to view the private key"])]]]
 
        [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
         [:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
@@ -136,9 +136,11 @@
 
 (rum/defcs encryption-input-secret-inner <
   (rum/local "" ::secret)
+  (rum/local false ::loading)
   [state repo-url db-encrypted-secret close-fn]
   (rum/with-context [[t] i18n/*tongue-context*]
-    (let [secret (get state ::secret)]
+    (let [secret (::secret state)
+          loading (::loading state)]
       [:div
        [:div.sm:flex.sm:items-start
         [:div.mt-3.text-center.sm:mt-0.sm:text-left
@@ -156,14 +158,19 @@
          [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
           {:type "button"
            :on-click (fn []
+                       (reset! loading true)
                        (let [value @secret]
                          (when-not (string/blank? value) ; TODO: length or other checks
-                           (p/let [repo (state/get-current-repo)
-                                   keys (e/decrypt-with-passphrase value db-encrypted-secret)]
-                             (e/save-key-pair! repo keys)
-                             (close-fn true)
-                             (state/set-state! :encryption/graph-parsing? false)))))}
-          "Submit"]]]])))
+                           (let [repo (state/get-current-repo)]
+                             (p/do!
+                              (-> (e/decrypt-with-passphrase value db-encrypted-secret)
+                                  (p/then (fn [keys]
+                                            (e/save-key-pair! repo keys)
+                                            (close-fn true)
+                                            (state/set-state! :encryption/graph-parsing? false)))
+                                  (p/catch #(notification/show! "The password is not matched." :warning true))
+                                  (p/finally #(reset! loading false))))))))}
+          (if @loading (ui/loading "Decrypting") "Decrypt")]]]])))
 
 (defn encryption-input-secret-dialog
   [repo-url db-encrypted-secret close-fn]

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

@@ -161,6 +161,8 @@
   (let [repos (->> (state/sub [:me :repos])
                    (remove #(= (:url %) config/local-repo)))
         electron-mac? (and util/mac? (util/electron?))
+        vw-state (state/sub :ui/visual-viewport-state)
+        vw-pending? (state/sub :ui/visual-viewport-pending?)
         show-open-folder? (and (or (nfs/supported?)
                                    (mobile-util/is-native-platform?))
                                (empty? repos)
@@ -168,15 +170,17 @@
         refreshing? (state/sub :nfs/refreshing?)]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div.cp__header#head
-       {:class (cond electron-mac? "electron-mac"
-                     (mobile-util/native-ios?) "native-ios"
-                     (mobile-util/native-android?) "native-android")
+       {:class           (util/classnames [{:electron-mac   electron-mac?
+                                            :native-ios     (mobile-util/native-ios?)
+                                            :native-android (mobile-util/native-android?)
+                                            :is-vw-pending  (boolean vw-pending?)}])
         :on-double-click (fn [^js e]
                            (when-let [target (.-target e)]
                              (when (and (util/electron?)
                                         (or (.. target -classList (contains "cp__header"))))
                                (js/window.apis.toggleMaxOrMinActiveWindow))))
-        :style {:fontSize 50}}
+        :style           {:fontSize  50
+                          :transform (str "translateY(" (or (:offset-top vw-state) 0) "px)")}}
        [:div.l.flex
         (left-menu-button {:on-click (fn []
                                        (open-fn)

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

@@ -1,8 +1,10 @@
 .cp__header {
-  @apply shadow z-10 h-12;
+  @apply shadow z-10;
   -webkit-app-region: drag;
 
   padding-right: 0.5rem;
+  padding-top: var(--ls-headbar-inner-top-padding);
+  height: calc(var(--ls-headbar-height) + var(--ls-headbar-inner-top-padding));
   display: flex;
   align-items: center;
   justify-content: space-between;
@@ -170,3 +172,23 @@ a.button {
 .is-mac.is-electron :is(.cp__header, .cp__right-sidebar-topbar) :is(button, .button, a) {
   cursor: default !important;
 }
+
+html.is-native-ios,
+html.is-ios.is-safari {
+  #main-container {
+    padding-top: 20px;
+  }
+
+  .cp__header {
+    position: fixed !important;
+    background-color: var(--ls-primary-background-color);
+  }
+
+  .is-vw-pending {
+    display: none !important;
+  }
+}
+
+html.is-native-ios {
+  --ls-headbar-inner-top-padding: 36px;
+}

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

@@ -9,6 +9,7 @@
             [frontend.util :as util]
             [frontend.util.clock :as clock]
             [frontend.util.property :as property]
+            [frontend.format.block :as block]
             [medley.core :as medley]
             [rum.core :as rum]))
 
@@ -47,7 +48,8 @@
   [result]
   (let [ks [:block/properties :clock-time]
         result (map (fn [b]
-                      (assoc-in b ks (or (clock/clock-summary (:block/body b) false) 0)))
+                      (let [b (block/parse-title-and-body b)]
+                        (assoc-in b ks (or (clock/clock-summary (:block/body b) false) 0))))
                  result)]
     (if (every? #(zero? (get-in % ks)) result)
       (map #(medley/dissoc-in % ks) result)
@@ -125,10 +127,11 @@
                                           (:block/name item))]
 
                              :block       ; block title
-                             (let [title (:block/title item)]
+                             (let [content (:block/content item)
+                                   {:block/keys [title]} (block/parse-title-and-body (:block/format item) (:block/pre-block? item) content)]
                                (if (seq title)
                                  [:element (->elem :div (map-inline config title))]
-                                 [:string (:block/content item)]))
+                                 [:string content]))
 
                              :created-at
                              [:string (when-let [created-at (:block/created-at item)]

+ 68 - 56
src/main/frontend/components/repo.cljs

@@ -25,6 +25,17 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.text :as text]))
 
+(defn- open-repo-url [url]
+  (repo-handler/push-if-auto-enabled! (state/get-current-repo))
+  (state/set-current-repo! url)
+  ;; load config
+  (common-handler/reset-config! url nil)
+  (shortcut/refresh!)
+  (when-not (= :draw (state/get-current-route))
+    (route-handler/redirect-to-home!))
+  (when-let [dir-name (config/get-repo-dir url)]
+    (fs/watch-dir! dir-name)))
+
 (rum/defc add-repo
   [args]
   (if-let [graph-types (get-in args [:query-params :graph-types])]
@@ -66,6 +77,8 @@
               [:div.flex.justify-between.mb-4 {:key id}
                (if local?
                  [:a
+                  {:title url ;; show full path on hover
+                   :on-click #(open-repo-url url)}
                   (some-> (config/get-local-dir url)
                           (text/get-graph-name-from-path))]
                  [:a {:target "_blank"
@@ -197,18 +210,12 @@
                                  repos)
             repo-links (mapv
                         (fn [{:keys [id url]}]
-                          {:title (get-repo-name url)
-                           :options {:class "ml-1"
-                                     :on-click (fn []
-                                                 (repo-handler/push-if-auto-enabled! (state/get-current-repo))
-                                                 (state/set-current-repo! url)
-                                                 ;; load config
-                                                 (common-handler/reset-config! url nil)
-                                                 (shortcut/refresh!)
-                                                 (when-not (= :draw (state/get-current-route))
-                                                   (route-handler/redirect-to-home!))
-                                                 (when-let [dir-name (config/get-repo-dir url)]
-                                                   (fs/watch-dir! dir-name)))}})
+                          (let [repo-path (get-repo-name url)
+                                short-repo-name (text/get-graph-name-from-path repo-path)]
+                            {:title short-repo-name
+                             :hover-detail repo-path ;; show full path on hover
+                             :options {:class "ml-1"
+                                       :on-click #(open-repo-url url)}}))
                         switch-repos)
             links (concat repo-links
                           [(when (seq switch-repos)
@@ -218,56 +225,61 @@
                            {:title (t :all-graphs)
                             :options {:href (rfe/href :repos)}}
                            (let [nfs-repo? (config/local-db? current-repo)]
-                              (when (and nfs-repo?
-                                         (not= current-repo config/local-repo)
-                                         (or (nfs-handler/supported?)
-                                             (mobile-util/is-native-platform?)))
-                                {:title (t :sync-from-local-files)
-                                 :options {:on-click
-                                           (fn []
-                                             (state/pub-event!
-                                              [:modal/show
-                                               [:div {:style {:max-width 700}}
-                                                [:p "Refresh detects and processes files modified on your disk and diverged from the actual Logseq page content. Continue?"]
-                                                (ui/button
-                                                  "Yes"
-                                                  :on-click (fn []
-                                                              (state/close-modal!)
-                                                              (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))}}))
-                           {:title (t :re-index)
-                            :options {:on-click (fn []
-                                                  (state/pub-event!
-                                                   [:modal/show
-                                                    [:div {:style {:max-width 700}}
-                                                     [:p "Re-index will discard the current graph, and then processes all the files again as they are currently stored on disk. You will lose unsaved changes and it might take a while. Continue?"]
-                                                     (ui/button
-                                                       "Yes"
-                                                       :on-click (fn []
-                                                                   (state/close-modal!)
-                                                                   (repo-handler/re-index!
-                                                                    nfs-handler/rebuild-index!
-                                                                    page-handler/create-today-journal!)))]]))}}])]
+                             (when (and nfs-repo?
+                                        (not= current-repo config/local-repo)
+                                        (or (nfs-handler/supported?)
+                                            (mobile-util/is-native-platform?)))
+                               {:title (t :sync-from-local-files)
+                                :hover-detail (t :sync-from-local-files-detail)
+                                :options {:on-click
+                                          (fn []
+                                            (state/pub-event!
+                                             [:modal/show
+                                              [:div {:style {:max-width 700}}
+                                               [:p "Refresh detects and processes files modified on your disk and diverged from the actual Logseq page content. Continue?"]
+                                               (ui/button
+                                                "Yes"
+                                                :autoFocus "on"
+                                                :large? true
+                                                :on-click (fn []
+                                                            (state/close-modal!)
+                                                            (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))}}))
+                           {:title        (t :re-index)
+                            :hover-detail (t :re-index-detail)
+                            :options {:on-click
+                                      (fn []
+                                        (state/pub-event!
+                                         [:modal/show
+                                          [:div {:style {:max-width 700}}
+                                           [:p "Re-index will discard the current graph, and then processes all the files again as they are currently stored on disk. You will lose unsaved changes and it might take a while. Continue?"]
+                                           (ui/button
+                                            "Yes"
+                                            :autoFocus "on"
+                                            :large? true
+                                            :on-click (fn []
+                                                        (state/close-modal!)
+                                                        (repo-handler/re-index!
+                                                         nfs-handler/rebuild-index!
+                                                         page-handler/create-today-journal!)))]]))}}])]
         (when (seq repos)
           (ui/dropdown-with-links
            (fn [{:keys [toggle-fn]}]
-             [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md {:on-click toggle-fn}
-              (ui/icon "database mr-3" {:style {:font-size 20}
-                                        :id "database-icon"})
-              [:div.graphs
-               [:span#repo-switch.block.pr-2.whitespace-nowrap
-                [:span
-                 (let [repo-name (get-repo-name current-repo)
-                       repo-name (if (or (util/electron?)
-                                         (mobile-util/is-native-platform?))
-                                   (last
-                                    (string/split repo-name #"/"))
-                                   repo-name)]
-                   [:span#repo-name.font-medium {:title repo-name} repo-name])
-                 [:span.dropdown-caret.ml-2 {:style {:border-top-color "#6b7280"}}]]]]])
+             (let [repo-path (get-repo-name current-repo)
+                   short-repo-name (if (or (util/electron?)
+                                           (mobile-util/is-native-platform?))
+                                     (text/get-file-basename repo-path)
+                                     repo-path)]
+               [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
+                {:on-click toggle-fn :title repo-path} ;; show full path on hover
+                (ui/icon "database mr-3" {:style {:font-size 20} :id "database-icon"})
+                [:div.graphs
+                 [:span#repo-switch.block.pr-2.whitespace-nowrap
+                  [:span [:span#repo-name.font-medium short-repo-name]]
+                  [:span.dropdown-caret.ml-2 {:style {:border-top-color "#6b7280"}}]]]]))
            links
            (cond->
             {:modal-class (util/hiccup->class
                            "origin-top-right.absolute.left-0.mt-2.rounded-md.shadow-lg")}
              (seq switch-repos)
-             (assoc :links-header [:div.font-medium.text-sm.opacity-70.px-4.py-2
+             (assoc :links-header [:div.font-medium.text-sm.opacity-60.px-4.pt-2
                                    "Switch to:"]))))))))

+ 68 - 99
src/main/frontend/components/sidebar.cljs

@@ -65,15 +65,6 @@
      [:a.more svg/arrow-down-v2]]]
    [:div.bd child]])
 
-;; TODO: enhance
-(defn- pick-one-ast-page-ref
-  [block]
-  (when-let [title-ast (and block (:block/title block))]
-    (when-let [link-ref (and (= (ffirst title-ast) "Link")
-                             (:url (second (first title-ast))))]
-      (when (= "Page_ref" (first link-ref))
-        (second link-ref)))))
-
 (defn- delta-y
   [e]
   (let [rect (.. (.. e -target) getBoundingClientRect)]
@@ -227,92 +218,78 @@
     (ui/icon (str icon " mr-3") {:style {:font-size 20}})
     [:span.flex-1 title]]])
 
-(rum/defc sidebar-nav < rum/reactive
+(rum/defc sidebar-nav
   [route-match close-modal-fn]
   (rum/with-context [[t] i18n/*tongue-context*]
     (let [active? (fn [route] (= route (get-in route-match [:data :name])))
           page-active? (fn [page]
                          (= page (get-in route-match [:parameters :path :name])))
-          left-sidebar? (state/sub :ui/left-sidebar-open?)
           default-home (get-default-home-if-valid)]
-      (when left-sidebar?
-        [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
-         {:on-click #(when-let [^js target (and (util/mobile?) (.-target %))]
-                       (when (some (fn [sel] (boolean (.closest target sel)))
-                                   [".favorites" ".recent" ".dropdown-wrapper" ".nav-header"])
-                         (close-modal-fn)))}
-         [:div.flex.flex-col.pb-4.wrap
-          [:nav.px-2.space-y-1 {:aria-label "Sidebar"}
-           (repo/repos-dropdown)
-
-           [:div.nav-header
-
-            (if (:page default-home)
-              (sidebar-item
-               {:class "home-nav"
-                :title (:page default-home)
-                :on-click-handler route-handler/redirect-to-home!
-                :icon "home"})
-              (sidebar-item
-               {:class "journals-nav"
-                :title (t :right-side-bar/journals)
-                :on-click-handler route-handler/go-to-journals!
-                :icon "calendar"}))
-
-            [:div.flashcards-nav
-             (flashcards)]
 
-            (sidebar-item
-             {:class "graph-view-nav"
-              :title (t :right-side-bar/graph-view)
-              :href (rfe/href :graph)
-              :icon "hierarchy"})
+      [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
+       {:on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
+                     (when (some (fn [sel] (boolean (.closest target sel)))
+                                 [".favorites" ".recent" ".dropdown-wrapper" ".nav-header"])
+                       (close-modal-fn)))}
+       [:div.flex.flex-col.pb-4.wrap
+        [:nav.px-2.space-y-1 {:aria-label "Sidebar"}
+         (repo/repos-dropdown)
+
+         [:div.nav-header
 
+          (if (:page default-home)
             (sidebar-item
-             {:class "all-pages-nav"
-              :title (t :right-side-bar/all-pages)
-              :href (rfe/href :all-pages)
-              :icon "files"})]]
-
-          (favorites t)
-
-          (recent-pages t)
-
-          [:div.flex-column-spacer] ;; Push following objects to the bottom
-
-          [:nav.px-2.space-y-1 {:aria-label "Sidebar"
-                                :class "new-page"}
-           (when-not config/publishing?
-             [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
-              {:on-click (fn []
-                           (state/toggle-left-sidebar!)
-                           (state/pub-event! [:go/search]))}
-              (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
-              [:span.flex-1 (t :right-side-bar/new-page)]])]]]))))
-
-(rum/defc sidebar-mobile-sidebar < rum/reactive
-  [{:keys [left-sidebar-open? close-fn route-match]}]
-  [:div.md:hidden.ls-mobile-left-sidebar
-   {:class (if left-sidebar-open? "is-left-sidebar-open" "")}
-   [:div.fixed.inset-0.z-30.bg-gray-600.pointer-events-none.ease-linear.duration-300
-    {:class (if left-sidebar-open?
-              "opacity-75 pointer-events-auto"
-              "opacity-0 pointer-events-none")
-     :on-click close-fn}]
-   [:div#left-bar.fixed.inset-y-0.left-0.flex.flex-col.z-40.transform.ease-in-out.duration-300
-    {:class (if left-sidebar-open?
-              "translate-x-0"
-              "-translate-x-full")
-     :style {:padding-top (ui/main-content-top-padding)}}
-    (when left-sidebar-open?
-      [:div.cp__header#head
-       [:div.l.flex
-        (header/left-menu-button
-         {:on-click (fn []
-                      (state/set-left-sidebar-open!
-                       (not (:ui/left-sidebar-open? @state/state))))})]])
-    [:div.flex-1.h-0.overflow-y-auto
-     (sidebar-nav route-match close-fn)]]])
+              {:class            "home-nav"
+               :title            (:page default-home)
+               :on-click-handler route-handler/redirect-to-home!
+               :icon             "home"})
+            (sidebar-item
+              {:class            "journals-nav"
+               :title            (t :right-side-bar/journals)
+               :on-click-handler route-handler/go-to-journals!
+               :icon             "calendar"}))
+
+          [:div.flashcards-nav
+           (flashcards)]
+
+          (sidebar-item
+            {:class "graph-view-nav"
+             :title (t :right-side-bar/graph-view)
+             :href  (rfe/href :graph)
+             :icon  "hierarchy"})
+
+          (sidebar-item
+            {:class "all-pages-nav"
+             :title (t :right-side-bar/all-pages)
+             :href  (rfe/href :all-pages)
+             :icon  "files"})]]
+
+        (favorites t)
+
+        (recent-pages t)
+
+        [:div.flex-column-spacer]                           ;; Push following objects to the bottom
+
+        [:nav.px-2.space-y-1 {:aria-label "Sidebar"
+                              :class      "new-page"}
+         (when-not config/publishing?
+           [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
+            {:on-click (fn []
+                         (and (util/sm-breakpoint?)
+                              (state/toggle-left-sidebar!))
+                         (state/pub-event! [:go/search]))}
+            (ui/icon "circle-plus mr-3" {:style {:font-size 20}})
+            [:span.flex-1 (t :right-side-bar/new-page)]])]]])))
+
+(rum/defc left-sidebar < rum/reactive
+  [{:keys [left-sidebar-open? route-match]}]
+  (let [close-fn #(state/set-left-sidebar-open! false)]
+    [:div#left-sidebar.cp__sidebar-left-layout
+     {:class (util/classnames [{:is-open left-sidebar-open?}])}
+
+     [ ;; sidebar contents
+      (sidebar-nav route-match close-fn)
+      [:span.shade-mask {:on-click close-fn}]]]))
 
 (rum/defc main <
   {:did-mount (fn [state]
@@ -335,12 +312,8 @@
         :style {:padding-top (ui/main-content-top-padding)}}
 
        ;; desktop left sidebar layout
-       (when-not mobile?
-         [:div#sidebar-nav-wrapper.cp__sidebar-left-layout.overflow-y-auto.h-full
-          {:class (util/classnames [{:is-open left-sidebar-open?}])}
-
-          ;; sidebar contents
-          (sidebar-nav route-match nil)])
+       (left-sidebar {:left-sidebar-open? left-sidebar-open?
+                      :route-match route-match})
 
        [:div#main-content-container.w-full.flex.justify-center
         [:div.cp__sidebar-main-content
@@ -563,13 +536,9 @@
        [:div.theme-inner
         {:class (util/classnames [{:ls-left-sidebar-open left-sidebar-open?}])}
 
-        (sidebar-mobile-sidebar
-         {:left-sidebar-open? left-sidebar-open?
-          :close-fn    close-fn
-          :route-match route-match})
-
-        [:div.#app-container.h-screen.flex {:style {:padding-top (ui/main-content-top-padding)}}
-         [:div.flex-1.h-full.flex.flex-col#left-container.relative
+        [:div.#app-container
+         {:style {:padding-top (ui/main-content-top-padding)}}
+         [:div#left-container
           {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}
           (header/header {:open-fn        open-fn
                           :white?         white?

+ 101 - 83
src/main/frontend/components/sidebar.css

@@ -1,8 +1,8 @@
-@supports(padding: max(0px)) {
-    .post {
-        padding-left: max(12px, env(safe-area-inset-left));
-        padding-right: max(12px, env(safe-area-inset-right));
-    }
+@supports (padding: max(0px)) {
+  .post {
+    padding-left: max(12px, env(safe-area-inset-left));
+    padding-right: max(12px, env(safe-area-inset-right));
+  }
 }
 
 #app-container {
@@ -21,9 +21,14 @@
 }
 
 #app-container {
+  display: flex;
   flex: 0 0 100%;
 }
 
+#left-container {
+  @apply flex flex-1 flex-col relative h-screen;
+}
+
 #main-container {
   position: relative;
   height: 100%;
@@ -31,7 +36,11 @@
 
 #main-content {
   &.is-left-sidebar-open {
-    padding-left: var(--ls-left-sidebar-width);
+    padding-left: 0;
+
+    @screen sm {
+      padding-left: var(--ls-left-sidebar-width);
+    }
   }
 
   &-container {
@@ -40,70 +49,20 @@
   }
 }
 
-html.is-mobile {
-  #main-content.is-left-sidebar-open {
-    padding-left: 0;
-  }
-}
-
-#left-sidebar {
-  width: 240px;
-  height: 100%;
-  top: 0;
-  left: -240px;
-  position: absolute;
-  z-index: 11;
-  opacity: 0;
-  background-color: #002b36;
-
-  .enter {
-    opacity: 1;
-    left: 0;
-  }
-}
-
-#left-bar {
-  background-color: var(--ls-primary-background-color);
-  width:             70vw;
-  max-width:         300px;
-
-  > .head-wrap {
-    background-color: var(--ls-search-background-color);
-  }
-
-  .close-panel-btn {
-    color: var(--ls-active-primary-color);
-  }
-
-  nav > a {
-    color: var(--ls-icon-color);
-  }
-
-  a {
-    color: var(--ls-primary-text-color);
-  }
-}
-
-.ls-mobile-left-sidebar {
-  position: relative;
-  z-index: -1;
-
-  &.is-left-sidebar-open {
-    z-index: var(--ls-z-index-level-3);
-  }
-}
-
 .left-sidebar-inner {
   position: relative;
   height: 100%;
-  z-index: 1;
-  overflow: auto;
+  padding-top: 12px;
+  width: var(--ls-left-sidebar-sm-width);
+  overflow-x: hidden;
+  overflow-y: auto;
+  background-color: var(--ls-primary-background-color);
+  transition: transform .3s;
+  transform: translateX(-100%);
+  z-index: 3;
 
   > .wrap {
-      padding-top: 24px;
-      @screen md {
-          padding-top: 60px;
-      }
+    padding-top: 55px;
   }
 
   .dropdown-wrapper {
@@ -112,10 +71,15 @@ html.is-mobile {
 
   a.item {
     user-select: none;
+    transition: none;
 
     > span {
       margin-top: 3px;
     }
+
+    &:hover {
+      background-color: var(--ls-quaternary-background-color);
+    }
   }
 
   .nav-content-item {
@@ -213,46 +177,100 @@ html.is-mobile {
     }
   }
 
-  a.item:hover {
-    background-color: var(--ls-quaternary-background-color);
+  @screen sm {
+    background-color: var(--ls-secondary-background-color);
+    width: var(--ls-left-sidebar-width);
+    padding-top: 0;
   }
 }
 
-.white-theme .left-sidebar-inner a, .white-theme .left-bar a {
-  color: var(--ls-primary-text-color);
-}
-
 .cp__sidebar-left-layout {
-  @apply flex-col sm:block;
-
-  background-color: var(--ls-secondary-background-color);
-  width: var(--ls-left-sidebar-width);
+  width: 0;
   height: 100vh;
   position: fixed;
   top: 0;
   left: 0;
   z-index: 9;
+  transition: width 1.2s;
 
-  transition: transform .25s;
-  transform: translateX(-100%);
-  overflow: hidden;
+  > .shade-mask {
+    background-color: rgba(0, 0, 0, .5);
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    z-index: -1;
+    opacity: 0;
+    transition: opacity .1s;
+  }
+
+  &.is-open {
+    transition: none;
+    width: 100%;
+
+    .left-sidebar-inner {
+      transform: translateX(0);
+    }
+
+    > .shade-mask {
+      opacity: 1;
+      z-index: 1;
+    }
+  }
 
   &:before {
     content: " ";
     height: 3rem;
-    width: calc(var(--ls-left-sidebar-width) - var(--ls-scrollbar-width));
-    background-color: var(--ls-secondary-background-color);
+    width: calc(var(--ls-left-sidebar-sm-width) - var(--ls-scrollbar-width));
+    transition: width .3s;
+    background-color: transparent;
     position: fixed;
     left: 0;
     top: 0;
-    z-index: 2;
+    opacity: .8;
+    z-index: 5;
   }
 
-  &.is-open {
-    transform: translateX(0);
+  @screen sm {
+    width: 0;
+
+    &:before {
+      background-color: var(--ls-secondary-background-color);
+      width: 0;
+      overflow: hidden;
+    }
+
+    &.is-open {
+      width: var(--ls-left-sidebar-width);
+
+      &:before {
+        width: calc(var(--ls-left-sidebar-width) - var(--ls-scrollbar-width));
+      }
+    }
+
+    > .shade-mask {
+      display: none;
+    }
   }
 }
 
+.ls-left-sidebar-open {
+  .cp__header > .r {
+    display: none;
+  }
+
+  @screen sm {
+    .cp__header > .r {
+      display: flex;
+    }
+  }
+}
+
+.white-theme #left-sidebar a {
+  color: var(--ls-primary-text-color);
+}
+
 .settings-modal {
   margin: -15px;
 }

+ 0 - 6
src/main/frontend/components/theme.cljs

@@ -62,12 +62,6 @@
      #(when system-theme?
         (ui/setup-system-theme-effect!))
      [system-theme?])
-
-    (rum/use-effect!
-     #(when (mobile-util/native-ios?)
-        (ui/setup-patch-ios-fixed-bottom-position!))
-     [edit?])
-
     [:div
      {:class    (str theme "-theme")
       :on-click on-click}

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

@@ -83,7 +83,7 @@
                     :error
                     false)))))))]])))
 
-(rum/defcs add-local-directory
+(rum/defc add-local-directory
   []
   (rum/with-context [[t] i18n/*tongue-context*]
     [:div.flex.flex-col

+ 1 - 4
src/main/frontend/db/model.cljs

@@ -27,7 +27,6 @@
     :block/type
     :block/left
     :block/format
-    :block/title
     :block/refs
     :block/_refs
     :block/path-refs
@@ -36,7 +35,6 @@
     :block/marker
     :block/priority
     :block/properties
-    :block/body
     :block/pre-block?
     :block/scheduled
     :block/deadline
@@ -45,7 +43,6 @@
     :block/updated-at
     :block/file
     :block/parent
-    :block/unordered
     :block/heading-level
     {:block/page [:db/id :block/name :block/original-name :block/journal-day]}
     {:block/_parent ...}])
@@ -900,7 +897,7 @@
    (has-children? (state/get-current-repo) block-id))
   ([repo block-id]
    (let [db (conn/get-conn repo)]
-     (when-let [block (db-utils/entity [:block/uuid block-id])]
+     (when-let [block (get-block-by-uuid block-id)]
        ;; perf: early stop
        (let [result (d/datoms db :avet :block/parent (:db/id block))]
          (boolean (seq result)))))))

+ 1 - 10
src/main/frontend/db_schema.cljs

@@ -30,9 +30,6 @@
    ;; :markdown, :org
    :block/format {}
 
-   ;; mldoc parsed ast
-   :block/title {}
-
    ;; belongs to which page
    :block/page {:db/valueType :db.type/ref
                 :db/index true}
@@ -71,9 +68,6 @@
    ;; vector
    :block/properties-order {}
 
-   ;; parsed ast
-   :block/body {}
-
    ;; first block that's not a heading or unordered list
    :block/pre-block? {}
 
@@ -140,8 +134,6 @@
     :block/level
     :block/heading-level
     :block/type
-    :block/title
-    :block/body
     :block/properties
     :block/created-at
     :block/updated-at
@@ -160,5 +152,4 @@
     :block/content
     :block/properties
     :block/alias
-    :block/tags
-    :block/unordered})
+    :block/tags})

+ 4 - 2
src/main/frontend/dicts.cljs

@@ -276,8 +276,10 @@
         :cancel "Cancel"
         :close "Close"
         :delete "Delete"
-        :re-index "Re-index (rebuild the graph)"
-        :sync-from-local-files "Refresh (import changes from local files)"
+        :re-index "Re-index"
+        :re-index-detail "Rebuild the graph"
+        :sync-from-local-files "Refresh"
+        :sync-from-local-files-detail "Import changes from local files"
         :unlink "unlink"
         :search (if config/publishing?
                   "Search"

+ 1 - 1
src/main/frontend/extensions/pdf/pdf.css

@@ -671,7 +671,7 @@ body.is-pdf-active {
     padding-left: calc(var(--ph-view-container-width) + 15px);
   }
 
-  #sidebar-nav-wrapper {
+  #left-sidebar {
     border: none !important;
     display: none;
   }

+ 38 - 28
src/main/frontend/extensions/srs.cljs

@@ -416,6 +416,16 @@
 (def review-finished
   [:p.p-2 "Congrats, you've reviewed all the cards for this query, see you next time! 💯"])
 
+(defn btn-with-shortcut [{:keys [shortcut id btn-text background on-click]}]
+  (ui/tippy
+   {:html  [:div.text-sm.font-medium shortcut]
+    :delay 200
+    :theme "monospace"}
+   (ui/button btn-text
+              :id id
+              :background background
+              :on-click on-click)))
+
 (rum/defcs view
   < rum/reactive
   (rum/local 1 ::phase)
@@ -455,14 +465,15 @@
          (if (or preview? modal?)
            [:div.flex.my-4.justify-between
             [:div.flex-1
-             (when-not (and (not preview?) (= next-phase 1))
-               (ui/button (case next-phase
-                            1 [:span "Hide answers " (ui/render-keyboard-shortcut [:s])]
-                            2 [:span "Show answers " (ui/render-keyboard-shortcut [:s])]
-                            3 [:span "Show clozes " (ui/render-keyboard-shortcut [:s])])
-                          :id "card-answers"
-                          :class "mr-2"
-                 :on-click #(reset! phase next-phase)))
+              (when-not (and (not preview?) (= next-phase 1))
+                [:div.flex.flex-row.justify-between
+                 (btn-with-shortcut {:btn-text (case next-phase
+                                                 1 "Hide answers"
+                                                 2 "Show answers"
+                                                 3 "Show clozes")
+                                     :shortcut "s"
+                                     :id       "card-answers"
+                                     :on-click #(reset! phase next-phase)})])
 
              (when (and (> (count cards) 1) preview?)
                (ui/button [:span "Next " (ui/render-keyboard-shortcut [:n])]
@@ -475,26 +486,25 @@
                      interval-days-score-4 (get (get-next-interval card 5) card-last-interval-property)
                      interval-days-score-5 (get (get-next-interval card 5) card-last-interval-property)]
                  [:div.flex.flex-row.justify-between
-                  (ui/button (if (util/mobile?)
-                               "Forgotten"
-                               [:span "Forgotten " (ui/render-keyboard-shortcut [:f])])
-                    :id "card-forgotten"
-                    :on-click (fn []
-                                (score-and-next-card 1 card card-index cards phase review-records cb)
-                                (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
-                                  (editor-handler/set-block-property! root-block-id card-next-schedule-property tomorrow))))
-
-                  (ui/button (if (util/mobile?)
-                                 "Remembered"
-                                 [:span "Remembered " (ui/render-keyboard-shortcut [:r])])
-                    :id "card-remembered"
-                    :on-click #(score-and-next-card 5 card card-index cards phase review-records cb))
-
-                  (ui/button (if (util/mobile?)
-                               "Hard"
-                               [:span "Took a while to recall " (ui/render-keyboard-shortcut [:t])])
-                    :id "card-recall"
-                    :on-click #(score-and-next-card 3 card card-index cards phase review-records cb))]))]
+                  (btn-with-shortcut {:btn-text   "Forgotten"
+                                      :shortcut   "f"
+                                      :id         "card-forgotten"
+                                      :background "red"
+                                      :on-click   (fn []
+                                                    (score-and-next-card 1 card card-index cards phase review-records cb)
+                                                    (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
+                                                      (editor-handler/set-block-property! root-block-id card-next-schedule-property tomorrow)))})
+
+                  (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" "Took a while to recall")
+                                      :shortcut "t"
+                                      :id       "card-recall"
+                                      :on-click #(score-and-next-card 3 card card-index cards phase review-records cb)})
+
+                  (btn-with-shortcut {:btn-text   "Remembered"
+                                      :shortcut   "r"
+                                      :id         "card-remembered"
+                                      :background "green"
+                                      :on-click   #(score-and-next-card 5 card card-index cards phase review-records cb)})]))]
 
             (when preview?
               (ui/tippy {:html [:div.text-sm

+ 20 - 15
src/main/frontend/format/block.cljs

@@ -716,24 +716,29 @@
                     new-block
                     {:block/path-refs path-ref-pages})
                    (> (count blocks) 1)
-                   (assoc :block/warning :multiple-blocks))]
+                   (assoc :block/warning :multiple-blocks))
+           block (dissoc block :block/title :block/body :block/level)]
        (if uuid (assoc block :block/uuid uuid) block)))))
 
 (defn parse-title-and-body
-  [format pre-block? content]
-  (def content content)
-  (let [ast (format/to-edn content format nil)
-        content (if pre-block? content
-                    (str (config/get-block-pattern format) " " (string/triml content)))
-        content (property/remove-properties format content)
-        ast (->> (format/to-edn content format nil)
-                 (map first))
-        title (when (heading-block? (first ast))
-                (:title (second (first ast))))]
-    (cond->
-      {:block/body (vec (if title (rest ast) ast))}
-      title
-      (assoc :block/title title))))
+  ([block]
+   (when (map? block)
+     (merge block
+            (parse-title-and-body (:block/format block)
+                                  (:block/pre-block? block)
+                                  (:block/content block)))))
+  ([format pre-block? content]
+   (let [content (if pre-block? content
+                     (str (config/get-block-pattern format) " " (string/triml content)))
+         content (property/remove-properties format content)
+         ast (->> (format/to-edn content format nil)
+                  (map first))
+         title (when (heading-block? (first ast))
+                 (:title (second (first ast))))]
+     (cond->
+       {:block/body (vec (if title (rest ast) ast))}
+       title
+       (assoc :block/title title)))))
 
 (defn macro-subs
   [macro-content arguments]

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

@@ -248,9 +248,9 @@
                          (.preventDefault e)
                          (state/pub-event! [:modal/show
                                             [:div
-                                             [:h1.title "Reload Logseq?"]
+                                             [:p "Reload Logseq?"]
                                              (ui/button
-                                              [:span "Yes " (ui/render-keyboard-shortcut ["enter"])]
+                                              "Yes"
                                               :autoFocus "on"
                                               :large? true
                                               :on-click (fn []

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

@@ -68,7 +68,7 @@
                (remove nil?)))))))
 
 ;; TODO: reduced version
-(defn walk-block
+(defn- walk-block
   [block check? transform]
   (let [result (atom nil)]
     (walk/postwalk

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

@@ -8,9 +8,7 @@
 
 (s/def :command/id keyword?)
 (s/def :command/desc string?)
-(s/def :command/action (and fn?
-                            ;; action fn expects zero number of arities
-                            (fn [action] (zero? (.-length action)))))
+(s/def :command/action fn?)
 (s/def :command/shortcut string?)
 (s/def :command/tag vector?)
 
@@ -22,11 +20,7 @@
   (->> [:shortcut.handler/editor-global
         :shortcut.handler/global-prevent-default
         :shortcut.handler/global-non-editing-only]
-       (mapcat shortcut-helper/shortcuts->commands)
-       ;; some of the shortcut fn takes the shape of (fn [e] xx)
-       ;; instead of (fn [] xx)
-       ;; remove them for now
-       (remove (fn [{:keys [action]}] (not (zero? (.-length action)))))))
+       (mapcat shortcut-helper/shortcuts->commands)))
 
 (defn get-commands []
   (->> (get @state/state :command-palette/commands)

+ 117 - 101
src/main/frontend/handler/editor.cljs

@@ -347,8 +347,10 @@
     value))
 
 (defn wrap-parse-block
-  [{:block/keys [content format left page uuid level] :as block}]
-  (let [block (or (and (:db/id block) (db/pull (:db/id block))) block)
+  [{:block/keys [content format left page uuid level pre-block?] :as block}]
+  (let [block (merge
+               (or (and (:db/id block) (db/pull (:db/id block))) block)
+               (block/parse-title-and-body format pre-block? content))
         properties (:block/properties block)
         real-content (:block/content block)
         content (if (and (seq properties) real-content (not= real-content content))
@@ -747,7 +749,6 @@
      :block/format format
      :block/content content
      :block/parent page
-     :block/unordered true
      :block/page page}))
 
 (defn default-properties-block
@@ -767,7 +768,6 @@
       :block/format format
       :block/content content
       :block/parent page
-      :block/unordered true
       :block/page page})))
 
 (defn add-default-title-property-if-needed!
@@ -1186,16 +1186,41 @@
 
 (defn copy-block-refs
   []
-  (when-let [blocks (seq (get-selected-blocks-with-children))]
-    (let [ids (->> (distinct (map #(when-let [id (dom/attr % "blockid")]
-                                     (uuid id)) blocks))
-                   (remove nil?))
-          ids-str (some->> ids
-                           (map (fn [id] (util/format "((%s))" id)))
-                           (string/join "\n\n"))]
-      (doseq [id ids]
-        (set-block-id! id))
-      (util/copy-to-clipboard! ids-str))))
+  (when-let [selected-blocks (seq (get-selected-blocks-with-children))]
+    (let [blocks (->> (distinct (map #(when-let [id (dom/attr % "blockid")]
+                                        (let [level (dom/attr % "level")]
+                                          {:id (uuid id)
+                                           :level (int level)}))
+                                     selected-blocks))
+                      (remove nil?))
+          first-block (first blocks)
+          first-root-level-index (ffirst
+                                   (filter (fn [[_ block]] (= (:level block) 1))
+                                           (map-indexed vector blocks)))
+          root-level (atom (:level first-block))
+          adjusted-blocks (map-indexed
+                            (fn [index {:keys [id level]}]
+                              {:id id
+                               :level (if (and (< index first-root-level-index))
+                                        (if (< level @root-level)
+                                          (do
+                                            (reset! root-level level)
+                                            1)
+                                          (inc (- level @root-level)))
+                                        level)})
+                            blocks)
+          block (db/pull [:block/uuid (:id first-block)])
+          copy-str (some->> adjusted-blocks
+                            (map (fn [{:keys [id level]}]
+                                   (condp = (:block/format block)
+                                    :org
+                                    (util/format (str (string/join (repeat level "*")) " ((%s))") id)
+                                    :markdown
+                                    (util/format (str (string/join (repeat (dec level) "\t")) "- ((%s))") id))))
+                            (string/join "\n\n"))]
+      (doseq [block blocks]
+        (set-block-id! (:id block)))
+      (util/copy-to-clipboard! copy-str))))
 
 (defn copy-block-embeds
   []
@@ -1879,7 +1904,8 @@
           (let [blocks (-> (state/get-selection-blocks)
                            reorder-selected-blocks)
                 blocks (filter #(= (:block/parent %) (:block/parent (first blocks))) blocks)]
-            (move-nodes blocks)))))))
+            (when (seq blocks)
+              (move-nodes blocks))))))))
 
 ;; selections
 (defn on-tab
@@ -2131,22 +2157,10 @@
   [uuid page exclude-properties format content-update-fn]
   (fn [block]
     (outliner-core/block
-     (let [[new-content new-title]
+     (let [new-content
            (if content-update-fn
-             (let [new-content (content-update-fn (:block/content block))
-                   new-title (or (->> (mldoc/->edn
-                                       (str (case format
-                                              :markdown "- "
-                                              :org "* ")
-                                            (if (seq (:block/title block)) "" "\n")
-                                            new-content)
-                                       (mldoc/default-config format))
-                                      (ffirst)
-                                      (second)
-                                      (:title))
-                                 (:block/title block))]
-               [new-content new-title])
-             [(:block/content block) (:block/title block)])
+             (content-update-fn (:block/content block))
+             (:block/content block))
            new-content
            (->> new-content
                 (property/remove-property format "id")
@@ -2165,7 +2179,6 @@
                                                  exclude-properties))
                      :block/meta (dissoc (:block/meta block) :start-pos :end-pos)
                      :block/content new-content
-                     :block/title new-title
                      :block/path-refs (->> (cons (:db/id page) (:block/path-refs block))
                                            (remove nil?))})]
        m))))
@@ -2421,7 +2434,7 @@
                   (when (thingatpt/get-setting :properties?)
                     (thingatpt/properties-at-point input))
                   (when (thingatpt/get-setting :list?)
-                    (and (cursor/end-of-line? input) ;; only apply DWIM when cursor at EOL 
+                    (and (cursor/end-of-line? input) ;; only apply DWIM when cursor at EOL
                          (thingatpt/list-item-at-point input))))]
           (cond
             thing-at-point
@@ -3293,10 +3306,11 @@
 
 (defn collapsable? [block-id]
   (if-let [block (db-model/get-block-by-uuid block-id)]
-    (and
-     (nil? (-> block :block/properties :collapsed))
-     (or (not-empty (:block/body block))
-         (db-model/has-children? block-id)))
+    (let [block (block/parse-title-and-body block)]
+      (and
+       (nil? (-> block :block/properties :collapsed))
+       (or (not-empty (:block/body block))
+           (db-model/has-children? block-id))))
     false))
 
 (defn collapse-block! [block-id]
@@ -3307,73 +3321,75 @@
   (remove-block-property! block-id :collapsed))
 
 (defn expand!
-  [e]
-  (util/stop e)
-  (cond
-    (state/editing?)
-    (when-let [block-id (:block/uuid (state/get-edit-block))]
-      (expand-block! block-id))
-
-    (state/selection?)
-    (do
-      (->> (get-selected-blocks-with-children)
-           (map (fn [dom]
-                  (-> (dom/attr dom "blockid")
-                      medley/uuid
-                      expand-block!)))
-           doall)
-      (clear-selection!))
-
-    :else
-    ;; expand one level
-    (let [blocks-with-level (all-blocks-with-level {})
-          max-level (or (apply max (map :block/level blocks-with-level)) 99)]
-      (loop [level 1]
-        (if (> level max-level)
-          nil
-          (let [blocks-to-expand (->> blocks-with-level
-                                      (filter (fn [b] (= (:block/level b) level)))
-                                      (filter (fn [{:block/keys [properties]}]
-                                                (contains? properties :collapsed))))]
-            (if (empty? blocks-to-expand)
-              (recur (inc level))
-              (doseq [{:block/keys [uuid]} blocks-to-expand]
-                (expand-block! uuid)))))))))
+  ([e] (expand! e false))
+  ([e clear-selection?]
+   (util/stop e)
+   (cond
+     (state/editing?)
+     (when-let [block-id (:block/uuid (state/get-edit-block))]
+       (expand-block! block-id))
+
+     (state/selection?)
+     (do
+       (->> (get-selected-blocks-with-children)
+            (map (fn [dom]
+                   (-> (dom/attr dom "blockid")
+                       medley/uuid
+                       expand-block!)))
+            doall)
+       (and clear-selection? (clear-selection!)))
+
+     :else
+     ;; expand one level
+     (let [blocks-with-level (all-blocks-with-level {})
+           max-level (or (apply max (map :block/level blocks-with-level)) 99)]
+       (loop [level 1]
+         (if (> level max-level)
+           nil
+           (let [blocks-to-expand (->> blocks-with-level
+                                       (filter (fn [b] (= (:block/level b) level)))
+                                       (filter (fn [{:block/keys [properties]}]
+                                                 (contains? properties :collapsed))))]
+             (if (empty? blocks-to-expand)
+               (recur (inc level))
+               (doseq [{:block/keys [uuid]} blocks-to-expand]
+                 (expand-block! uuid))))))))))
 
 (defn collapse!
-  [e]
-  (util/stop e)
-  (cond
-    (state/editing?)
-    (when-let [block-id (:block/uuid (state/get-edit-block))]
-      (collapse-block! block-id))
-
-    (state/selection?)
-    (do
-      (->> (get-selected-blocks-with-children)
-           (map (fn [dom]
-                  (-> (dom/attr dom "blockid")
-                      medley/uuid
-                      collapse-block!)))
-           doall)
-      (clear-selection!))
-
-    :else
-    ;; collapse by one level from outside
-    (let [blocks-with-level
-          (all-blocks-with-level {:collapse? true})
-          max-level (or (apply max (map :block/level blocks-with-level)) 99)]
-      (loop [level max-level]
-        (if (zero? level)
-          nil
-          (let [blocks-to-collapse
-                (->> blocks-with-level
-                     (filter (fn [b] (= (:block/level b) level)))
-                     (filter (fn [b] (collapsable? (:block/uuid b)))))]
-            (if (empty? blocks-to-collapse)
-              (recur (dec level))
-              (doseq [{:block/keys [uuid]} blocks-to-collapse]
-                (collapse-block! uuid)))))))))
+  ([e] (collapse! e false))
+  ([e clear-selection?]
+   (util/stop e)
+   (cond
+     (state/editing?)
+     (when-let [block-id (:block/uuid (state/get-edit-block))]
+       (collapse-block! block-id))
+
+     (state/selection?)
+     (do
+       (->> (get-selected-blocks-with-children)
+            (map (fn [dom]
+                   (-> (dom/attr dom "blockid")
+                       medley/uuid
+                       collapse-block!)))
+            doall)
+       (and clear-selection? (clear-selection!)))
+
+     :else
+     ;; collapse by one level from outside
+     (let [blocks-with-level
+           (all-blocks-with-level {:collapse? true})
+           max-level (or (apply max (map :block/level blocks-with-level)) 99)]
+       (loop [level max-level]
+         (if (zero? level)
+           nil
+           (let [blocks-to-collapse
+                 (->> blocks-with-level
+                      (filter (fn [b] (= (:block/level b) level)))
+                      (filter (fn [b] (collapsable? (:block/uuid b)))))]
+             (if (empty? blocks-to-collapse)
+               (recur (dec level))
+               (doseq [{:block/keys [uuid]} blocks-to-collapse]
+                 (collapse-block! uuid))))))))))
 
 (defn- collapse-all!
   []

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

@@ -547,8 +547,6 @@
           :block/heading-level
           :block/format
           :block/children
-          :block/title
-          :block/body
           :block/content]))})
 
 (defn- file-name [repo extension]

+ 9 - 5
src/main/frontend/handler/extract.cljs

@@ -137,9 +137,10 @@
                            (remove nil? blocks))
                      (remove nil?))
           pages (remove nil? pages)
-          pages (map (fn [page] (assoc page :block/uuid (db/new-block-id))) pages)]
-      [pages
-       (remove nil? blocks)])
+          pages (map (fn [page] (assoc page :block/uuid (db/new-block-id))) pages)
+          blocks (->> (remove nil? blocks)
+                      (map (fn [b] (dissoc b :block/title :block/body :block/level))))]
+      [pages blocks])
     (catch js/Error e
       (log/error :exception e))))
 
@@ -232,8 +233,11 @@
                             pages (with-ref-pages pages blocks)
                             blocks (map (fn [block]
                                           (let [id (:block/uuid block)
-                                                properties (get-in metadata [:block/properties id])]
-                                            (update block :block/properties merge properties)))
+                                                properties (merge (get-in metadata [:block/properties id])
+                                                                  (:block/properties block))]
+                                            (if (seq properties)
+                                              (assoc block :block/properties properties)
+                                              (dissoc block :block/properties))))
                                      blocks)
                             ;; To prevent "unique constraint" on datascript
                             pages-index (map #(select-keys % [:block/name]) pages)

+ 16 - 34
src/main/frontend/handler/page.cljs

@@ -126,31 +126,6 @@
            (route-handler/redirect-to-page! page))
          page)))))
 
-
-
-(defn get-plugins
-  [blocks]
-  (let [plugins (atom {})
-        add-plugin #(swap! plugins assoc % true)]
-    (walk/postwalk
-     (fn [x]
-       (if (and (vector? x)
-                (>= (count x) 2))
-         (let [[type option] x]
-           (case type
-             "Src" (when (:language option)
-                     (add-plugin "highlight"))
-             "Export" (when (= option "latex")
-                        (add-plugin "latex"))
-             "Latex_Fragment" (add-plugin "latex")
-             "Math" (add-plugin "latex")
-             "Latex_Environment" (add-plugin "latex")
-             nil)
-           x)
-         x))
-     (map :block/body blocks))
-    @plugins))
-
 (defn delete-file!
   [repo page-name]
   (let [file (db/get-page-file page-name)
@@ -294,6 +269,15 @@
                          (vec))]
       (config-handler/set-config! :favorites favorites))))
 
+(defn toggle-favorite! []
+  (let [page-name  (state/get-current-page)
+        favorites  (:favorites (state/sub-graph-config))
+        favorited? (contains? (set (map string/lower-case favorites))
+                              (string/lower-case page-name))]
+    (if favorited?
+      (unfavorite-page! page-name)
+      (favorite-page! page-name))))
+
 (defn delete!
   [page-name ok-handler & {:keys [delete-file?]
                            :or {delete-file? true}}]
@@ -341,22 +325,20 @@
         page-ids (->> (map :block/page blocks)
                       (remove nil?)
                       (set))
-        tx       (->> (map (fn [{:block/keys [uuid title content properties format pre-block?] :as block}]
+        tx       (->> (map (fn [{:block/keys [uuid content properties format pre-block?] :as block}]
                              (let [content    (let [content' (replace-old-page! content old-original-name new-name)]
                                                 (when-not (= content' content)
                                                   content'))
                                    properties (let [properties' (walk-replace-old-page! properties old-original-name new-name)]
                                                 (when-not (= properties' properties)
                                                   properties'))]
-                               (when (or title content properties)
+                               (when (or content properties)
                                  (util/remove-nils-non-nested
-                                  (merge
-                                   {:block/uuid       uuid
-                                    :block/content    content
-                                    :block/properties properties
-                                    :block/refs (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))
-                                    :block/path-refs (rename-update-block-refs! (:block/path-refs block) (:db/id page) (:db/id to-page))}
-                                   (block/parse-title-and-body format pre-block? content)))))) blocks)
+                                  {:block/uuid       uuid
+                                   :block/content    content
+                                   :block/properties properties
+                                   :block/refs (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page))
+                                   :block/path-refs (rename-update-block-refs! (:block/path-refs block) (:db/id page) (:db/id to-page))})))) blocks)
                       (remove nil?))]
     (db/transact! repo tx)
     (doseq [page-id page-ids]

+ 6 - 4
src/main/frontend/modules/file/core.cljs

@@ -6,8 +6,9 @@
             [frontend.db :as db]
             [frontend.db.utils :as db-utils]
             [frontend.state :as state]
-            [frontend.util :as util]
-            [frontend.debug :as debug]))
+            [frontend.util :as util :refer [profile]]
+            [frontend.debug :as debug]
+            [frontend.format.block :as block]))
 
 (defn- indented-block-content
   [content spaces-tabs]
@@ -23,8 +24,9 @@
         (ffirst body))))
 
 (defn transform-content
-  [{:block/keys [format pre-block? title content unordered body heading-level left page scheduled deadline parent] :as block} level {:keys [heading-to-list?]}]
-  (let [content (or content "")
+  [{:block/keys [format pre-block? unordered content heading-level left page scheduled deadline parent] :as block} level {:keys [heading-to-list?]}]
+  (let [{:block/keys [title body]} (block/parse-title-and-body format pre-block? content)
+        content (or content "")
         heading-with-title? (seq title)
         allowed-block-as-title? (allowed-block-as-title? title body)
         first-block? (= left page)

+ 1 - 11
src/main/frontend/modules/outliner/core.cljs

@@ -60,15 +60,6 @@
      (outliner-state/get-by-parent-id repo [:block/uuid id])
      (mapv block))))
 
-(defn- update-block-unordered
-  [block]
-  (let [parent (:block/parent block)
-        page (:block/page block)
-        type (:block/type block)]
-    (if (and parent page type (= parent page) (= type :heading))
-      (assoc block :block/unordered false)
-      (assoc block :block/unordered true))))
-
 (defn- block-with-timestamps
   [block]
   (let [updated-at (util/time-ms)
@@ -155,8 +146,7 @@
   (-save [this txs-state]
     (assert (ds/outliner-txs-state? txs-state)
             "db should be satisfied outliner-tx-state?")
-    (let [this (block (update-block-unordered (:data this)))
-          m (-> (:data this)
+    (let [m (-> (:data this)
                 (dissoc :block/children :block/meta :block/top? :block/bottom?)
                 (util/remove-nils))
           m (if (state/enable-block-timestamps?) (block-with-timestamps m) m)

+ 20 - 9
src/main/frontend/modules/shortcut/config.cljs

@@ -182,11 +182,13 @@
 
    :editor/up                      {:desc    "Move cursor up / Select up"
                                     :binding "up"
-                                    :fn      (editor-handler/shortcut-up-down :up)}
+                                    :fn      (editor-handler/shortcut-up-down :up)
+                                    :force?  true}
 
    :editor/down                    {:desc    "Move cursor down / Select down"
                                     :binding "down"
-                                    :fn      (editor-handler/shortcut-up-down :down)}
+                                    :fn      (editor-handler/shortcut-up-down :down)
+                                    :force?  true}
 
    :editor/left                    {:desc    "Move cursor left / Open selected block at beginning"
                                     :binding "left"
@@ -223,11 +225,13 @@
 
    :editor/expand-block-children   {:desc    "Expand"
                                     :binding "mod+down"
-                                    :fn      editor-handler/expand!}
+                                    :fn      editor-handler/expand!
+                                    :force?  true}
 
    :editor/collapse-block-children {:desc    "Collapse"
                                     :binding "mod+up"
-                                    :fn      editor-handler/collapse!}
+                                    :fn      editor-handler/collapse!
+                                    :force?  true}
 
    :editor/indent                  {:desc    "Indent block"
                                     :binding "tab"
@@ -312,7 +316,8 @@
 
    :command-palette/toggle         {:desc    "Toggle command palette"
                                     :binding "mod+shift+p"
-                                    :fn      (fn [] (state/toggle! :ui/command-palette-open?))}
+                                    :fn      (fn [] (state/toggle! :ui/command-palette-open?))
+                                    :force?   true}
 
    :command/run                    {:desc    "Run git command"
                                     :binding "mod+shift+1"
@@ -366,12 +371,16 @@
                                      :binding "t f"
                                      :fn      ui-handler/toggle-contents!}
 
+   :command/toggle-favorite         {:desc    "Add to/remove from favorites"
+                                     :binding "mod+shift+f"
+                                     :fn      page-handler/toggle-favorite!}
+
    :editor/open-file-in-default-app {:desc    "Open file in default app"
-                                     :binding "o f"
+                                     :binding nil
                                      :fn      page-handler/open-file-in-default-app}
 
    :editor/open-file-in-directory   {:desc    "Open file in parent directory"
-                                     :binding "o d"
+                                     :binding nil
                                      :fn      page-handler/open-file-in-directory}
 
    :ui/toggle-wide-mode             {:desc    "Toggle wide mode"
@@ -459,7 +468,8 @@
 
     :shortcut.handler/editor-global
     (->
-     (build-category-map [:editor/cycle-todo
+     (build-category-map [:command-palette/toggle
+                          :editor/cycle-todo
                           :editor/up
                           :editor/down
                           :editor/left
@@ -477,7 +487,8 @@
                           :editor/copy
                           :editor/cut
                           :editor/undo
-                          :editor/redo])
+                          :editor/redo
+                          :command/toggle-favorite])
      (with-meta {:before m/enable-when-not-component-editing!}))
 
     :shortcut.handler/global-prevent-default

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

@@ -18,10 +18,10 @@
 (def *pending (atom []))
 
 (def global-keys #js
-  [KeyCodes/TAB
-   KeyCodes/ENTER
-   KeyCodes/BACKSPACE KeyCodes/DELETE
-   KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
+                  [KeyCodes/TAB
+                   KeyCodes/ENTER
+                   KeyCodes/BACKSPACE KeyCodes/DELETE
+                   KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
 
 (def key-names (js->clj KeyNames))
 
@@ -109,7 +109,7 @@
 
     ;; register shortcuts
     (doseq [[id _] shortcut-map]
-      ;; (log/info :shortcut/install-shortcut {:id id :shortcut (dh/shortcut-binding id)})
+      ;;(log/info :shortcut/install-shortcut {:id id :shortcut (str (dh/shortcut-binding id))})
       (register-shortcut! handler id))
 
     (let [f (fn [e]
@@ -123,7 +123,7 @@
                        :dispatch-fn f
                        :handler    handler}}]
 
-      (events/listen handler EventType/SHORTCUT_TRIGGERED f)
+      (.listen handler EventType/SHORTCUT_TRIGGERED f)
 
       (when-not skip-installed?
         (swap! *installed merge data))

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

@@ -30,7 +30,7 @@
                       (get (get-bindings) id))]
     (cond
       (nil? shortcut)
-      (log/error :shortcut/binding-not-found {:id id})
+      (log/warn :shortcut/binding-not-found {:id id})
 
       (false? shortcut)
       (do
@@ -191,11 +191,10 @@
         data    (->> (vals @config/config)
                      (into  {})
                      id)]
-    (when binding
-      (assoc
-       data
-       :binding
-       (binding-for-display id binding)))))
+    (assoc
+      data
+      :binding
+      (binding-for-display id binding))))
 
 (defn shortcuts->commands [handler-id]
   (let [m (get @config/config handler-id)]

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

@@ -17,7 +17,9 @@
                    (ui/inject-document-devices-envs!)
                    (ui/inject-dynamic-style-node!)
                    (plugin-handler/host-mounted!)
-                   (let [teardown-fn (ui/setup-active-keystroke!)]
+                   (let [teardown-fn (comp
+                                      (ui/setup-active-keystroke!)
+                                      (ui/setup-patch-ios-visual-viewport-state!))]
                      (assoc state ::teardown teardown-fn)))
    :will-unmount (fn [state]
                    (let [teardown (::teardown state)]

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

@@ -82,6 +82,8 @@
       :ui/shortcut-tooltip? (if (false? (storage/get :ui/shortcut-tooltip?))
                               false
                               true)
+      :ui/visual-viewport-pending? false
+      :ui/visual-viewport-state nil
 
       :document/mode? document-mode?
 
@@ -1578,3 +1580,11 @@
 (defn get-last-key-code
   []
   (:editor/last-key-code @state))
+
+(defn set-visual-viewport-state
+  [input]
+  (set-state! :ui/visual-viewport-state input))
+
+(defn get-visual-viewport-state
+  []
+  (:ui/visual-viewport-state @state))

+ 4 - 2
src/main/frontend/tools/html_export.cljs

@@ -5,7 +5,8 @@
             [frontend.components.block :as block]
             [frontend.db :as db]
             [frontend.extensions.slide :as slide]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.format.block :as block]))
 
 ;; Consider generate a db index so that search can still works
 
@@ -16,7 +17,8 @@
 
 (defn- build-block
   [config block]
-  (let [body (:block/body block)
+  (let [block (block/parse-title-and-body block)
+        body (:block/body block)
         block (block/build-block-title config block)]
     [:div.block
      block

+ 44 - 42
src/main/frontend/ui.cljs

@@ -50,7 +50,6 @@
     :else
     0))
 
-
 (defonce icon-size (if (mobile-util/is-native-platform?) 23 20))
 
 (rum/defc ls-textarea
@@ -130,13 +129,14 @@
    (fn [{:keys [close-fn] :as state}]
      [:div.py-1.rounded-md.shadow-xs
       (when links-header links-header)
-      (for [{:keys [options title icon hr]} (if (fn? links) (links) links)]
+      (for [{:keys [options title icon hr hover-detail]} (if (fn? links) (links) links)]
         (let [new-options
-              (assoc options
-                     :on-click (fn [e]
-                                 (when-let [on-click-fn (:on-click options)]
-                                   (on-click-fn e))
-                                 (close-fn)))
+              (merge options
+                     {:title hover-detail
+                      :on-click (fn [e]
+                                  (when-let [on-click-fn (:on-click options)]
+                                    (on-click-fn e))
+                                  (close-fn))})
               child (if hr
                       nil
                       [:div
@@ -205,10 +205,10 @@
                 "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
                 :fill-rule "evenodd"}]]])]
       [:div.ui__notifications-content
-       {:style {:z-index (if (or (= state "exiting")
-                                 (= state "exited"))
-                           -1
-                           99)}}
+       {:style
+        (when (or (= state "exiting")
+                  (= state "exited"))
+                  {:z-index -1})}
        [:div.max-w-sm.w-full.shadow-lg.rounded-lg.pointer-events-auto.notification-area
         {:class (case state
                   "entering" "transition ease-out duration-300 transform opacity-0 translate-y-2 sm:translate-x-0"
@@ -304,35 +304,37 @@
         (.appendChild js/document.head node))
       style)))
 
-(defn setup-patch-ios-fixed-bottom-position!
-  "fix a common issue about ios webpage viewport
-   when soft keyboard setup"
+(defn setup-patch-ios-visual-viewport-state!
   []
-  (when (and
-         (util/ios?)
-         (not (nil? js/window.visualViewport)))
-    (let [viewport js/visualViewport
-          style (get-dynamic-style-node)
-          sheet (.-sheet style)
-          raf-pending? (atom false)
+  (when-let [^js vp (and (or (and (util/mobile?) (util/safari?))
+                             (mobile-util/native-ios?))
+                         js/window.visualViewport)]
+    (let [raf-pending? (atom false)
           set-raf-pending! #(reset! raf-pending? %)
-          handler
+          on-viewport-changed
           (fn []
-            (when-not @raf-pending?
-              (let [f (fn []
-                        (set-raf-pending! false)
-                        (let [vh (+ (.-offsetTop viewport) (.-height viewport))
-                              rule (.. sheet -rules (item 0))
-                              set-top #(set! (.. rule -style -top) (str % "px"))]
-                          (set-top vh)))]
-                (set-raf-pending! true)
-                (js/window.requestAnimationFrame f))))]
-      (.insertRule sheet ".fix-ios-fixed-bottom {bottom:unset !important; transform: translateY(-100%); top: 100vh;}")
-      (.addEventListener viewport "resize" handler)
-      (.addEventListener viewport "scroll" handler)
+            (let [update-vw-state
+                  (util/debounce 20
+                                 (fn []
+                                   (state/set-visual-viewport-state {:height     (.-height vp)
+                                                                     :page-top   (.-pageTop vp)
+                                                                     :offset-top (.-offsetTop vp)})
+                                   (state/set-state! :ui/visual-viewport-pending? false)))]
+              (when-not @raf-pending?
+                (let [f (fn []
+                          (set-raf-pending! false)
+                          (update-vw-state))]
+                  (set-raf-pending! true)
+                  (state/set-state! :ui/visual-viewport-pending? true)
+                  (js/window.requestAnimationFrame f)))))]
+
+      (.addEventListener vp "resize" on-viewport-changed)
+      (.addEventListener vp "scroll" on-viewport-changed)
+
       (fn []
-        (.removeEventListener viewport "resize" handler)
-        (.removeEventListener viewport "scroll" handler)))))
+        (.removeEventListener vp "resize" on-viewport-changed)
+        (.removeEventListener vp "scroll" on-viewport-changed)
+        (state/set-visual-viewport-state nil)))))
 
 (defn setup-system-theme-effect!
   []
@@ -470,14 +472,14 @@
                        (str/split  #" |\+"))
                    sequence)]
     [:span.keyboard-shortcut
-   (map-indexed (fn [i key]
-                  [:code {:key i}
+     (map-indexed (fn [i key]
+                    [:code {:key i}
                    ;; Display "cmd" rather than "meta" to the user to describe the Mac
                    ;; mod key, because that's what the Mac keyboards actually say.
-                   (if (or (= :meta key) (= "meta" key))
-                     (util/meta-key-name)
-                     (name key))])
-                sequence)]))
+                     (if (or (= :meta key) (= "meta" key))
+                       (util/meta-key-name)
+                       (name key))])
+                  sequence)]))
 
 (defn keyboard-shortcut-from-config [shortcut-name]
   (let [default-binding (:binding (get shortcut-config/all-default-keyboard-shortcuts shortcut-name))

+ 7 - 2
src/main/frontend/util.cljc

@@ -193,7 +193,7 @@
 
 (defn ext-of-image? [s]
   (some #(string/ends-with? s %)
-        [".png" ".jpg" ".jpeg" ".bmp" ".gif" ".webp"]))
+        [".png" ".jpg" ".jpeg" ".bmp" ".gif" ".webp" ".svg"]))
 
 ;; ".lg:absolute.lg:inset-y-0.lg:right-0.lg:w-1/2"
 (defn hiccup->class
@@ -1471,4 +1471,9 @@
            target-bottom (.-bottom (.getBoundingClientRect el))]
        (when (> (+ target-bottom (or (safe-parse-int offset) 0))
                 wrap-height)
-         (.scrollIntoView el #js {:block "center" :behavior "smooth"})))))
+         (.scrollIntoView el #js {:block "center" :behavior "smooth"})))))
+
+#?(:cljs
+   (defn sm-breakpoint?
+     []
+     (< (.-offsetWidth js/document.documentElement) 640)))

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

@@ -76,7 +76,7 @@
                             lines (concat [title] scheduled deadline before
                                           [(drawer-start typ)] middle [drawer-end] after)]
                         (string/join "\n" lines))
-                      
+
                       :else
                       content)]
         (string/trimr result))

+ 0 - 2
src/main/frontend/util/page_property.cljs

@@ -109,7 +109,6 @@
                      :block/left page-id
                      :block/parent page-id
                      :block/page page-id
-                     :block/title []
                      :block/content (if org?
                                       (str "#+" (string/upper-case (name key)) ": " value)
                                       (str (name key) ":: " value))
@@ -124,4 +123,3 @@
                              :data [block]})
           (ui-handler/re-render-root!)))
       (outliner-file/sync-to-file page-id))))
-

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.5.1")
+(defonce version "0.5.2")

+ 48 - 0
src/main/logseq/api.cljs

@@ -57,6 +57,15 @@
         (js/console.error "[parse hiccup error]" e) input))))
 
 ;; base
+(defn ^:export get_state_from_store
+  [^js path]
+  (when-let [path (if (string? path) [path] (bean/->clj path))]
+    (->> path
+         (map #(if (string/starts-with? % "@")
+                 (subs % 1)
+                 (keyword %)))
+         (get-in @state/state))))
+
 (def ^:export get_user_configs
   (fn []
     (bean/->js
@@ -233,6 +242,7 @@
   (fn [pid ^js cmd-action palette?]
     (when-let [[cmd action] (bean/->clj cmd-action)]
       (let [action (assoc action 0 (keyword (first action)))
+            cmd (assoc cmd :key (string/replace (:key cmd) ":" "-"))
             key (:key cmd)
             keybinding (:keybinding cmd)
             palette-cmd (and palette? (plugin-handler/simple-cmd->palette-cmd pid cmd action))]
@@ -288,6 +298,15 @@
     (when (re-find #"https?://" url)
       (js/apis.openExternal url))))
 
+(def ^:export invoke_external_command
+  (fn [type & args]
+    (when-let [id (and (string/starts-with? type "logseq.")
+                       (-> (string/replace type #"^logseq." "")
+                           (util/safe-lower-case)
+                           (keyword)))]
+      (when-let [action (get-in (palette-handler/get-commands-unique) [id :action])]
+        (apply action args)))))
+
 ;; flag - boolean | 'toggle'
 (def ^:export set_left_sidebar_visible
   (fn [flag]
@@ -352,10 +371,22 @@
 (def ^:export get_current_block
   (fn []
     (let [block (state/get-edit-block)
+          block (or block (some-> (first (:selection/blocks @state/state))
+                                  (.getAttribute "blockid")
+                                  (db-model/get-block-by-uuid)))
           block (or block (state/get-last-edit-block))
           block (and block (db-utils/pull (:db/id block)))]
       (bean/->js (normalize-keyword-for-json block)))))
 
+(def ^:export get_selected_blocks
+  (fn []
+    (when-let [blocks (and (state/in-selection-mode?)
+                           (seq (:selection/blocks @state/state)))]
+      (let [blocks (->> blocks
+                        (map (fn [^js el] (some-> (.getAttribute el "blockid")
+                                                  (db-model/query-block-by-uuid)))))]
+        (bean/->js (normalize-keyword-for-json blocks))))))
+
 (def ^:export get_current_page
   (fn []
     (when-let [page (state/get-current-page)]
@@ -496,6 +527,16 @@
       (when-let [right-siblings (outliner/get-right-siblings (outliner/->Block block))]
         (bean/->js (normalize-keyword-for-json (:data (first right-siblings))))))))
 
+(def ^:export set_block_collapsed
+  (fn [uuid ^js opts]
+    (when-let [block (db-model/get-block-by-uuid uuid)]
+      (let [{:keys [flag]} (bean/->clj opts)
+            flag (if (= "toggle" flag)
+                   (not (-> block :block/properties :collapsed))
+                   (boolean flag))]
+        (if flag (editor-handler/collapse-block! uuid)
+                 (editor-handler/expand-block! uuid))))))
+
 (def ^:export upsert_block_property
   (fn [block-uuid key value]
     (editor-handler/set-block-property! (medley/uuid block-uuid) key value)))
@@ -589,3 +630,10 @@
   []
   (p/let [_ (el/persist-dbs!)
           _ (reset! handler/triggered? true)]))
+
+(defn ^:export __debug_state
+  [path]
+  (-> (if (string? path)
+        (get @state/state (keyword path))
+        @state/state)
+      (bean/->js)))

+ 1 - 1
src/test/frontend/handler/block_test.cljs

@@ -5,5 +5,5 @@
 (comment
   (defn clip-block [x]
     (map #(select-keys % [:block/parent :block/left :block/pre-block? :block/uuid :block/level
-                          :block/title :db/id])
+                          :db/id])
       x)))

+ 1 - 5
src/test/frontend/modules/outliner/ds_test.cljs

@@ -15,12 +15,10 @@
                                   :block/refs (),
                                   :block/anchor "level_2123123",
                                   :block/repo "logseq_local_test_navtive_fs",
-                                  :block/body [],
                                   :block/meta {:timestamps [], :properties [], :start-pos 0, :end-pos 15},
                                   :block/format :markdown,
                                   :block/level 1,
                                   :block/tags [],
-                                  :block/title [["Plain" "level 2123123"]],
                                   :block/refs-with-children (),
                                   :block/content "level test",
                                   :db/id 72,
@@ -29,14 +27,12 @@
         rt [[72 :block/uuid #uuid "606c1962-ad7f-424e-b120-0dc7fcb25415" 536870913 true]
             [72 :block/anchor "level_2123123" 536870913 true]
             [72 :block/repo "logseq_local_test_navtive_fs" 536870913 true]
-            [72 :block/body [] 536870913 true]
             [72 :block/meta {:timestamps [], :properties [], :start-pos 0, :end-pos 15} 536870913 true]
             [72 :block/format :markdown 536870913 true]
             [72 :block/level 1 536870913 true]
-            [72 :block/title [["Plain" "level 2123123"]] 536870913 true]
             [72 :block/refs-with-children () 536870913 true]
             [72 :block/content "level test" 536870913 true]]]
     (is (= rt (mapv vec (:tx-data db-report))))))
 
 (comment
-  (run-test test-with-db-macro))
+  (run-test test-with-db-macro))

Some files were not shown because too many files changed in this diff