Compare commits

...

93 Commits

Author SHA1 Message Date
Ammar Ahmed
aa7f14ae79 mobile: fix email confirmed description 2025-08-28 10:12:32 +05:00
Ammar Ahmed
874352e753 Merge pull request #8400 from streetwriters/mobile-fix-open-note-notification
Fix opening pinned note from notification on android
2025-08-20 08:49:55 +05:00
Ammar Ahmed
49d1ab4be5 Merge pull request #8401 from streetwriters/mobile-fix-notebook-tag-sorting-filter
Fix filtered tags and notebooks not sorted correctly
2025-08-20 08:49:21 +05:00
Ammar Ahmed
65a80dbd46 Merge pull request #8402 from streetwriters/mobile-fix-back-navigation
Fix back navigation on first app launch by resetting routes
2025-08-20 08:48:40 +05:00
Ammar Ahmed
e55456f492 Merge pull request #8467 from streetwriters/fix-cache-clear-error
mobile: fix error when clearing cache on android
2025-08-20 08:48:01 +05:00
Ammar Ahmed
3f9fce0d34 Merge pull request #8482 from streetwriters/fix-open-share-exported-notes-ios
mobile: fix sharing and opening exported notes on iOS
2025-08-20 08:47:21 +05:00
Ammar Ahmed
2ae094cf1f mobile: fix sharing and opening exported notes 2025-08-19 12:51:16 +05:00
Abdullah Atta
bc224f1674 Merge pull request #8466 from 01zulfi/web/list-pane-max-size
web: increase list pane max size to 1000 for large screens
2025-08-19 09:52:28 +05:00
01zulfi
8bb1582cca web: increase list pane max size to 1000
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-08-18 15:48:59 +05:00
Ammar Ahmed
d02125a93c mobile: fix error when clearing cache on android 2025-08-18 13:15:45 +05:00
Abdullah Atta
d91ce4e936 Merge pull request #8389 from 01zulfi/common/update-keybindings
common: update keybindings for editor search, link, internal link, and command palette
2025-08-13 13:30:39 +05:00
Abdullah Atta
deb902549b Merge pull request #8392 from 01zulfi/web/fix-open-settings-shortcut
web: fix open settings shortcut interfering with editor shortcut
2025-08-13 13:29:34 +05:00
Abdullah Atta
e41ed07805 Merge pull request #8385 from Scott-will/master
Handle pasting urls in html
2025-08-13 13:27:14 +05:00
Scott-will
f6e8dc0a88 editor: fix pasting html links with titles that look like links
Signed-off-by: Scott Williams scottwill1999@gmail.com

editor: update tests for html handling

Signed-off-by: Scott Williams scottwill1999@gmail.com

editor: add logic for markdown links

Signed-off-by: Scott Williams scottwill1999@gmail.com

editor: remove plugin in favour of markPasteRule

Signed-off-by: Scott Williams scottwill1999@gmail.com
2025-08-13 13:26:01 +05:00
Abdullah Atta
044dd5ce59 Merge pull request #8391 from 01zulfi/docs/update-desktop-instructions
docs: add missing instruction in desktop app dev setup
2025-08-13 12:21:12 +05:00
Ammar Ahmed
b24830666c Merge pull request #8409 from streetwriters/fix-fdroid-build
mobile: fix fdroid build
2025-07-31 09:10:13 +05:00
Ammar Ahmed
470802c3e2 mobile: fix fdroid build 2025-07-31 09:09:38 +05:00
Ammar Ahmed
7e0e1eac93 Merge pull request #8406 from streetwriters/mobile-fix-fdroid-build
mobile: compile lazysodium from source
2025-07-29 15:05:39 +05:00
Ammar Ahmed
58cbf653b2 mobile: compile lazysodium from source 2025-07-29 15:04:49 +05:00
Ammar Ahmed
4b35f2ecd2 mobile: fix back navigation on first app launch 2025-07-28 14:20:11 +05:00
Ammar Ahmed
48d9a863ec mobile: fix filtered tags and notebooks not sorted correctly 2025-07-28 13:42:46 +05:00
Ammar Ahmed
1d616cf68f mobile: fix opening note from notification on android 2025-07-28 11:56:54 +05:00
Ammar Ahmed
cdf6a4bb5b Merge pull request #8398 from streetwriters/mobile-release-3.2.10-fix
mobile: release v3.2.10
2025-07-26 12:48:28 +05:00
Ammar Ahmed
1b3735e2b2 mobile: release v3.2.10 2025-07-26 12:19:13 +05:00
Ammar Ahmed
850fc6b066 Merge pull request #8397 from streetwriters/mobile-release-3.2.9
mobile: release v3.2.9
2025-07-26 11:58:20 +05:00
Ammar Ahmed
8a62070c8e Merge pull request #8394 from streetwriters/mobile-editor-ux-improvement
Mobile editor ux improvement
2025-07-26 10:52:01 +05:00
Ammar Ahmed
8aa31b8380 mobile: fix exiting table not working 2025-07-25 14:24:14 +05:00
Ammar Ahmed
f445a963f9 mobile: improve ux when clicking empty area in editor 2025-07-25 12:44:55 +05:00
Ammar Ahmed
d6e3f27612 mobile: add scroll to top and bottom in editor 2025-07-25 12:44:34 +05:00
Ammar Ahmed
4688d74edb mobile: improvements to editor ux
1. Tapping on empty in editor opens the keyboard
2. Swiping to open the editor when editor is empty opens the keyboard automatically
2025-07-25 11:57:44 +05:00
Ammar Ahmed
1985007039 Merge pull request #8393 from streetwriters/mobile-fix-edit-notebook
mobile: fix edit notebook button causes app freeze on iOS
2025-07-25 11:01:03 +05:00
Ammar Ahmed
039c8faabe mobile: fix edit notebook button causes app freeze 2025-07-25 10:57:14 +05:00
01zulfi
fb1eb234b6 web: fix open settings shortcut interfering with editor shortcut
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-25 08:25:01 +05:00
01zulfi
7e70cd96b9 docs: add missing instruction in desktop app dev setup
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-24 15:56:17 +05:00
01zulfi
f9a89783ed common: update keybindings for editor search, link, internal link, and command palette
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-23 09:45:12 +05:00
Abdullah Atta
301ac781d6 Merge pull request #8387 from 01zulfi/docs/keyboard-shortcuts-heading
docs: add keyboard shortcuts heading
2025-07-23 08:31:41 +05:00
01zulfi
ce3c98ae9b docs: add keyboard shortcuts heading
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 16:28:07 +05:00
Abdullah Atta
2447b3bea7 Merge pull request #7655 from 01zulfi/global/keyboard
global: keyboard shortcuts documentation && web dialog
2025-07-22 14:21:30 +05:00
Abdullah Atta
63888ff70e web: improve keyboard shortcuts dialog UI 2025-07-22 14:20:15 +05:00
Abdullah Atta
c6e3d1bd47 common: New note should be in the General category 2025-07-22 14:20:15 +05:00
Abdullah Atta
e03e02d702 common: add license header to keybindings.ts 2025-07-22 14:20:15 +05:00
Abdullah Atta
1abf51ae2b common: getGroupedKeybindings should return an array
this is so the groups are always ordered. It also makes it easier to iterate
over the groups.
2025-07-22 14:20:15 +05:00
Abdullah Atta
c41dfa3185 common: letter keys should be upper cased
this is common in a lot of places and it provides better readability as well.
2025-07-22 14:20:15 +05:00
01zulfi
52a07da230 editor: remove unneeded code
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:15 +05:00
01zulfi
f572bb1637 editor: fix insertParagraph keys
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:15 +05:00
01zulfi
773ede4b6c common: improve key formatting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:15 +05:00
01zulfi
e4d42c8bac web: add command to show keyboard shortcuts in command palette
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
5b85067e15 web: improve contrast of keys text in keyboard shortcuts dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
0d111f3a79 web: use keys from common/keybindings in command palette dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
f6b6f2b4b7 web: support mac key formatting in keyboard shortcuts dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
d55a2aa04d common: support mac keyboard formatting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
0185eea740 editor: remove hardcoded keybind definitions
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
d44e8874b8 docs: move keyboard shortcuts script to docs/help
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
e34d33dd61 web: update key maps registeration
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
4b2fb80c23 global: add keyboard shortcuts
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

global: (wip) move keyboard shortcuts metadata into common package
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

common: refactor keybindings.ts
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

global: (wip) more refactoring
* minor refactor dialog
* refactor logic out from script file to keybindings file

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

common: filter keybindings based on platform && minor ui changes

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

editor: use keyboard keys from common package
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: remove unrelated title-box change
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

editor: add TODO:remove comments
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:19:46 +05:00
Abdullah Atta
3c908ac079 Merge pull request #8384 from streetwriters/themebuilder/release/1.4.0
Update theme builder to new UI
2025-07-22 09:08:02 +05:00
Abdullah Atta
563f068948 theme-builder: bump version to 1.4.0 2025-07-22 09:03:09 +05:00
Abdullah Atta
1344b958cb Merge pull request #8382 from 01zulfi/web/note-linking-dialog
web: migrate note linking dialog lists from tanstack virtual to virtuoso
2025-07-22 08:41:38 +05:00
01zulfi
32d7cab70c web: migrate note linking dialog lists from tanstack virtual to virtuoso
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-21 14:49:41 +05:00
Abdullah Atta
1293fa2dec Merge pull request #8365 from 01zulfi/web/fix-nav-pane-on-small-screens
web: fix nav pane on small screens
2025-07-21 12:23:50 +05:00
Abdullah Atta
8542cbf37e Merge pull request #8381 from streetwriters/fix/email-should-not-be-required-session-expiry
Email should not be required on session expiry
2025-07-21 12:19:42 +05:00
Abdullah Atta
62c6fba9a0 web: email should not be required on session expiry 2025-07-21 12:10:43 +05:00
Abdullah Atta
2d84154226 web: show hamburger menu button on mobile 2025-07-21 12:09:58 +05:00
01zulfi
c4e306d569 web: fix nav pane on small screens
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-21 11:56:45 +05:00
Abdullah Atta
e86f7491ef Merge pull request #8380 from streetwriters/ci/run-tests-on-pr-ref
Run tests on pr ref instead of base branch
2025-07-21 11:47:27 +05:00
Abdullah Atta
5151d83af9 ci: run tests on pr ref instead of base branch 2025-07-21 11:38:30 +05:00
Abdullah Atta
e462cb0596 Merge pull request #8379 from streetwriters/docs/remove-onenote-sn-docs
Remove standard notes and one note importer docs
2025-07-21 11:30:52 +05:00
Abdullah Atta
a30c0e4608 docs(help): remove standard notes and one note importer docs 2025-07-21 11:30:32 +05:00
Abdullah Atta
f90e7982f7 Merge pull request #8378 from streetwriters/ci/migrate-to-pr-target
Fix tests from PRs opened from external forks
2025-07-21 11:28:11 +05:00
Abdullah Atta
40c69d7cba ci: use pull_request_target with authorization hook where required 2025-07-21 11:26:03 +05:00
Abdullah Atta
23ee50f688 Merge pull request #8372 from streetwriters/fix/web-tests
Fix web E2E tests
2025-07-21 09:53:05 +05:00
Abdullah Atta
a7c857fb71 desktop: increase hookTimeout ton 120 seconds 2025-07-21 09:35:39 +05:00
Abdullah Atta
0f6622360c desktop: ignore error on failure to remove test artifacts 2025-07-21 09:28:26 +05:00
Abdullah Atta
578bc6e546 desktop: build app only once during tests 2025-07-21 09:19:53 +05:00
Abdullah Atta
70fd038207 desktop: add build retrying when running tests 2025-07-21 08:57:16 +05:00
Abdullah Atta
b30110a48a desktop: add support for running tests in parallel
we do this by copying the build directory instead of building it over and over
again for each test. This also makes the tests super fast.
2025-07-19 15:05:24 +05:00
Abdullah Atta
21978c4c1d desktop: patch better-sqlite3-multiple-ciphers on each test build 2025-07-19 12:32:22 +05:00
Abdullah Atta
ec48f2350d desktop: run tests sequentially
this is required since each test builds a new app and on linux & windows that
can cause issues if done in parallel
2025-07-19 11:02:32 +05:00
Abdullah Atta
fe1e3bb847 desktop: migrate tests to vitest 3 2025-07-19 11:01:57 +05:00
Abdullah Atta
69776f26d8 desktop: upgrade to vitest 3 2025-07-19 11:01:42 +05:00
Abdullah Atta
ab4092c088 desktop: fix dialog skipping in tests 2025-07-19 09:43:39 +05:00
Abdullah Atta
4948a8d7de desktop: add test artifacts cleanup on test finish 2025-07-19 09:43:23 +05:00
Abdullah Atta
0a607bce18 web: increase app lock test timeouts 2025-07-19 08:51:29 +05:00
Abdullah Atta
e59820e3d6 web: fix adding a one-time reminder before current time should not be possible 2025-07-19 08:50:22 +05:00
Abdullah Atta
85a2d25c1b web: wait for reminder dialog to close before proceeding in tests 2025-07-18 15:06:59 +05:00
Abdullah Atta
3bc7fc84f9 web: fix `if note is active in multiple tabs, moving the note to trash should
close those tabs` test
2025-07-18 14:45:06 +05:00
Abdullah Atta
249ce1ecbb web: remove new line at eof in test snapshots 2025-07-18 13:02:03 +05:00
Abdullah Atta
be7f8d9916 ci: run tests based on affected files on pr open 2025-07-18 12:59:04 +05:00
Abdullah Atta
0a0495f1a7 config: include editor/src/extensions/code-block/languages to build output 2025-07-18 12:51:11 +05:00
Abdullah Atta
2f7afa8556 web: update readonly edited note snapshots for linux & windows 2025-07-18 12:44:45 +05:00
Abdullah Atta
a6d0177153 Merge pull request #8366 from Scott-will/master
editor: maintain relative cursor location when list is sorted
2025-07-18 12:29:13 +05:00
Scott-will
b9e24888e7 editor: maintain relative cursor location when list is sorted
Signed-off-by: Scott Williams scottwill1999@gmail.com
2025-07-16 14:01:40 -07:00
Ammar Ahmed
765f3a476d mobile: release v3.2.9 2025-07-16 11:26:19 +05:00
93 changed files with 3386 additions and 1147 deletions

View File

@@ -9,7 +9,13 @@ on:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
pull_request:
pull_request_target:
branches:
- "master"
paths:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
types:
- "ready_for_review"
- "opened"
@@ -17,11 +23,21 @@ on:
- "reopened"
jobs:
test:
runs-on: ubuntu-22.04
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
test:
needs: authorize
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -10,6 +10,12 @@ on:
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
pull_request:
branches:
- "master"
paths:
- "app/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
jobs:
build:

View File

@@ -10,6 +10,12 @@ on:
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
pull_request:
branches:
- "master"
paths:
- "packages/editor/**"
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
types:
- "ready_for_review"
- "opened"

View File

@@ -9,7 +9,13 @@ on:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.tests.yml"
pull_request:
pull_request_target:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.tests.yml"
types:
- "ready_for_review"
- "opened"
@@ -17,12 +23,23 @@ on:
- "reopened"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -54,6 +71,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Download build
uses: actions/download-artifact@v4

View File

@@ -24,7 +24,7 @@ Requirements:
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
Once you have completed the setup, the first step is to `clone` the monorepo:
1. `clone` the monorepo:
```bash
git clone https://github.com/streetwriters/notesnook.git
@@ -33,19 +33,28 @@ git clone https://github.com/streetwriters/notesnook.git
cd notesnook
```
Once you are inside the `./notesnook` directory, run the preparation step:
2. Install dependencies:
```bash
# this might take a while to complete
npm install
```
Now you can finally start the desktop app for development:
3. Run the webapp for desktop environment:
```bash
cd apps/web
npm run start:desktop
```
4. In a separate terminal session, run the desktop app from the root of the project:
```bash
npm run start:desktop
```
### Release mode
To run the app in release mode:
```bash

View File

@@ -17,230 +17,226 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test } from "vitest";
import { harness } from "./utils.js";
import { testCleanup, test } from "./test-override.js";
import { writeFile } from "fs/promises";
import { Page } from "playwright";
import { gt, lt } from "semver";
import { describe } from "vitest";
test("update starts downloading if version is outdated", async (t) => {
await harness(
t,
async ({ page }) => {
await page.waitForSelector("#authForm");
test("update starts downloading if version is outdated", async ({
ctx: { page },
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /updating/i })
.waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /updating/i })
.waitFor({ state: "attached" });
});
test("update is only shown if version is outdated and auto updates are disabled", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false
})
);
test("update is only shown if version is outdated and auto updates are disabled", async ({
ctx,
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
await ctx.relaunch();
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false
})
);
const { page } = ctx;
await ctx.relaunch();
await page.waitForSelector("#authForm");
const { page } = ctx;
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page.waitForSelector("#authForm");
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await skipDialog(page);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.waitFor({ state: "attached" });
});
describe("update to stable if it is newer", () => {
test.scoped({ options: { version: "3.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
expect(gt(version, "3.0.0-beta.0")).toBe(true);
});
});
describe("update is not available if it latest stable version is older", () => {
test.scoped({ options: { version: "99.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
expect(
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
.isHidden()
).toBe(true);
});
});
test("update to stable if it is newer", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
describe("downgrade to stable on switching to stable release track", () => {
test.scoped({ options: { version: "99.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.relaunch();
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "stable"
})
);
const { page } = ctx;
await ctx.relaunch();
await page.waitForSelector("#authForm");
const { page } = ctx;
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page.waitForSelector("#authForm");
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await skipDialog(page);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
await skipDialog(page);
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect(gt(version, "3.0.0-beta.0")).toBe(true);
},
{ version: "3.0.0-beta.0" }
);
});
await page.waitForSelector(".ProseMirror");
test("update is not available if it latest stable version is older", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
t.expect(
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.isHidden()
).toBe(true);
},
{ version: "99.0.0-beta.0" }
);
});
test("downgrade to stable on switching to stable release track", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "stable"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect(lt(version, "99.0.0-beta.0")).toBe(true);
},
{ version: "99.0.0-beta.0" }
);
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
expect(lt(version, "99.0.0-beta.0")).toBe(true);
});
});
async function skipDialog(page: Page) {
await page
.waitForSelector(".ReactModal__Content", {
timeout: 1000
})
.catch(() => {})
.then(async () => {
const positiveButton = page.locator(
"button[data-role='positive-button']"
);
const negativeButton = page.locator(
"button[data-role='negative-button']"
);
if (await positiveButton.isVisible()) await positiveButton.click();
else if (await negativeButton.isVisible()) await negativeButton.click();
});
try {
const dialog = page.locator(".ReactModal__Content");
const positiveButton = dialog.locator(
"button[data-role='positive-button']"
);
const negativeButton = dialog.locator(
"button[data-role='negative-button']"
);
if (await positiveButton.isVisible())
await positiveButton.click({ timeout: 1000 });
else if (await negativeButton.isVisible())
await negativeButton.click({ timeout: 1000 });
} catch (e) {
// ignore error
}
}

View File

@@ -0,0 +1,24 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { buildApp } from "./utils";
export default async function setup() {
await buildApp();
}

View File

@@ -17,22 +17,24 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test } from "vitest";
import { harness } from "./utils.js";
import assert from "assert";
import { testCleanup, test } from "./test-override.js";
test("make sure app loads", async (t) => {
await harness(t, async ({ page }) => {
await page.waitForSelector("#authForm");
test("make sure app loads", async ({
ctx: { page },
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
assert.ok(
await page.getByRole("button", { name: "Create account" }).isVisible()
);
await page.waitForSelector("#authForm");
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page.waitForSelector(".ProseMirror");
});
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
});

View File

@@ -0,0 +1,64 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test as vitestTest, TestContext } from "vitest";
import { buildAndLaunchApp, Fixtures, TestOptions } from "./utils";
import { mkdir, rm } from "fs/promises";
import path from "path";
import slugify from "slugify";
export const test = vitestTest.extend<Fixtures>({
options: { version: "3.0.0" } as TestOptions,
ctx: async ({ options }, use) => {
const ctx = await buildAndLaunchApp(options);
await use(ctx);
}
});
export async function testCleanup(context: TestContext) {
const ctx = (context.task.context as unknown as Fixtures).ctx;
if (context.task.result?.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(context.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
await rm(ctx.userDataDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
await rm(ctx.outputDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
}

View File

@@ -18,146 +18,235 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { execSync } from "child_process";
import { mkdir } from "fs/promises";
import { cp, readFile, writeFile } from "fs/promises";
import { fileURLToPath } from "node:url";
import path from "path";
import path, { join, resolve } from "path";
import { _electron as electron } from "playwright";
import slugify from "slugify";
import { TaskContext } from "vitest";
import { existsSync } from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
const productName = `NotesnookTestHarness`;
const SOURCE_DIR = resolve("output", productName);
interface AppContext {
export interface AppContext {
app: import("playwright").ElectronApplication;
page: import("playwright").Page;
configPath: string;
userDataDir: string;
outputDir: string;
relaunch: () => Promise<void>;
}
interface TestOptions {
export interface TestOptions {
version: string;
}
export async function harness(
t: TaskContext,
cb: (ctx: AppContext) => Promise<void>,
options?: TestOptions
) {
const ctx = await buildAndLaunchApp(options);
t.onTestFinished(async (result) => {
if (result.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(t.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
});
await cb(ctx);
export interface Fixtures {
options: TestOptions;
ctx: AppContext;
}
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> {
const productName = makeid(10);
const executablePath = await buildApp({ ...options, productName });
const { app, page, configPath } = await launchApp(executablePath);
export async function buildAndLaunchApp(
options?: TestOptions
): Promise<AppContext> {
const productName = `notesnooktest${makeid(10)}`;
const outputDir = path.join("test-artifacts", `${productName}-output`);
const executablePath = await copyBuild({
...options,
outputDir
});
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName
);
const ctx: AppContext = {
app,
page,
configPath,
userDataDir,
outputDir,
relaunch: async () => {
const { app, page, configPath } = await launchApp(executablePath);
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName
);
ctx.app = app;
ctx.page = page;
ctx.userDataDir = userDataDir;
ctx.configPath = configPath;
}
};
return ctx;
}
async function launchApp(executablePath: string) {
async function launchApp(executablePath: string, packageName: string) {
const userDataDir = resolve(
__dirname,
"..",
"test-artifacts",
"user_data_dirs",
packageName
);
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
env:
process.platform === "linux"
env: {
...(process.platform === "linux"
? {
...(process.env as Record<string, string>),
APPIMAGE: "true"
}
: (process.env as Record<string, string>)
: (process.env as Record<string, string>)),
CUSTOM_USER_DATA_DIR: userDataDir
}
});
const page = await app.firstWindow();
const userDataDirectory = await app.evaluate((a) => {
return a.app.getPath("userData");
});
const configPath = path.join(userDataDirectory, "config.json");
const configPath = path.join(userDataDir, "UserData", "config.json");
return {
app,
page,
configPath
configPath,
userDataDir
};
}
async function buildApp({
let MAX_RETRIES = 3;
export async function buildApp(version?: string) {
if (!existsSync(SOURCE_DIR)) {
const args = [
"electron-builder",
"--dir",
`--${process.arch}`,
`--config electron-builder.config.js`,
`--c.extraMetadata.productName=${productName}`,
`--c.compression=store`,
"--publish=never"
];
if (version) args.push(`--c.extraMetadata.version=${version}`);
try {
execSync(`npx ${args.join(" ")}`, {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: SOURCE_DIR
}
});
} catch (e) {
if (--MAX_RETRIES) {
console.log("retrying...");
return await buildApp(version);
} else throw e;
}
}
}
async function copyBuild({
version,
productName
outputDir
}: {
version?: string;
productName: string;
outputDir: string;
}) {
const buildRoot = path.join("test-artifacts", `${productName}-build`);
const output = path.join("test-artifacts", `${productName}-output`);
execSync(`npm run release -- --root ${buildRoot} --skip-tsc-build`, {
stdio: IS_DEBUG ? "inherit" : "ignore"
});
return process.platform === "win32"
? await makeBuildCopyWindows(outputDir, productName, version)
: process.platform === "darwin"
? await makeBuildCopyMacOS(outputDir, productName, version)
: await makeBuildCopyLinux(outputDir, productName, version);
}
const args = [
`--config electron-builder.config.js`,
`--c.extraMetadata.productName=${productName}`,
"--publish=never"
];
if (version) args.push(`--c.extraMetadata.version=${version}`);
execSync(`npx electron-builder --dir --${process.arch} ${args.join(" ")}`, {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
NOTESNOOK_STAGING: "true",
NN_BUILD_ROOT: buildRoot,
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: output
}
});
return path.join(
async function makeBuildCopyLinux(
outputDir: string,
productName: string,
version?: string
) {
const platformDir =
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
const appDir = await makeBuildCopy(
outputDir,
platformDir,
"resources",
version
);
return resolve(
__dirname,
"..",
output,
process.platform === "linux"
? process.arch === "arm64"
? "linux-arm64-unpacked"
: "linux-unpacked"
: process.platform === "darwin"
? process.arch === "arm64"
? `mac-arm64/${productName}.app/Contents/MacOS/`
: `mac/${productName}.app/Contents/MacOS/`
: "win-unpacked",
process.platform === "win32" ? `${productName}.exe` : productName
appDir,
productName.toLowerCase().replace(/\s+/g, "-")
);
}
async function makeBuildCopyWindows(
outputDir: string,
productName: string,
version?: string
) {
const platformDir =
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
const appDir = await makeBuildCopy(
outputDir,
platformDir,
"resources",
version
);
return resolve(__dirname, "..", appDir, `${productName}.exe`);
}
async function makeBuildCopyMacOS(
outputDir: string,
productName: string,
version?: string
) {
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
const appDir = await makeBuildCopy(
outputDir,
platformDir,
join(`${productName}.app`, "Contents", "Resources"),
version
);
return resolve(
__dirname,
"..",
appDir,
`${productName}.app`,
"Contents",
"MacOS",
productName
);
}
async function makeBuildCopy(
outputDir: string,
platformDir: string,
resourcesDir: string,
version?: string
) {
const appDir = outputDir;
await cp(join(SOURCE_DIR, platformDir), outputDir, {
recursive: true,
preserveTimestamps: true,
verbatimSymlinks: true,
dereference: false,
force: true
});
const packageJsonPath = join(appDir, resourcesDir, "app", "package.json");
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
if (version) {
packageJson.version = version;
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
return appDir;
}
function makeid(length: number) {
let result = "";
const characters =

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,9 @@
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": false,
"sideEffects": [
"src/overrides.ts"
],
"exports": {
".": {
"require": {
@@ -55,7 +57,7 @@
"slugify": "1.6.6",
"tree-kill": "^1.2.2",
"undici": "^7.8.0",
"vitest": "2.1.8"
"vitest": "^3.2.4"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./overrides";
import { app, BrowserWindow, nativeTheme, shell } from "electron";
import { isDevelopment } from "./utils";
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";

View File

@@ -0,0 +1,35 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { app } from "electron";
import path from "path";
if (process.env.CUSTOM_USER_DATA_DIR) {
app.setPath(
"appData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
);
app.setPath(
"userData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
);
app.setPath(
"documents",
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
);
}

View File

@@ -21,11 +21,13 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
testTimeout: process.env.CI ? 120 * 1000 : 120 * 1000,
testTimeout: 120 * 1000,
hookTimeout: 120 * 1000,
sequence: {
concurrent: true,
shuffle: true
},
globalSetup: "./__tests__/global-setup.ts",
dir: "./__tests__/",
exclude: [
"**/node_modules/**",

View File

@@ -137,6 +137,7 @@ export async function deleteFile(
export async function clearFileStorage() {
try {
await createCacheDir();
const files = await RNFetchBlob.fs.ls(cacheDir);
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
@@ -221,6 +222,7 @@ export async function deleteCacheFileByName(name: string) {
}
export async function deleteDCacheFiles() {
await createCacheDir();
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (file.includes("_dcache") || file.startsWith("NN_")) {

View File

@@ -33,6 +33,9 @@ export function hideAuth() {
eSendEvent(eCloseLoginDialog);
if (initialAuthMode.current === AuthMode.welcomeSignup) {
Navigation.replace("FluidPanelsView");
setTimeout(() => {
Navigation.resetRootState();
}, 300);
} else {
Navigation.goBack();
}

View File

@@ -89,6 +89,9 @@ const Auth = ({ navigation, route }) => {
onPress={() => {
if (initialAuthMode.current === 2) {
Navigation.replace("FluidPanelsView");
setTimeout(() => {
Navigation.resetRootState();
}, 300);
} else {
Navigation.goBack();
}

View File

@@ -30,6 +30,7 @@ import { BackHandler, Platform, ViewProps } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
runOnJS,
SharedValue,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
@@ -67,6 +68,7 @@ export interface TabsRef {
setScrollEnabled: () => true;
isDrawerOpen: () => boolean;
node: RefObject<Animated.View>;
tabChangedFromSwipeAction: SharedValue<boolean>;
}
export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
@@ -116,6 +118,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
const isLoaded = useRef(false);
const prevWidths = useRef(widths);
const isIPhone = Platform.OS === "ios";
const tabChangedFromSwipeAction = useSharedValue(false);
useEffect(() => {
if (deviceMode === "tablet" || fullscreen) {
@@ -191,6 +194,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
: withTiming(editorPosition);
currentTab.value = 2;
}
tabChangedFromSwipeAction.value = false;
},
goToIndex: (index: number, animated = true) => {
if (deviceMode === "tablet") {
@@ -210,6 +214,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
: editorPosition;
currentTab.value = 2;
}
tabChangedFromSwipeAction.value = false;
},
unlock: () => {
forcedLock.value = false;
@@ -252,7 +257,8 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
},
page: () => (currentTab.value === 1 ? "home" : "editor"),
setScrollEnabled: () => true,
node: node
node: node,
tabChangedFromSwipeAction: tabChangedFromSwipeAction
}),
[
currentTab,
@@ -262,7 +268,8 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
homePosition,
editorPosition,
forcedLock,
isDrawerOpen
isDrawerOpen,
tabChangedFromSwipeAction
]
);
@@ -362,18 +369,20 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
isDrawerOpen.value = true;
currentTab.value = 1;
runOnJS(onDrawerStateChange)(true);
tabChangedFromSwipeAction.value = true;
return;
} else if (!isSwipeLeft && finalValue > 100) {
translateX.value = withSpring(homePosition, animationConfig);
isDrawerOpen.value = false;
currentTab.value = 1;
runOnJS(onDrawerStateChange)(false);
tabChangedFromSwipeAction.value = true;
return;
} else if (!isSwipeLeft && finalValue < 100) {
translateX.value = withSpring(0, animationConfig);
isDrawerOpen.value = true;
currentTab.value = 1;
tabChangedFromSwipeAction.value = true;
return;
}
}
@@ -384,6 +393,8 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
translateX.value = withSpring(editorPosition, animationConfig);
currentTab.value = 2;
isDrawerOpen.value = false;
tabChangedFromSwipeAction.value = true;
return;
}
}
@@ -398,6 +409,8 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
translateX.value = withSpring(editorPosition, animationConfig);
currentTab.value = 2;
}
tabChangedFromSwipeAction.value = true;
return;
}

View File

@@ -310,6 +310,7 @@ const ExportNotesSheet = ({
ToastManager.error(e as Error);
});
} else {
await sleep(500);
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
@@ -338,6 +339,7 @@ const ExportNotesSheet = ({
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
await sleep(500);
Share.open({
url: result?.fileDir + result.fileName
}).catch(() => {

View File

@@ -66,7 +66,7 @@ export const SideMenuNotebooks = () => {
if (lastQuery.current) {
db.lookup
.notebooks(lastQuery.current)
.sorted()
.sorted(db.settings.getGroupOptions("notebooks"))
.then((filtered) => {
setFilteredNotebooks(filtered);
});

View File

@@ -220,7 +220,7 @@ export const SideMenuTags = () => {
if (lastQuery.current) {
db.lookup
.tags(lastQuery.current.trim())
.sorted()
.sorted(db.settings.getGroupOptions("tags"))
.then(async (filtered) => {
setFilteredTags(filtered);
});

View File

@@ -336,7 +336,7 @@ const emailconfirmed: { id: string; steps: TStep[] } = {
steps: [
{
title: strings.emailConfirmed(),
text: strings.emailNotConfirmedDesc(),
text: strings.emailConfirmedDesc(),
walkthroughItem: (colors) => (
<SvgView src={WELCOME_SVG(colors.primary.paragraph)} />
),

View File

@@ -19,9 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [
{
title: "Improved Search",
body: "Improved search results with better relevance and highlighting."
}
];
export const features: FeatureType[] = [];

View File

@@ -515,6 +515,8 @@ export const useActions = ({
title: strings.addNotebook(),
icon: "plus",
onPress: async () => {
close();
await sleep(300);
AddNotebookSheet.present(undefined, item);
}
},
@@ -523,6 +525,8 @@ export const useActions = ({
title: strings.editNotebook(),
icon: "square-edit-outline",
onPress: async () => {
close();
await sleep(300);
AddNotebookSheet.present(item);
}
},

View File

@@ -30,6 +30,10 @@ import React, {
useState
} from "react";
import { Dimensions, LayoutChangeEvent, Platform, View } from "react-native";
import Orientation, {
OrientationType,
useDeviceOrientationChange
} from "react-native-orientation-locker";
import Animated, {
useAnimatedStyle,
useSharedValue,
@@ -67,10 +71,6 @@ import {
} from "../utils/events";
import { editorRef, fluidTabsRef } from "../utils/global-refs";
import { AppNavigationStack } from "./navigation-stack";
import Orientation, {
OrientationType,
useDeviceOrientationChange
} from "react-native-orientation-locker";
const MOBILE_SIDEBAR_SIZE = 0.85;
@@ -535,6 +535,15 @@ const onChangeTab = async (event: { i: number; from: number }) => {
) {
eSendEvent(eUnlockNote);
}
if (
fluidTabsRef.current?.tabChangedFromSwipeAction.value &&
!useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab)
) {
editorController?.current?.commands?.focus(
useTabStore.getState().currentTab
);
}
} else {
if (event.from === 2) {
deactivateKeepAwake();

View File

@@ -166,6 +166,20 @@ function closeDrawer() {
fluidTabsRef.current?.closeDrawer();
}
function resetRootState() {
const state = rootNavigatorRef.getState();
const focusedRouteKey = state.routes[state.index].key;
const routes = state.routes.filter(
(route) => route.name !== "Auth" && route.name !== "Welcome"
);
const newIndex = routes.findIndex((route) => route.key === focusedRouteKey);
rootNavigatorRef.reset({
...state,
routes: routes,
index: newIndex
});
}
const Navigation = {
navigate,
goBack,
@@ -177,7 +191,8 @@ const Navigation = {
queueRoutesForUpdate,
routeNeedsUpdate,
routeNames,
routeUpdateFunctions
routeUpdateFunctions,
resetRootState
};
export default Navigation;

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFormattedReminderTime } from "@notesnook/common";
import { isReminderActive, Reminder } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import notifee, {
@@ -36,6 +37,7 @@ import dayjs, { Dayjs } from "dayjs";
import { encodeNonAsciiHTML } from "entities";
import { Platform } from "react-native";
import { db, setupDatabase } from "../common/database";
import { MMKV } from "../common/database/mmkv";
import { presentDialog } from "../components/dialog/functions";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import { editorState } from "../screens/editor/tiptap/utils";
@@ -52,8 +54,6 @@ import { DDS } from "./device-detection";
import { eSendEvent } from "./event-manager";
import Navigation from "./navigation";
import SettingsService from "./settings";
import { getFormattedReminderTime } from "@notesnook/common";
import { MMKV } from "../common/database/mmkv";
let pinned: DisplayedNotification[] = [];
@@ -454,7 +454,7 @@ async function loadNote(id: string, jump: boolean) {
const tab = useTabStore.getState().getTabForNote(id);
if (useTabStore.getState().currentTab !== tab) {
eSendEvent(eOnLoadNote, {
note: note
item: note
});
}
}
@@ -919,12 +919,14 @@ async function pinQuickNote(launch: boolean) {
async function setupReminders(checkNeedsScheduling = false) {
const reminders = ((await db.reminders?.all.items()) as Reminder[]) || [];
let notificationsCancelled = false;
if (Platform.OS === "android") {
// If the API level has changed, cancel all notifications.
// This is to ensure that the app does not crash on Android 14+.
const API_LEVEL = MMKV.getInt("android_apiLevel");
if (API_LEVEL !== (Platform.Version as number)) {
await notifee.cancelAllNotifications();
notificationsCancelled = true;
MMKV.setInt("android_apiLevel", Platform.Version as number);
}
}
@@ -938,20 +940,22 @@ async function setupReminders(checkNeedsScheduling = false) {
// Skip reminders that are not repeating and their trigger date is in past.
if (reminder.mode === "once" && dayjs().isAfter(reminder.date)) continue;
const pending = triggers.filter((t) =>
t.notification.id?.startsWith(reminder.id)
);
if (!notificationsCancelled) {
const pending = triggers.filter((t) =>
t.notification.id?.startsWith(reminder.id)
);
let needsReschedule = pending.length === 0 ? true : false;
if (!needsReschedule) {
needsReschedule = pending[0].notification.data?.dateModified
? parseInt(pending[0].notification.data?.dateModified as string) <
reminder.dateModified
: true;
let needsReschedule = pending.length === 0 ? true : false;
if (!needsReschedule) {
needsReschedule = pending[0].notification.data?.dateModified
? parseInt(pending[0].notification.data?.dateModified as string) <
reminder.dateModified
: true;
}
if (!needsReschedule && checkNeedsScheduling) continue;
}
if (!needsReschedule && checkNeedsScheduling) continue;
await scheduleNotification(reminder);
}
// Check for any triggers whose notifications

View File

@@ -124,7 +124,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3064
versionCode 3066
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -1,3 +1,4 @@
- Add scroll to top/bottom in editor
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -5,5 +5,5 @@ rootProject.name = 'Notesnook'
include ':app'
includeBuild('../../node_modules/@react-native/gradle-plugin')
include ":lazysodium"
project(':lazysodium').projectDir = new File(rootProject.projectDir, '../../node_modules/@ammarahmed/react-native-sodium/android/lazysodium')
include ":lazysodium-android"
project(':lazysodium-android').projectDir = new File(rootProject.projectDir, '../../node_modules/@ammarahmed/react-native-sodium/android/lazysodium-android/app')

View File

@@ -1089,7 +1089,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2143;
CURRENT_PROJECT_VERSION = 2145;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1163,7 +1163,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.2.8;
MARKETING_VERSION = 3.2.10;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1194,7 +1194,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2143;
CURRENT_PROJECT_VERSION = 2145;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1268,7 +1268,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.2.8;
MARKETING_VERSION = 3.2.10;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1427,7 +1427,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2143;
CURRENT_PROJECT_VERSION = 2145;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1439,7 +1439,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.2.8;
MARKETING_VERSION = 3.2.10;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1470,7 +1470,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2143;
CURRENT_PROJECT_VERSION = 2145;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1483,7 +1483,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.2.8;
MARKETING_VERSION = 3.2.10;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1513,7 +1513,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2143;
CURRENT_PROJECT_VERSION = 2145;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1594,7 +1594,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.2.8;
MARKETING_VERSION = 3.2.10;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1625,7 +1625,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2143;
CURRENT_PROJECT_VERSION = 2145;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1707,7 +1707,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.2.8;
MARKETING_VERSION = 3.2.10;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -66,7 +66,7 @@
"react-native-screenguard": "^1.0.0",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-pluralrules": "5.2.14",
"@ammarahmed/react-native-sodium": "^1.6.4",
"@ammarahmed/react-native-sodium": "^1.6.5",
"@react-native-community/datetimepicker": "^8.2.0",
"react-native-exit-app": "github:ammarahm-ed/react-native-exit-app",
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.2.8",
"version": "3.2.10",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -56,4 +56,4 @@
"react": "18.2.0",
"react-native": "0.77.2"
}
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/theme-builder",
"version": "1.3.0",
"version": "1.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/theme-builder",
"version": "1.3.0",
"version": "1.4.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"@emotion/react": "11.11.1",

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/theme-builder",
"description": "Your private note taking space",
"version": "1.3.0",
"version": "1.4.0",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -1 +1 @@
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1

View File

@@ -1 +1 @@
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1

View File

@@ -1 +1 @@
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1

View File

@@ -162,9 +162,9 @@ export class SettingsViewModel {
await appLockSwitch.click();
await fillPasswordDialog(this.page, userPassword);
await this.page.waitForTimeout(100);
await this.page.waitForTimeout(500);
await fillConfirmPasswordDialog(this.page, appLockPassword);
await this.page.waitForTimeout(100);
await this.page.waitForTimeout(500);
}
async disableAppLock(appLockPassword: string) {

View File

@@ -91,6 +91,7 @@ export async function fillReminderDialog(
}
await confirmDialog(dialog);
await dialog.waitFor({ state: "hidden" });
}
export async function fillItemDialog(page: Page, item: Item) {
@@ -133,7 +134,6 @@ export async function fillConfirmPasswordDialog(page: Page, password: string) {
export async function confirmDialog(dialog: Locator) {
const dialogConfirm = dialog.locator(getTestId("dialog-yes"));
await dialogConfirm.click();
// await dialogConfirm.waitFor({ state: "detached" });
}
export async function denyDialog(page: Page) {

View File

@@ -53,16 +53,17 @@ test("adding a one-time reminder before current time should not be possible", as
await app.goto();
const reminders = await app.goToReminders();
await reminders.createReminder({
...ONE_TIME_REMINDER,
date: 0
});
expect(
await app.toasts.waitForToast(
const result = await Promise.race([
reminders.createReminder({
...ONE_TIME_REMINDER,
date: 0
}),
app.toasts.waitForToast(
"Reminder time cannot be earlier than the current time."
)
).toBeTruthy();
]);
expect(result).toBeTruthy();
});
for (const recurringMode of ["Daily", "Weekly", "Monthly"] as const) {

View File

@@ -455,6 +455,7 @@ test("if note is active in multiple tabs, moving the note to trash should close
title: "Note 1"
});
await note?.contextMenu.openInNewTab();
await page.waitForTimeout(1000);
await note?.contextMenu.moveToTrash();

View File

@@ -230,7 +230,7 @@ kbd {
background: var(--background);
border-radius: 3px;
padding: 2px 5px;
color: var(--paragraph-secondary);
color: var(--paragraph);
}
.ping {

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState, Suspense, useEffect, useRef } from "react";
import { Suspense, useEffect, useRef } from "react";
import { Box, Flex } from "@theme-ui/components";
import { ScopedThemeProvider } from "./components/theme-provider";
import useMobile from "./hooks/use-mobile";
@@ -60,6 +60,14 @@ function App() {
useSettingStore.getState().desktopIntegrationSettings?.nativeTitlebar;
console.timeEnd("loading app");
useEffect(() => {
if (isMobile) {
useStore.setState({
isNavPaneCollapsed: false
});
}
}, [isMobile]);
return (
<>
{isFocused ? null : (
@@ -216,7 +224,7 @@ function DesktopAppContents() {
initialSize={380}
style={{ flex: 1, display: "flex" }}
snapSize={120}
maxSize={500}
maxSize={1000}
className="list-pane"
>
<ScopedThemeProvider

View File

@@ -23,6 +23,9 @@ import { useStore as useSearchStore } from "../stores/search-store";
import { useEditorManager } from "../components/editor/manager";
import { CommandPaletteDialog } from "../dialogs/command-palette";
import { hashNavigate } from "../navigation";
import { getKeybinding, keybindings } from "@notesnook/common";
import { KeyboardShortcutsDialog } from "../dialogs/keyboard-shortcuts-dialog";
import { isMac } from "../utils/platform";
function isInEditor(e: KeyboardEvent) {
return (
@@ -30,200 +33,71 @@ function isInEditor(e: KeyboardEvent) {
);
}
const KEYMAP = [
// {
// keys: ["command+n", "ctrl+n", "command+alt+n", "ctrl+alt+n"],
// description: "Create a new note",
// global: true,
// action: (e) => {
// e.preventDefault();
// hashNavigate("/notes/create", {
// addNonce: true,
// replace: true,
// notify: true,
// });
// },
// },
// {
// keys: [
// "command+shift+n",
// "ctrl+shift+n",
// "command+shift+alt+n",
// "ctrl+shift+alt+n",
// ],
// description: "Create a new notebook",
// global: true,
// action: (e) => {
// e.preventDefault();
// hashNavigate("/notebooks/create", {
// replace: true,
// notify: true,
// });
// },
// },
{
keys: [
"command+option+right",
"ctrl+alt+right",
"command+option+shift+right",
"ctrl+alt+shift+right",
"ctrl+tab",
"command+tab"
],
description: "Go to next tab",
action: () => useEditorStore.getState().focusNextTab()
},
{
keys: [
"command+option+left",
"ctrl+alt+left",
"command+option+shift+left",
"ctrl+alt+shift+left",
"ctrl+shift+tab",
"command+shift+tab"
],
description: "Go to previous tab",
action: () => useEditorStore.getState().focusPreviousTab()
},
{
keys: ["ctrl+t", "command+t"],
description: "Create a new tab",
action: () => useEditorStore.getState().addTab()
},
{
keys: ["ctrl+n", "command+n"],
description: "Create a new note",
action: () => useEditorStore.getState().newSession()
},
{
keys: ["ctrl+w", "command+w"],
description:
"Close active tab or focus previously activated tab if active tab pinned",
action: () => {
const activeTab = useEditorStore.getState().getActiveTab();
if (activeTab?.pinned) {
useEditorStore.getState().focusLastActiveTab();
return;
}
useEditorStore.getState().closeActiveTab();
const actions: Partial<
Record<keyof typeof keybindings, (() => void) | ((e: KeyboardEvent) => void)>
> = {
nextTab: () => useEditorStore.getState().focusNextTab(),
previousTab: () => useEditorStore.getState().focusPreviousTab(),
newTab: () => useEditorStore.getState().addTab(),
newNote: () => useEditorStore.getState().newSession(),
closeActiveTab: () => {
const activeTab = useEditorStore.getState().getActiveTab();
if (activeTab?.pinned) {
useEditorStore.getState().focusLastActiveTab();
return;
}
useEditorStore.getState().closeActiveTab();
},
{
keys: ["ctrl+shift+w", "command+shift+w"],
description: "Close all tabs",
action: () => useEditorStore.getState().closeAllTabs()
},
{
keys: ["command+f", "ctrl+f"],
description: "Search all notes",
global: false,
action: (e: KeyboardEvent) => {
if (isInEditor(e)) {
const activeSession = useEditorStore.getState().getActiveSession();
if (activeSession?.type === "readonly") {
e.preventDefault();
const editor = useEditorManager
.getState()
.getEditor(activeSession.id);
editor?.editor?.startSearch();
}
return;
closeAllTabs: () => useEditorStore.getState().closeAllTabs(),
searchInNotes: (e: KeyboardEvent) => {
if (isInEditor(e)) {
const activeSession = useEditorStore.getState().getActiveSession();
if (activeSession?.type === "readonly") {
e.preventDefault();
const editor = useEditorManager.getState().getEditor(activeSession.id);
editor?.editor?.startSearch();
}
e.preventDefault();
return;
}
e.preventDefault();
useSearchStore.setState({ isSearching: true, searchType: "notes" });
}
useSearchStore.setState({ isSearching: true, searchType: "notes" });
},
// {
// keys: ["alt+n"],
// description: "Go to Notes",
// global: false,
// action: (e) => {
// e.preventDefault();
// navigate("/notes");
// },
// },
// {
// keys: ["alt+b"],
// description: "Go to Notebooks",
// global: false,
// action: (e) => {
// e.preventDefault();
// navigate("/notebooks");
// },
// },
// {
// keys: ["alt+f"],
// description: "Go to Favorites",
// global: false,
// action: (e) => {
// e.preventDefault();
// navigate("/favorites");
// },
// },
// {
// keys: ["alt+t"],
// description: "Go to Tags",
// global: false,
// action: (e) => {
// e.preventDefault();
// navigate("/tags");
// },
// },
// {
// keys: ["alt+d"],
// description: "Go to Trash",
// global: false,
// action: (e) => {
// e.preventDefault();
// navigate("/trash");
// },
// },
// {
// keys: ["alt+s"],
// description: "Go to Settings",
// global: false,
// action: (e) => {
// e.preventDefault();
// navigate("/settings");
// },
// },
// {
// keys: ["command+d", "ctrl+d"],
// description: "Toggle dark/light mode",
// global: true,
// action: (e) => {
// e.preventDefault();
// themestore.get().toggleNightMode();
// },
// },
{
keys: ["ctrl+k", "cmd+k", "ctrl+p", "cmd+p"],
description: "Open command palette",
action: (e: KeyboardEvent) => {
e.preventDefault();
CommandPaletteDialog.close();
CommandPaletteDialog.show({
isCommandMode: e.key === "k"
}).catch(() => {});
}
openCommandPalette: () => {
CommandPaletteDialog.close();
CommandPaletteDialog.show({
isCommandMode: true
}).catch(() => {});
},
{
keys: ["ctrl+,", "command+,"],
description: "Open settings",
action: () => hashNavigate("/settings", { replace: true })
}
];
openQuickOpen: () => {
CommandPaletteDialog.close();
CommandPaletteDialog.show({
isCommandMode: false
}).catch(() => {});
},
openSettings: (e) => {
if (isInEditor(e)) return;
hashNavigate("/settings", { replace: true });
},
openKeyboardShortcuts: () => KeyboardShortcutsDialog.show({})
};
export function registerKeyMap() {
hotkeys.filter = function () {
return true;
};
KEYMAP.forEach((key) => {
hotkeys(key.keys.join(","), (e) => {
Object.entries(actions).forEach(([id, action]) => {
const keys = getKeybinding(
id as keyof typeof keybindings,
IS_DESKTOP_APP,
isMac()
);
if (!keys || keys.length === 0) return;
hotkeys(keys.join(","), (e) => {
e.preventDefault();
key.action?.(e);
action(e);
});
});
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { PropsWithChildren, useEffect, useRef } from "react";
import { Box } from "@theme-ui/components";
import { Close, AddReminder } from "../icons";
import { Close, AddReminder, Menu } from "../icons";
import { useStore as useSearchStore } from "../../stores/search-store";
import useMobile from "../../hooks/use-mobile";
import { debounce, usePromise } from "@notesnook/common";
@@ -27,6 +27,7 @@ import Field from "../field";
import { strings } from "@notesnook/intl";
import { RouteResult } from "../../navigation/types";
import { CREATE_BUTTON_MAP } from "../../common";
import { AppEventManager, AppEvents } from "../../common/app-events";
export type RouteContainerButtons = {
search?: {
@@ -133,6 +134,16 @@ function Header(props: RouteContainerProps) {
if (e.key === "Escape") useSearchStore.getState().resetSearch();
else useSearchStore.setState({ isSearching: true, searchType: type });
}}
leftActions={[
{
icon: Menu,
hidden: !isMobile,
id: "hamburger-menu",
onClick: () => {
AppEventManager.publish(AppEvents.toggleSideMenu, true);
}
}
]}
rightActions={[
{
icon: Close,

View File

@@ -17,7 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { debounce, usePromise } from "@notesnook/common";
import {
debounce,
formatKey,
keybindings,
usePromise
} from "@notesnook/common";
import { EVENTS, fuzzy, Note, Notebook, Reminder, Tag } from "@notesnook/core";
import { Box, Button, Flex, Input, Text } from "@theme-ui/components";
import { useCallback, useEffect, useRef, useState } from "react";
@@ -539,7 +544,10 @@ function getCommandPaletteHelp(isCommandMode: boolean) {
...(isCommandMode
? [
{
key: isMac() ? "⌘P" : "Ctrl+P",
key: keybindings.openQuickOpen
.keys(IS_DESKTOP_APP)
.map((k) => formatKey(k, isMac(), "+"))
.join(" / "),
description: strings.quickOpen()
}
]
@@ -553,7 +561,10 @@ function getCommandPaletteHelp(isCommandMode: boolean) {
description: strings.createNewNote()
},
{
key: isMac() ? "⌘K" : "Ctrl+K",
key: keybindings.openCommandPalette
.keys(IS_DESKTOP_APP)
.map((k) => formatKey(k, isMac(), "+"))
.join(" / "),
description: strings.commandPalette()
}
])

View File

@@ -50,6 +50,7 @@ import { notebookMenuItems } from "../../components/notebook";
import { tagMenuItems } from "../../components/tag";
import { useEditorManager } from "../../components/editor/manager";
import Config from "../../utils/config";
import { KeyboardShortcutsDialog } from "../keyboard-shortcuts-dialog";
export interface BaseCommand {
id: string;
@@ -181,6 +182,14 @@ const staticCommands: Command[] = [
group: strings.navigate(),
type: "command"
},
{
id: "keyboard-shortcuts",
title: "Keyboard shortcuts",
icon: ArrowTopRight,
action: () => KeyboardShortcutsDialog.show({}),
group: strings.navigate(),
type: "command"
},
{
id: "attachment-manager",
title: strings.attachmentManager(),

View File

@@ -0,0 +1,112 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatKey, getGroupedKeybindings } from "@notesnook/common";
import { Flex, Text } from "@theme-ui/components";
import { DialogManager } from "../common/dialog-manager";
import Dialog from "../components/dialog";
import { isMac } from "../utils/platform";
const groupedKeybindings = getGroupedKeybindings(IS_DESKTOP_APP, isMac());
export const KeyboardShortcutsDialog = DialogManager.register(
function KeyboardShortcutsDialog(props) {
return (
<Dialog
isOpen={true}
title={"Keyboard Shortcuts"}
width={750}
onClose={() => props.onClose(false)}
>
<Flex
sx={{
flexDirection: "column",
flexWrap: "nowrap",
mt: 2,
gap: 1
}}
>
{groupedKeybindings.map((group) => {
return (
<Flex key={group.category} sx={{ flexDirection: "column" }}>
<Text
variant="subtitle"
sx={{
borderBottom: "1px solid var(--border)",
mb: 1,
pb: 1
}}
>
{group.category}
</Text>
{group.shortcuts.map((shortcut) => {
return (
<Flex
key={shortcut.description}
sx={{
mb: 2,
flexDirection: "row",
justifyContent: "space-between"
}}
>
<Text variant="body">{shortcut.description}</Text>
<Keys keys={shortcut.keys} />
</Flex>
);
})}
</Flex>
);
})}
</Flex>
</Dialog>
);
}
);
function Keys({ keys }: { keys: string[] }) {
return (
<Flex sx={{ gap: 1 }}>
{keys.map((key, index) => (
<>
{key.split(" ").map((k) => (
<Text
key={k}
as="code"
sx={{
bg: "background",
color: "paragraph",
px: 1,
borderRadius: 5,
fontSize: "body",
border: "1px solid var(--border)"
}}
>
{formatKey(k, isMac())}
</Text>
))}
{keys.length - 1 !== index && (
<Text as="span" sx={{ fontSize: "0.8em" }}>
/
</Text>
)}
</>
))}
</Flex>
);
}

View File

@@ -27,15 +27,15 @@ import {
VirtualizedGrouping,
createInternalLink
} from "@notesnook/core";
import { VirtualizedList } from "../components/virtualized-list";
import { Button, Flex, Text } from "@theme-ui/components";
import { ScrollContainer } from "@notesnook/ui";
import { LinkAttributes } from "@notesnook/editor";
import { NoteResolvedData, ResolvedItem } from "@notesnook/common";
import { Lock } from "../components/icons";
import { ellipsize } from "@notesnook/core";
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
import { strings } from "@notesnook/intl";
import { Virtuoso } from "react-virtuoso";
import { CustomScrollbarsVirtualList } from "../components/list-container";
export type NoteLinkingDialogProps = BaseDialogProps<LinkAttributes | false> & {
attributes?: LinkAttributes;
@@ -83,7 +83,10 @@ export const NoteLinkingDialog = DialogManager.register(
}}
noScroll
>
<Flex variant="columnFill" sx={{ mx: 3, overflow: "hidden" }}>
<Flex
variant="columnFill"
sx={{ mx: 3, overflow: "hidden", height: 500 }}
>
{selectedNote ? (
<>
<Field
@@ -131,15 +134,15 @@ export const NoteLinkingDialog = DialogManager.register(
{strings.noBlocksOnNote()}
</Text>
) : null}
<ScrollContainer>
<VirtualizedList
items={filteredBlocks || blocks}
estimatedSize={34}
mode="dynamic"
itemGap={5}
getItemKey={(i) => blocks[i].id}
mt={1}
renderItem={({ item }) => (
<Virtuoso
style={{ height: "100%", width: "100%" }}
components={{
Scroller: CustomScrollbarsVirtualList
}}
data={filteredBlocks || blocks}
context={{ items: filteredBlocks || blocks }}
itemContent={(_, item) => {
return (
<Button
variant="menuitem"
sx={{
@@ -162,7 +165,10 @@ export const NoteLinkingDialog = DialogManager.register(
>
<Text
variant="body"
sx={{ fontFamily: "monospace", whiteSpace: "pre-wrap" }}
sx={{
fontFamily: "monospace",
whiteSpace: "pre-wrap"
}}
>
{ellipsize(item.content, 200, "end").trim() ||
strings.linkNoteEmptyBlock()}
@@ -181,9 +187,9 @@ export const NoteLinkingDialog = DialogManager.register(
{item.type.toUpperCase()}
</Text>
</Button>
)}
/>
</ScrollContainer>
);
}}
/>
</>
) : (
<>
@@ -203,14 +209,14 @@ export const NoteLinkingDialog = DialogManager.register(
}}
/>
{notes && (
<ScrollContainer>
<VirtualizedList
items={notes.placeholders}
estimatedSize={28}
itemGap={5}
getItemKey={notes.key}
mt={1}
renderItem={({ index }) => (
<Virtuoso
data={notes.placeholders}
components={{
Scroller: CustomScrollbarsVirtualList
}}
style={{ height: "100%", width: "100%" }}
itemContent={(index) => (
<div style={{ height: 28 }}>
<ResolvedItem items={notes} index={index} type="note">
{({ item: note, data }) => (
<Button
@@ -239,9 +245,9 @@ export const NoteLinkingDialog = DialogManager.register(
</Button>
)}
</ResolvedItem>
)}
/>
</ScrollContainer>
</div>
)}
/>
)}
</>
)}

View File

@@ -492,6 +492,7 @@ function SessionExpiry(props: BaseAuthComponentProps<"sessionExpiry">) {
placeholder={user ? maskEmail(user.email) : undefined}
autoFocus
disabled
required={false}
/>
<Button
data-test-id="auth-forgot-password"
@@ -974,10 +975,10 @@ function SubtitleWithAction(props: SubtitleWithActionProps) {
export function AuthField(props: FieldProps) {
return (
<Field
required
{...props}
name={props.name || props.id}
data-test-id={props["data-test-id"] || props.id}
required
sx={{ mt: 2, width: "100%" }}
styles={{
// label: { fontWeight: "normal" },

View File

@@ -1,21 +0,0 @@
---
title: OneNote
---
# How do I import notes from OneNote notes app?
The following steps will help you import your notes from OneNote easily.
1. Go to [https://importer.notesnook.com/](https://importer.notesnook.com/) and select `OneNote` from list of apps.
![](/static/onenote-importer/1.png)
2. Click on `Start importing`
![](/static/onenote-importer/2.png)
3. Login to your account
![](/static/onenote-importer/3.png)
4. Give permission to Notesnook Importer to access your notes
![](/static/onenote-importer/4.png)
5. Wait while Notesnook importer imports your notes from OneNote account. Once importer completes processing, download the .zip file.
![](/static/onenote-importer/5.png)
6. After you have downloaded the `.zip` file, [go to the Notesnook Web App](https://app.notesnook.com/) > Settings > Notesnook Importer. Select the .zip you downloaded earlier and click "Start import" button.
![](/static/import-zip-app.png)
7. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, feel free to [report them on GitHub](https://github.com/streetwriters/notesnook-importer).

View File

@@ -1,20 +0,0 @@
---
title: Standard Notes
---
# How do I import notes from Standard Notes?
The following steps will help you import your notes from Standard notes easily.
1. Open Standard Notes app on Desktop or visit [https://app.standardnotes.org](https://app.standardnotes.org) and Login to your account.
2. Click on `Preferences` icon on bottom left corner to open preferences.
![](/static/standard-notes-importer/1.png)
3. Scroll down to `Data backups` section and choose backup type `Decrypted`.
![](/static/standard-notes-importer/2.png)
4. Go to `Backups` from the Sidebar & select "Data backup" to download backup .zip file.
5. Open the Notesnook app (web or desktop)
6. Go to `Settings > Notesnook Importer` and select `Standard Notes` from list of apps.
![](/static/standard-notes-importer/3.png)
7. Drop the .zip backup file you exported earlier from Standard Notes in the box or click anywhere to open system file picker to select the backup.
![](/static/standard-notes-importer/4.png)
8. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, [report it on github](https://github.com/streetwriters/notesnook).

View File

@@ -0,0 +1,78 @@
---
title: Keyboard Shortcuts
description: Keyboard shortcuts for Notesnook
---
# Keyboard shortcuts
The following keyboard shortcuts will help you navigate Notesnook faster.
### General
| Description | Web | Windows/Linux | Mac |
| --- | --- | --- | --- |
| Search in notes list view if editor is not focused | Ctrl F | Ctrl F | ⌘ F |
| Settings | Ctrl , | Ctrl , | ⌘ , |
| Keyboard shortcuts | Ctrl / | Ctrl / | ⌘ / |
| New note | - | Ctrl N | ⌘ N |
### Navigation
| Description | Web | Windows/Linux | Mac |
| --- | --- | --- | --- |
| Next tab | Ctrl Alt → / Ctrl Alt ⇧ → | Ctrl tab | ⌘ tab |
| Previous tab | Ctrl Alt ← / Ctrl Alt ⇧ ← | Ctrl ⇧ tab | ⌘ ⇧ tab |
| Command palette | Ctrl ⇧ P | Ctrl ⇧ P | ⌘ ⇧ P |
| Quick open | Ctrl P | Ctrl P | ⌘ P |
| New tab | - | Ctrl T | ⌘ T |
| Close active tab | - | Ctrl W | ⌘ W |
| Close all tabs | - | Ctrl ⇧ W | ⌘ ⇧ W |
### Editor
| Description | Web | Windows/Linux | Mac |
| --- | --- | --- | --- |
| Add attachment | Ctrl ⇧ A | Ctrl ⇧ A | ⌘ ⇧ A |
| Insert blockquote | Ctrl ⇧ B | Ctrl ⇧ B | ⌘ ⇧ B |
| Toggle bold | Ctrl B | Ctrl B | ⌘ B |
| Toggle bullet list | Ctrl ⇧ 8 | Ctrl ⇧ 8 | ⌘ ⇧ 8 |
| Toggle check list | Ctrl ⇧ 9 | Ctrl ⇧ 9 | ⌘ ⇧ 9 |
| Split list item | ↵ | ↵ | ↵ |
| Lift list item | ⇧ Tab | ⇧ Tab | ⇧ Tab |
| Sink list item | Ctrl ⇧ Down | Ctrl ⇧ Down | ⌘ ⇧ Down |
| Toggle code | Ctrl E | Ctrl E | ⌘ E |
| Toggle code block | Ctrl ⇧ C | Ctrl ⇧ C | ⌘ ⇧ C |
| Insert date | Alt D | Alt D | ⌥ D |
| Insert time | Alt T | Alt T | ⌥ T |
| Insert date and time | Ctrl Alt D | Ctrl Alt D | ⌘ ⌥ D |
| Insert date and time with timezone | Ctrl Alt Z | Ctrl Alt Z | ⌘ ⌥ Z |
| Increase font size | Ctrl [ | Ctrl [ | ⌘ [ |
| Decrease font size | Ctrl ] | Ctrl ] | ⌘ ] |
| Insert paragraph | Ctrl ⇧ 0 | Ctrl ⇧ 0 | ⌘ ⇧ 0 |
| Insert heading 1 | Ctrl Alt 1 | Ctrl Alt 1 | ⌘ ⌥ 1 |
| Insert heading 2 | Ctrl Alt 2 | Ctrl Alt 2 | ⌘ ⌥ 2 |
| Insert heading 3 | Ctrl Alt 3 | Ctrl Alt 3 | ⌘ ⌥ 3 |
| Insert heading 4 | Ctrl Alt 4 | Ctrl Alt 4 | ⌘ ⌥ 4 |
| Insert heading 5 | Ctrl Alt 5 | Ctrl Alt 5 | ⌘ ⌥ 5 |
| Insert heading 6 | Ctrl Alt 6 | Ctrl Alt 6 | ⌘ ⌥ 6 |
| Undo | Ctrl Z | Ctrl Z | ⌘ Z |
| Redo | Ctrl ⇧ Z / Ctrl Y | Ctrl ⇧ Z / Ctrl Y | ⌘ ⇧ Z / ⌘ Y |
| Add image | Ctrl ⇧ I | Ctrl ⇧ I | ⌘ ⇧ I |
| Toggle italic | Ctrl I | Ctrl I | ⌘ I |
| Remove formatting in selection | Ctrl \ | Ctrl \ | ⌘ \ |
| Insert internal link | Ctrl ⇧ K | Ctrl ⇧ K | ⌘ ⇧ K |
| Insert link | Ctrl K | Ctrl K | ⌘ K |
| Insert math block | Ctrl ⇧ M | Ctrl ⇧ M | ⌘ ⇧ M |
| Toggle ordered list | Ctrl ⇧ 7 | Ctrl ⇧ 7 | ⌘ ⇧ 7 |
| Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O |
| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space |
| Open search | Ctrl F | Ctrl F | ⌘ F |
| Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S |
| Toggle subscript | Ctrl , | Ctrl , | ⌘ , |
| Toggle superscript | Ctrl . | Ctrl . | ⌘ . |
| Toggle task list | Ctrl ⇧ T | Ctrl ⇧ T | ⌘ ⇧ T |
| Text align center | Ctrl ⇧ E | Ctrl ⇧ E | ⌘ ⇧ E |
| Text align justify | Ctrl ⇧ J | Ctrl ⇧ J | ⌘ ⇧ J |
| Text align left | Ctrl ⇧ L | Ctrl ⇧ L | ⌘ ⇧ L |
| Text align right | Ctrl ⇧ R | Ctrl ⇧ R | ⌘ ⇧ R |
| Underline | Ctrl U | Ctrl U | ⌘ U |

View File

@@ -54,6 +54,7 @@ navigation:
- path: deleting-your-account.md
- path: app-lock.md
- path: gift-cards.md
- path: keyboard-shortcuts.md
- path: privacy-mode.md
- path: web-clipper

49
docs/help/package-lock.json generated Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "@notesnook/docs-help",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/docs-help",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"devDependencies": {
"@notesnook/common": "file:../../packages/common"
}
},
"../../../packages/common": {
"extraneous": true
},
"../../packages/common": {
"name": "@notesnook/common",
"version": "2.1.3",
"dev": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/core": "file:../core",
"@readme/data-urls": "^3.0.0",
"dayjs": "1.11.13",
"pathe": "^1.1.2",
"timeago.js": "4.0.2"
},
"devDependencies": {
"@notesnook/core": "file:../core",
"@types/react": "18.3.5",
"react": "18.3.1",
"vitest": "2.1.8"
},
"peerDependencies": {
"react": ">=18",
"timeago.js": "4.0.2"
}
},
"../common": {
"extraneous": true
},
"node_modules/@notesnook/common": {
"resolved": "../../packages/common",
"link": true
}
}
}

25
docs/help/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "@notesnook/docs-help",
"version": "1.0.0",
"scripts": {
"document-keyboard-shortcuts": "node scripts/document-keyboard-shortcuts.mjs"
},
"repository": {
"type": "git",
"url": "git+https://github.com/streetwriters/notesnook.git"
},
"keywords": [
"notesnook",
"docs",
"help"
],
"author": "",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/streetwriters/notesnook/issues"
},
"homepage": "https://github.com/streetwriters/notesnook#readme",
"devDependencies": {
"@notesnook/common": "file:../../packages/common"
}
}

View File

@@ -0,0 +1,100 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { writeFileSync } from "fs";
import {
getGroupedKeybindings,
formatKey,
macify,
CATEGORIES
} from "@notesnook/common";
console.log("Generating keyboard shortcuts documentation...");
const keyboardShortcutFilePath = "./contents/keyboard-shortcuts.md";
const frontmatter = `---
title: Keyboard Shortcuts
description: Keyboard shortcuts for Notesnook
---
`;
const content = `# Keyboard shortcuts
The following keyboard shortcuts will help you navigate Notesnook faster.`;
const markdownTable = getGroupedTableKeybindingsMarkdown();
writeFileSync(
keyboardShortcutFilePath,
frontmatter + "\n" + content + "\n\n" + markdownTable,
"utf-8"
);
console.log("Keyboard shortcuts documentation updated successfully!");
/**
* @returns markdown formatted table of keyboard shortcuts grouped by category.
*/
function getGroupedTableKeybindingsMarkdown() {
const desktopKeybindings = getGroupedKeybindings(true, false);
const webKeybindings = getGroupedKeybindings(false, false);
const header = `| Description | Web | Windows/Linux | Mac |
| --- | --- | --- | --- |`;
return CATEGORIES.map((category) => {
const webShortcuts =
webKeybindings.find((g) => g.category === category)?.shortcuts || [];
const desktopShortcuts =
desktopKeybindings.find((g) => g.category === category)?.shortcuts || [];
const mergedShortcuts = {};
webShortcuts.forEach(({ description, keys }) => {
if (!mergedShortcuts[description]) {
mergedShortcuts[description] = {};
}
mergedShortcuts[description].web = keys;
});
desktopShortcuts.forEach(({ description, keys }) => {
if (!mergedShortcuts[description]) {
mergedShortcuts[description] = {};
}
mergedShortcuts[description].desktop = keys;
});
const rows = Object.entries(mergedShortcuts)
.map(([description, { web, desktop }]) => {
const webKeys = web?.map((k) => formatKey(k)).join(" / ") || "-";
const windowsLinuxKeys =
desktop?.map((k) => formatKey(k)).join(" / ") || "-";
const macKeys =
desktop
?.map(macify)
.map((k) => formatKey(k, true))
.join(" / ") || "-";
return `| ${description} | ${webKeys} | ${windowsLinuxKeys} | ${macKeys} |`;
})
.join("\n");
return `### ${category}\n\n${header}\n${rows}`;
}).join("\n\n");
}

View File

@@ -0,0 +1,4 @@
- Fix crash on android devices upgraded to android 16
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,4 @@
- Add scroll to top/bottom in editor
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -5,7 +5,8 @@
"outputs": [
"{projectRoot}/build",
"{projectRoot}/dist",
"{projectRoot}/languages"
"{projectRoot}/languages",
"{projectRoot}/src/extensions/code-block/languages"
],
"cache": true
},

View File

@@ -28,3 +28,4 @@ export * from "./migrate-toolbar.js";
export * from "./export-notes.js";
export * from "./dataurl.js";
export * from "./tab-session-history.js";
export * from "./keybindings.js";

View File

@@ -0,0 +1,485 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
interface Hotkeys {
keys: (isDesktop: boolean) => string[];
description: string;
category: Category;
type: "hotkeys";
}
interface TipTapKey {
keys: string | string[];
description: string;
category: Category;
type: "tiptap";
}
type Category = (typeof CATEGORIES)[number];
export const CATEGORIES = ["General", "Navigation", "Editor"] as const;
/**
* consumed by hotkeys-js
*/
export const hotkeys = {
nextTab: {
keys: normalizeKeys({
web: ["ctrl+alt+right", "ctrl+alt+shift+right"],
desktop: ["ctrl+tab"]
}),
description: "Next tab",
category: "Navigation",
type: "hotkeys"
},
previousTab: {
keys: normalizeKeys({
web: ["ctrl+alt+left", "ctrl+alt+shift+left"],
desktop: ["ctrl+shift+tab"]
}),
description: "Previous tab",
category: "Navigation",
type: "hotkeys"
},
newTab: {
keys: normalizeKeys({
desktop: ["ctrl+t"]
}),
description: "New tab",
category: "Navigation",
type: "hotkeys"
},
closeActiveTab: {
keys: normalizeKeys({
desktop: ["ctrl+w"]
}),
description: "Close active tab",
category: "Navigation",
type: "hotkeys"
},
closeAllTabs: {
keys: normalizeKeys({
desktop: ["ctrl+shift+w"]
}),
description: "Close all tabs",
category: "Navigation",
type: "hotkeys"
},
newNote: {
keys: normalizeKeys({
desktop: ["ctrl+n"]
}),
description: "New note",
category: "General",
type: "hotkeys"
},
searchInNotes: {
keys: normalizeKeys(["ctrl+f"]),
description: "Search in notes list view if editor is not focused",
category: "General",
type: "hotkeys"
},
openCommandPalette: {
keys: normalizeKeys(["ctrl+shift+p"]),
description: "Command palette",
category: "Navigation",
type: "hotkeys"
},
openQuickOpen: {
keys: normalizeKeys(["ctrl+p"]),
description: "Quick open",
category: "Navigation",
type: "hotkeys"
},
openSettings: {
keys: normalizeKeys(["ctrl+,"]),
description: "Settings",
category: "General",
type: "hotkeys"
},
openKeyboardShortcuts: {
keys: normalizeKeys(["ctrl+/"]),
description: "Keyboard shortcuts",
category: "General",
type: "hotkeys"
}
} satisfies Record<string, Hotkeys>;
/**
* consumed by tiptap
*/
export const tiptapKeys = {
addAttachment: {
keys: "Mod-Shift-A",
description: "Add attachment",
category: "Editor",
type: "tiptap"
},
insertBlockquote: {
keys: "Mod-Shift-B",
description: "Insert blockquote",
category: "Editor",
type: "tiptap"
},
toggleBold: {
keys: "Mod-b",
description: "Toggle bold",
category: "Editor",
type: "tiptap"
},
toggleBulletList: {
keys: "Mod-Shift-8",
description: "Toggle bullet list",
category: "Editor",
type: "tiptap"
},
toggleCheckList: {
keys: "Mod-Shift-9",
description: "Toggle check list",
category: "Editor",
type: "tiptap"
},
splitListItem: {
keys: "Enter",
description: "Split list item",
category: "Editor",
type: "tiptap"
},
liftListItem: {
keys: "Shift-Tab",
description: "Lift list item",
category: "Editor",
type: "tiptap"
},
sinkListItem: {
keys: "Mod-Shift-Down",
description: "Sink list item",
category: "Editor",
type: "tiptap"
},
toggleCode: {
keys: "Mod-e",
description: "Toggle code",
category: "Editor",
type: "tiptap"
},
toggleCodeBlock: {
keys: "Mod-Shift-C",
description: "Toggle code block",
category: "Editor",
type: "tiptap"
},
insertDate: {
keys: "Alt-d",
description: "Insert date",
category: "Editor",
type: "tiptap"
},
insertTime: {
keys: "Alt-t",
description: "Insert time",
category: "Editor",
type: "tiptap"
},
insertDateTime: {
keys: "Mod-Alt-d",
description: "Insert date and time",
category: "Editor",
type: "tiptap"
},
insertDateTimeWithTimezone: {
keys: "Mod-Alt-z",
description: "Insert date and time with timezone",
category: "Editor",
type: "tiptap"
},
increaseFontSize: {
keys: "Ctrl-[",
description: "Increase font size",
category: "Editor",
type: "tiptap"
},
decreaseFontSize: {
keys: "Ctrl-]",
description: "Decrease font size",
category: "Editor",
type: "tiptap"
},
insertParagraph: {
keys: "Mod-Shift-0",
description: "Insert paragraph",
category: "Editor",
type: "tiptap"
},
insertHeading1: {
keys: "Mod-Alt-1",
description: "Insert heading 1",
category: "Editor",
type: "tiptap"
},
insertHeading2: {
keys: "Mod-Alt-2",
description: "Insert heading 2",
category: "Editor",
type: "tiptap"
},
insertHeading3: {
keys: "Mod-Alt-3",
description: "Insert heading 3",
category: "Editor",
type: "tiptap"
},
insertHeading4: {
keys: "Mod-Alt-4",
description: "Insert heading 4",
category: "Editor",
type: "tiptap"
},
insertHeading5: {
keys: "Mod-Alt-5",
description: "Insert heading 5",
category: "Editor",
type: "tiptap"
},
insertHeading6: {
keys: "Mod-Alt-6",
description: "Insert heading 6",
category: "Editor",
type: "tiptap"
},
undo: {
keys: "Mod-z",
description: "Undo",
category: "Editor",
type: "tiptap"
},
redo: {
keys: ["Mod-Shift-z", "Mod-y"],
description: "Redo",
category: "Editor",
type: "tiptap"
},
addImage: {
keys: "Mod-Shift-I",
description: "Add image",
category: "Editor",
type: "tiptap"
},
toggleItalic: {
keys: "Mod-i",
description: "Toggle italic",
category: "Editor",
type: "tiptap"
},
removeFormattingInSelection: {
keys: "Mod-\\",
description: "Remove formatting in selection",
category: "Editor",
type: "tiptap"
},
insertInternalLink: {
keys: "Mod-Shift-K",
description: "Insert internal link",
category: "Editor",
type: "tiptap"
},
insertLink: {
keys: "Mod-k",
description: "Insert link",
category: "Editor",
type: "tiptap"
},
insertMathBlock: {
keys: "Mod-Shift-M",
description: "Insert math block",
category: "Editor",
type: "tiptap"
},
toggleOrderedList: {
keys: "Mod-Shift-7",
description: "Toggle ordered list",
category: "Editor",
type: "tiptap"
},
toggleOutlineList: {
keys: "Mod-Shift-O",
description: "Toggle outline list",
category: "Editor",
type: "tiptap"
},
toggleOutlineListExpand: {
keys: "Mod-Space",
description: "Toggle outline list expand",
category: "Editor",
type: "tiptap"
},
openSearch: {
keys: "Mod-f",
description: "Open search",
category: "Editor",
type: "tiptap"
},
toggleStrike: {
keys: "Mod-Shift-S",
description: "Toggle strike",
category: "Editor",
type: "tiptap"
},
toggleSubscript: {
keys: "Mod-,",
description: "Toggle subscript",
category: "Editor",
type: "tiptap"
},
toggleSuperscript: {
keys: "Mod-.",
description: "Toggle superscript",
category: "Editor",
type: "tiptap"
},
toggleTaskList: {
keys: "Mod-Shift-T",
description: "Toggle task list",
category: "Editor",
type: "tiptap"
},
textAlignCenter: {
keys: "Mod-Shift-E",
description: "Text align center",
category: "Editor",
type: "tiptap"
},
textAlignJustify: {
keys: "Mod-Shift-J",
description: "Text align justify",
category: "Editor",
type: "tiptap"
},
textAlignLeft: {
keys: "Mod-Shift-L",
description: "Text align left",
category: "Editor",
type: "tiptap"
},
textAlignRight: {
keys: "Mod-Shift-R",
description: "Text align right",
category: "Editor",
type: "tiptap"
},
underline: {
keys: "Mod-u",
description: "Underline",
category: "Editor",
type: "tiptap"
}
} satisfies Record<string, TipTapKey>;
export const keybindings = {
...hotkeys,
...tiptapKeys
};
export function getKeybinding(
key: keyof typeof keybindings,
isDesktop = false,
isMac = false
) {
const keybinding = keybindings[key];
if (keybinding.type === "hotkeys") {
const hotkeys = keybinding.keys(isDesktop);
return isMac ? hotkeys.map(macify) : hotkeys;
}
const tiptapKeys = Array.isArray(keybinding.keys)
? keybinding.keys
: [keybinding.keys];
return isMac ? tiptapKeys.map(macify) : tiptapKeys;
}
function normalizeKeys(
keys: string[] | { web?: string[]; desktop?: string[] }
): (isDesktop?: boolean) => string[] {
return (isDesktop = false) => {
let keyList: string[] = [];
if (Array.isArray(keys)) {
keyList = keys;
} else {
keyList = isDesktop ? keys.desktop ?? [] : keys.web ?? [];
}
return keyList;
};
}
export function macify(key: string) {
return key
.replace(/ctrl/gi, "Command")
.replace(/alt/gi, "Option")
.replace(/mod/gi, "Command");
}
export function formatKey(key: string, isMac = false, separator = " ") {
return key
.replace(/\+|-/g, separator)
.replace(/\bcommand\b/gi, isMac ? "⌘" : "Ctrl")
.replace(/\bctrl\b/gi, isMac ? "⌘" : "Ctrl")
.replace(/\bmod\b/gi, isMac ? "⌘" : "Ctrl")
.replace(/\balt\b/gi, isMac ? "⌥" : "Alt")
.replace(/\boption\b/gi, isMac ? "⌥" : "Alt")
.replace(/\bshift\b/gi, "⇧")
.replace(/\bright\b/gi, "→")
.replace(/\bleft\b/gi, "←")
.replace(/\benter\b/gi, "↵")
.replace(/\b\w\b/gi, (e) => e.toUpperCase())
.trim();
}
export function getGroupedKeybindings(isDesktop: boolean, isMac: boolean) {
const grouped: {
shortcuts: { keys: string[]; description: string }[];
category: Category;
}[] = CATEGORIES.map((c) => ({
category: c,
shortcuts: []
}));
const allKeybindings = { ...hotkeys, ...tiptapKeys };
for (const key in allKeybindings) {
const binding = allKeybindings[key as keyof typeof allKeybindings];
let keys =
typeof binding.keys === "function"
? binding.keys(isDesktop)
: binding.keys;
if (!keys || !keys.length) continue;
if (isMac) {
keys = Array.isArray(keys) ? keys.map(macify) : macify(keys);
}
const group = grouped.find((g) => g.category === binding.category);
if (!group) throw new Error("Invalid group category: " + binding.category);
group.shortcuts.push({
keys: Array.isArray(keys) ? keys : [keys],
description: binding.description
});
}
return grouped;
}

View File

@@ -25,6 +25,7 @@ import {
toBlobURL,
usePermissionHandler
} from "@notesnook/editor";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import FingerprintIcon from "mdi-react/FingerprintIcon";
import {
@@ -47,7 +48,6 @@ import StatusBar from "./statusbar";
import Tags from "./tags";
import TiptapEditorWrapper from "./tiptap";
import Title from "./title";
import { strings } from "@notesnook/intl";
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
@@ -380,11 +380,15 @@ const Tiptap = ({
const editor = editors[tab.id];
const firstChild = editor?.state.doc.firstChild;
const isParagraph = firstChild?.type.name === "paragraph";
const isFirstChildEmpty =
!firstChild?.textContent || firstChild?.textContent?.length === 0;
if (isParagraph && isFirstChildEmpty) {
const firstChildNodeType = editor?.state.doc.firstChild?.type.name;
const isSimpleNode =
firstChildNodeType !== "image" &&
firstChildNodeType !== "embed" &&
firstChildNodeType !== "attachment" &&
firstChildNodeType !== "mathBlock" &&
firstChildNodeType !== "horizontalRule" &&
firstChildNodeType !== "table";
if (isSimpleNode) {
editor?.commands.focus("end");
return;
}
@@ -405,11 +409,15 @@ const Tiptap = ({
const editor = editors[tab.id];
const docSize = editor?.state.doc.content.size;
if (!docSize) return;
const lastChild = editor?.state.doc.lastChild;
const isParagraph = lastChild?.type.name === "paragraph";
const isLastChildEmpty =
!lastChild?.textContent || lastChild?.textContent?.length === 0;
if (isParagraph && isLastChildEmpty) {
const lastChildNodeType = editor?.state.doc.lastChild?.type.name;
const isSimpleNode =
lastChildNodeType !== "image" &&
lastChildNodeType !== "embed" &&
lastChildNodeType !== "attachment" &&
lastChildNodeType !== "mathBlock" &&
lastChildNodeType !== "horizontalRule" &&
lastChildNodeType !== "table";
if (isSimpleNode) {
editor?.commands.focus("end");
return;
}
@@ -544,10 +552,12 @@ const Tiptap = ({
<div
onScroll={controller.scroll}
ref={containerRef}
id="editor-container-scroller"
style={{
overflowY: controller.loading ? "hidden" : "scroll",
height: "100%",
display: "block",
display: "flex",
flexDirection: "column",
position: "relative"
}}
>
@@ -865,9 +875,10 @@ const Tiptap = ({
}
}}
style={{
flexGrow: 1,
width: "100%",
minHeight: 300
display: "flex",
flex: 1,
minHeight: 100
}}
/>
</div>

View File

@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ControlledMenu, MenuItem as MenuItemInner } from "@szhsin/react-menu";
import ArrowBackIcon from "mdi-react/ArrowBackIcon";
import ArrowTopIcon from "mdi-react/ArrowTopIcon";
import ArrowDownIcon from "mdi-react/ArrowDownIcon";
import ArrowForwardIcon from "mdi-react/ArrowForwardIcon";
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
@@ -428,6 +430,30 @@ function Header({
tab.session?.noteId
);
break;
case "scroll-top":
{
const element = document.getElementById(
"editor-container-scroller"
);
element?.scrollTo({
top: 0,
left: 0,
behavior: "smooth"
});
}
break;
case "scroll-bottom":
{
const element = document.getElementById(
"editor-container-scroller"
);
element?.scrollTo({
top: element?.scrollHeight,
left: 0,
behavior: "smooth"
});
}
break;
case "properties":
post(
EditorEvents.properties,
@@ -590,6 +616,47 @@ function Header({
{strings.toc()}
</span>
</MenuItem>
<MenuItem
value="scroll-top"
style={{
display: "flex",
gap: 10,
alignItems: "center"
}}
>
<ArrowTopIcon
size={22 * settings.fontScale}
color="var(--nn_primary_icon)"
/>
<span
style={{
color: "var(--nn_primary_paragraph)"
}}
>
{strings.scrollToTop()}
</span>
</MenuItem>
<MenuItem
value="scroll-bottom"
style={{
display: "flex",
gap: 10,
alignItems: "center"
}}
>
<ArrowDownIcon
size={22 * settings.fontScale}
color="var(--nn_primary_icon)"
/>
<span
style={{
color: "var(--nn_primary_paragraph)"
}}
>
{strings.scrollToBottom()}
</span>
</MenuItem>
<MenuItem
value="properties"
style={{

View File

@@ -22,6 +22,7 @@ import { Attribute } from "@tiptap/core";
import { createNodeView } from "../react/index.js";
import { AttachmentComponent } from "./component.js";
import { Attachment } from "./types.js";
import { tiptapKeys } from "@notesnook/common";
export type AttachmentType = "image" | "file" | "camera";
export interface AttachmentOptions {
@@ -161,7 +162,7 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
addKeyboardShortcuts() {
return {
"Mod-Shift-A": () =>
[tiptapKeys.addAttachment.keys]: () =>
this.editor.storage.openAttachmentPicker?.("file") || true
};
}

View File

@@ -16,6 +16,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { tiptapKeys } from "@notesnook/common";
import { getParentAttributes } from "../../utils/prosemirror.js";
import { wrappingInputRule } from "@tiptap/core";
import TiptapBlockquote, { inputRegex } from "@tiptap/extension-blockquote";
@@ -47,5 +48,13 @@ export const Blockquote = TiptapBlockquote.extend({
getAttributes: () => getParentAttributes(this.editor)
})
];
},
addKeyboardShortcuts() {
return {
...this.parent?.(),
[tiptapKeys.insertBlockquote.keys]: () =>
this.editor.commands.toggleBlockquote()
};
}
});

View File

@@ -16,6 +16,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { keybindings } from "@notesnook/common";
import { KeyboardShortcutCommand, mergeAttributes, Node } from "@tiptap/core";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
@@ -80,7 +81,8 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
[key: string]: KeyboardShortcutCommand;
} = {
Enter: () => this.editor.commands.splitListItem(this.name),
"Shift-Tab": () => this.editor.commands.liftListItem(this.name)
[keybindings.liftListItem.keys]: () =>
this.editor.commands.liftListItem(this.name)
};
if (!this.options.nested) {

View File

@@ -20,6 +20,7 @@ import { mergeAttributes, Node, wrappingInputRule } from "@tiptap/core";
import { inputRegex } from "@tiptap/extension-task-item";
import { getParentAttributes } from "../../utils/prosemirror.js";
import { ListItem } from "../list-item/index.js";
import { tiptapKeys } from "@notesnook/common";
export interface CheckListOptions {
itemTypeName: string;
@@ -123,7 +124,8 @@ export const CheckList = Node.create<CheckListOptions>({
addKeyboardShortcuts() {
return {
"Mod-Shift-9": () => this.editor.commands.toggleCheckList()
[tiptapKeys.toggleCheckList.keys]: () =>
this.editor.commands.toggleCheckList()
};
}
});

View File

@@ -36,6 +36,7 @@ import stripIndent from "strip-indent";
import { nanoid } from "nanoid";
import Languages from "./languages.json";
import { CaretPosition, CodeLine } from "./utils.js";
import { tiptapKeys } from "@notesnook/common";
interface Indent {
type: "tab" | "space";
@@ -292,7 +293,8 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
addKeyboardShortcuts() {
return {
"Mod-Shift-C": () => this.editor.commands.toggleCodeBlock(),
[tiptapKeys.toggleCodeBlock.keys]: () =>
this.editor.commands.toggleCodeBlock(),
"Mod-a": ({ editor }) => {
const { $anchor } = this.editor.state.selection;
if ($anchor.parent.type.name !== this.name) {

View File

@@ -23,7 +23,7 @@ import {
InputRuleFinder,
ExtendedRegExpMatchArray
} from "@tiptap/core";
import { formatDate } from "@notesnook/common";
import { formatDate, tiptapKeys } from "@notesnook/common";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
@@ -67,10 +67,14 @@ export const DateTime = Extension.create<DateTimeOptions>({
addKeyboardShortcuts() {
return {
"Alt-t": ({ editor }) => editor.commands.insertTime(),
"Alt-d": ({ editor }) => editor.commands.insertDate(),
"Mod-Alt-d": ({ editor }) => editor.commands.insertDateTime(),
"Mod-Alt-z": ({ editor }) => editor.commands.insertDateTimeWithTimeZone()
[tiptapKeys.insertTime.keys]: ({ editor }) =>
editor.commands.insertTime(),
[tiptapKeys.insertDate.keys]: ({ editor }) =>
editor.commands.insertDate(),
[tiptapKeys.insertDateTime.keys]: ({ editor }) =>
editor.commands.insertDateTime(),
[tiptapKeys.insertDateTimeWithTimezone.keys]: ({ editor }) =>
editor.commands.insertDateTimeWithTimeZone()
};
},

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { tiptapKeys } from "@notesnook/common";
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
import { Editor, Extension } from "@tiptap/core";
@@ -86,7 +87,7 @@ export const FontSize = Extension.create<FontSizeOptions>({
},
addKeyboardShortcuts() {
return {
"ctrl-]": ({ editor }) => {
[tiptapKeys.decreaseFontSize.keys]: ({ editor }) => {
editor
.chain()
.focus()
@@ -94,7 +95,7 @@ export const FontSize = Extension.create<FontSizeOptions>({
.run();
return true;
},
"Ctrl-[": ({ editor }) => {
[tiptapKeys.increaseFontSize.keys]: ({ editor }) => {
editor
.chain()
.focus()

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { tiptapKeys } from "@notesnook/common";
import { textblockTypeInputRule } from "@tiptap/core";
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
@@ -49,7 +50,8 @@ export const Heading = TiptapHeading.extend({
(items, level) => ({
...items,
...{
[`Mod-Alt-${level}`]: () => this.editor.commands.setHeading({ level })
[tiptapKeys[`insertHeading${level}`].keys]: () =>
this.editor.commands.setHeading({ level })
}
}),
{}

View File

@@ -27,6 +27,7 @@ import {
import { createNodeView } from "../react/index.js";
import { TextDirections } from "../text-direction/index.js";
import { ImageComponent } from "./component.js";
import { tiptapKeys } from "@notesnook/common";
export interface ImageOptions {
inline: boolean;
@@ -199,7 +200,7 @@ export const ImageNode = Node.create<ImageOptions>({
addKeyboardShortcuts() {
return {
"Mod-Shift-I": () =>
[tiptapKeys.addImage.keys]: () =>
this.editor.storage.openAttachmentPicker?.("image") || true
};
}

View File

@@ -22,6 +22,7 @@ import { isInTable } from "@tiptap/pm/tables";
import { CodeBlock } from "../code-block/index.js";
import { showLinkPopup } from "../../toolbar/popups/link-popup.js";
import { isListActive } from "../../utils/list.js";
import { tiptapKeys } from "@notesnook/common";
export const KeyMap = Extension.create({
name: "key-map",
@@ -42,7 +43,7 @@ export const KeyMap = Extension.create({
if (isListActive(editor)) return false;
return true;
},
"Mod-\\": ({ editor }) => {
[tiptapKeys.removeFormattingInSelection.keys]: ({ editor }) => {
editor
.chain()
.focus()
@@ -52,7 +53,7 @@ export const KeyMap = Extension.create({
.run();
return true;
},
"Shift-Mod-L": ({ editor }) => {
[tiptapKeys.insertInternalLink.keys]: ({ editor }) => {
editor.storage.createInternalLink?.().then((link) => {
if (!link) return;
@@ -67,7 +68,7 @@ export const KeyMap = Extension.create({
});
return true;
},
"Shift-Mod-k": ({ editor }) => {
[tiptapKeys.insertLink.keys]: ({ editor }) => {
showLinkPopup(editor);
return true;
}

View File

@@ -232,9 +232,10 @@ export const Link = Mark.create<LinkOptions>({
}
}),
markPasteRule({
find: (text) => {
find: (text, ev) => {
const foundLinks: PasteRuleMatch[] = [];
const html = ev?.clipboardData?.getData("text/html");
if (html && html.includes("<a")) return [];
if (text) {
const links = find(text).filter((item) => item.isLink);

View File

@@ -1,5 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`paste text > with html link 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" spellcheck="false" href="nn://note/68798972093c8c6ea22efca2">example.py</a></p></div></div>"`;
exports[`paste text > with markdown link 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" spellcheck="false" href="example.com">test</a></p></div></div>"`;
exports[`paste text > with multiple markdown links 1`] = `"<div><div contenteditable="true" translate="no" class="tiptap ProseMirror" tabindex="0"><p><a target="_blank" rel="noopener noreferrer nofollow" spellcheck="false" href="example.com">test</a> some text <a target="_blank" rel="noopener noreferrer nofollow" spellcheck="false" href="example2.com">test2</a></p></div></div>"`;

View File

@@ -114,4 +114,33 @@ describe("paste text", () => {
expect(editorElement.outerHTML).toMatchSnapshot();
});
test("with html link", async () => {
const editorElement = h("div");
const { editor } = createEditor({
element: editorElement,
extensions: {
link: Link
}
});
const clipboardEvent = new Event("paste", {
bubbles: true,
cancelable: true,
composed: true
});
(clipboardEvent as unknown as any)["clipboardData"] = {
getData: (type: string) =>
type === "text/html"
? `<meta charset='utf-8'><html><head></head><body><a href="nn://note/68798972093c8c6ea22efca2">example.py</a></body></html>`
: ""
};
editor.view.dom.dispatchEvent(clipboardEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(editorElement.outerHTML).toMatchSnapshot();
});
});

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Node, mergeAttributes } from "@tiptap/core";
import { insertMathNode } from "./plugin/index.js";
import { NodeSelection } from "prosemirror-state";
import { tiptapKeys } from "@notesnook/common";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
@@ -66,7 +67,8 @@ export const MathBlock = Node.create({
addKeyboardShortcuts() {
return {
"Mod-Shift-M": () => this.editor.commands.insertMathBlock()
[tiptapKeys.insertMathBlock.keys]: () =>
this.editor.commands.insertMathBlock()
};
},

View File

@@ -24,6 +24,7 @@ import {
} from "@tiptap/core";
import { findParentNodeOfTypeClosestToPos } from "../../utils/prosemirror.js";
import { OutlineList } from "../outline-list/outline-list.js";
import { keybindings, tiptapKeys } from "@notesnook/common";
export interface ListItemOptions {
HTMLAttributes: Record<string, unknown>;
@@ -75,7 +76,7 @@ export const OutlineListItem = Node.create<ListItemOptions>({
addKeyboardShortcuts() {
return {
"Mod-Space": ({ editor }) => {
[tiptapKeys.toggleOutlineListExpand.keys]: ({ editor }) => {
const { selection } = editor.state;
const { $from, empty } = selection;
@@ -104,7 +105,8 @@ export const OutlineListItem = Node.create<ListItemOptions>({
return this.editor.commands.splitListItem(this.name);
},
Tab: () => this.editor.commands.sinkListItem(this.name),
"Shift-Tab": () => this.editor.commands.liftListItem(this.name)
[keybindings.liftListItem.keys]: () =>
this.editor.commands.liftListItem(this.name)
};
},

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { tiptapKeys } from "@notesnook/common";
import { getParentAttributes } from "../../utils/prosemirror.js";
import { Node, mergeAttributes, wrappingInputRule } from "@tiptap/core";
@@ -100,7 +101,8 @@ export const OutlineList = Node.create<OutlineListOptions>({
addKeyboardShortcuts() {
return {
"Mod-Shift-O": () => this.editor.commands.toggleOutlineList()
[tiptapKeys.toggleOutlineList.keys]: () =>
this.editor.commands.toggleOutlineList()
};
},

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { keybindings } from "@notesnook/common";
import { Editor, mergeAttributes, Node } from "@tiptap/core";
import { NodeType } from "prosemirror-model";
@@ -120,7 +121,8 @@ export const Paragraph = Node.create<ParagraphOptions>({
createParagraph(editor, this.type, false, true),
"Shift-Enter": ({ editor }) =>
createParagraph(editor, this.type, false, true),
"Mod-Alt-0": () => this.editor.commands.setParagraph()
[keybindings.insertParagraph.keys]: () =>
this.editor.commands.setParagraph()
};
}
});

View File

@@ -27,6 +27,7 @@ import {
TextSelection
} from "prosemirror-state";
import { SearchSettings } from "../../toolbar/stores/search-store.js";
import { tiptapKeys } from "@notesnook/common";
type DispatchFn = (tr: Transaction) => void;
declare module "@tiptap/core" {
@@ -354,7 +355,8 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
addKeyboardShortcuts() {
return {
"Mod-f": ({ editor }) => editor.commands.startSearch(),
[tiptapKeys.openSearch.keys]: ({ editor }) =>
editor.commands.startSearch(),
Escape: ({ editor }) => editor.commands.endSearch()
};
},

View File

@@ -185,3 +185,20 @@ test(`sorting a task list with no checked items should do nothing`, async () =>
expect(eq(editor.state.doc, beforeDoc)).toBe(true);
});
test("sorting a task list should preserve cursor location", async () => {
const { editor } = createEditor({
initialContent: NESTED_TASK_LIST,
extensions: {
taskItem: TaskItemNode.configure({ nested: true }),
taskList: TaskListNode
}
});
const beforeFrom = editor.state.selection.from;
const beforeTo = editor.state.selection.to;
editor.commands.command(({ tr }) => !!sortList(tr, 0));
expect(editor.state.selection.from).toBe(beforeFrom);
expect(editor.state.selection.to).toBe(beforeTo);
});

View File

@@ -40,6 +40,7 @@ import {
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { TaskItemNode } from "../task-item/index.js";
import { ListItem } from "../list-item/list-item.js";
import { tiptapKeys } from "@notesnook/common";
type TaskListStats = { checked: number; total: number };
export type TaskListAttributes = {
@@ -368,7 +369,8 @@ export const TaskListNode = TaskList.extend({
addKeyboardShortcuts() {
return {
"Mod-Shift-T": () => this.editor.commands.toggleTaskList()
[tiptapKeys.toggleTaskList.keys]: () =>
this.editor.commands.toggleTaskList()
};
}
});

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import TaskList from "@tiptap/extension-task-list";
import { Transaction } from "@tiptap/pm/state";
import { TextSelection, Transaction } from "@tiptap/pm/state";
import { Fragment, Node as ProsemirrorNode } from "prosemirror-model";
import { NodeWithPos } from "@tiptap/core";
import { findParentNodeClosestToPos } from "../../utils/prosemirror.js";
@@ -74,11 +74,13 @@ export function deleteCheckedItems(tr: Transaction, pos: number) {
}
export function sortList(tr: Transaction, pos: number) {
const originalFrom = tr.selection.from;
const originalTo = tr.selection.to;
const node = tr.doc.nodeAt(pos);
const parent = node ? { node, pos } : null;
if (!parent || parent.node.type.name !== TaskList.name) return;
const sublists: NodeWithPos[] = [];
parent.node.descendants((node, nodePos) => {
if (node.type.name === TaskList.name)
@@ -86,16 +88,13 @@ export function sortList(tr: Transaction, pos: number) {
});
if (sublists.length > 1) sublists.reverse();
sublists.push(parent);
for (const list of sublists) {
const listNode = tr.doc.nodeAt(tr.mapping.map(list.pos));
if (!listNode) continue;
const children: {
checked: number;
index: number;
}[] = [];
listNode.forEach((node, _, index) => {
children.push({
index,
@@ -108,7 +107,6 @@ export function sortList(tr: Transaction, pos: number) {
children.every((a) => a.checked === 0)
)
continue;
tr.replaceWith(
tr.mapping.map(list.pos + 1),
tr.mapping.map(list.pos + listNode.nodeSize - 1),
@@ -121,6 +119,8 @@ export function sortList(tr: Transaction, pos: number) {
}
if (!tr.steps.length) return null;
//preserve cursor location
tr.setSelection(TextSelection.create(tr.doc, originalFrom, originalTo));
return tr;
}

View File

@@ -30,6 +30,7 @@ import { showPopup } from "../../components/popup-presenter/index.js";
import { ImageUploadPopup } from "../popups/image-upload.js";
import { Button } from "../../components/button.js";
import { strings } from "@notesnook/intl";
import { keybindings } from "@notesnook/common";
export function InsertBlock(props: ToolProps) {
const { editor } = props;
@@ -114,7 +115,7 @@ const codeblock = (editor: Editor): MenuItem => ({
icon: Icons.codeblock,
isChecked: editor.isActive("codeBlock"),
onClick: () => editor.chain().focus().toggleCodeBlock().run(),
modifier: "Mod-Shift-C"
modifier: keybindings.toggleCodeBlock.keys
});
const blockquote = (editor: Editor): MenuItem => ({
@@ -124,7 +125,7 @@ const blockquote = (editor: Editor): MenuItem => ({
icon: Icons.blockquote,
isChecked: editor.isActive("blockQuote"),
onClick: () => editor.chain().focus().toggleBlockquote().run(),
modifier: "Mod-Shift-B"
modifier: keybindings.insertBlockquote.keys
});
const mathblock = (editor: Editor): MenuItem => ({
@@ -134,7 +135,7 @@ const mathblock = (editor: Editor): MenuItem => ({
icon: Icons.mathBlock,
isChecked: editor.isActive("mathBlock"),
onClick: () => editor.chain().focus().insertMathBlock().run(),
modifier: "Mod-Shift-M"
modifier: keybindings.insertMathBlock.keys
});
const callout = (editor: Editor): MenuItem => ({
@@ -181,7 +182,7 @@ const image = (editor: Editor, isMobile: boolean): MenuItem => ({
title: strings.uploadFromDisk(),
icon: Icons.upload,
onClick: () => editor.storage.openAttachmentPicker?.("image"),
modifier: "Mod-Shift-I"
modifier: keybindings.addImage.keys
},
{
key: "camera",
@@ -284,7 +285,7 @@ const attachment = (editor: Editor): MenuItem => ({
icon: Icons.attachment,
isChecked: editor.isActive("attachment"),
onClick: () => editor.storage.openAttachmentPicker?.("file"),
modifier: "Mod-Shift-A"
modifier: keybindings.addAttachment.keys
});
const tasklist = (editor: Editor): MenuItem => ({
@@ -294,7 +295,7 @@ const tasklist = (editor: Editor): MenuItem => ({
icon: Icons.checkbox,
isChecked: editor.isActive("taskList"),
onClick: () => editor.chain().focus().toggleTaskList().run(),
modifier: "Mod-Shift-T"
modifier: keybindings.toggleTaskList.keys
});
const outlinelist = (editor: Editor): MenuItem => ({
@@ -304,7 +305,7 @@ const outlinelist = (editor: Editor): MenuItem => ({
icon: Icons.outlineList,
isChecked: editor.isActive("outlineList"),
onClick: () => editor.chain().focus().toggleOutlineList().run(),
modifier: "Mod-Shift-O"
modifier: keybindings.toggleOutlineList.keys
});
const uploadImageFromURLMobile = (editor: Editor): MenuItem => ({

View File

@@ -5301,6 +5301,14 @@ msgstr "Scan the QR code with your authenticator app"
msgid "School work"
msgstr "School work"
#: src/strings.ts:2491
msgid "Scroll to bottom"
msgstr "Scroll to bottom"
#: src/strings.ts:2490
msgid "Scroll to top"
msgstr "Scroll to top"
#: src/strings.ts:1509
#: src/strings.ts:1527
msgid "Search"
@@ -7107,6 +7115,10 @@ msgstr "Your data recovery key will be used to decrypt your data"
msgid "Your email has been confirmed."
msgstr "Your email has been confirmed."
#: src/strings.ts:2493
msgid "Your email has been confirmed. You can now securely sync your encrypted notes across all devices."
msgstr "Your email has been confirmed. You can now securely sync your encrypted notes across all devices."
#: src/strings.ts:765
msgid "Your email is not confirmed. Please confirm your email address to change account password."
msgstr "Your email is not confirmed. Please confirm your email address to change account password."

View File

@@ -5275,6 +5275,14 @@ msgstr ""
msgid "School work"
msgstr ""
#: src/strings.ts:2491
msgid "Scroll to bottom"
msgstr ""
#: src/strings.ts:2490
msgid "Scroll to top"
msgstr ""
#: src/strings.ts:1509
#: src/strings.ts:1527
msgid "Search"
@@ -7053,6 +7061,10 @@ msgstr ""
msgid "Your email has been confirmed."
msgstr ""
#: src/strings.ts:2493
msgid "Your email has been confirmed. You can now securely sync your encrypted notes across all devices."
msgstr ""
#: src/strings.ts:765
msgid "Your email is not confirmed. Please confirm your email address to change account password."
msgstr ""

View File

@@ -2486,5 +2486,9 @@ Use this if changes from other devices are not appearing on this device. This wi
unarchive: () => t`Unarchive`,
moveNotebookDesc: () =>
t`Select a notebook to move this notebook into, or unselect to move it to the root level.`,
noNotebooksSelectedToMove: () => t`No notebooks selected to move`
noNotebooksSelectedToMove: () => t`No notebooks selected to move`,
scrollToTop: () => t`Scroll to top`,
scrollToBottom: () => t`Scroll to bottom`,
emailConfirmedDesc: () =>
t`Your email has been confirmed. You can now securely sync your encrypted notes across all devices.`
};