Compare commits

...

131 Commits

Author SHA1 Message Date
Ammar Ahmed
c8abab1457 editor: do not use look behind regex 2025-03-28 12:17:41 +05:00
Abdullah Atta
b618386e6b web: clear caches on switching release track 2025-03-26 15:30:12 +05:00
Abdullah Atta
2e79b2c7a2 core: run trash cleanup after sync completion 2025-03-26 13:44:16 +05:00
01zulfi
4e4990bd98 web: add toggle focus mode command (#7736)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-25 12:09:19 +05:00
Abdullah Atta
4c8a44353a web: bump version to 3.0.31 2025-03-24 14:32:27 +05:00
Abdullah Atta
7dd8b13bc8 web: fix tests 2025-03-24 14:32:27 +05:00
01zulfi
e1aa3b59be web: add reset zoom button (#7750)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-24 12:36:01 +05:00
01zulfi
d79888793a web: mask email in session expired page description (#7783)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-24 12:34:57 +05:00
Abdullah Atta
b56e381022 web: fix editor cursor jumping to start on creating a new note 2025-03-24 12:28:30 +05:00
Ammar Ahmed
15b32b312d ci: use actions/cache v3
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-03-17 16:39:44 +05:00
Ammar Ahmed
6a0a00f126 ci: use actions/checkout v3
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-03-17 16:38:25 +05:00
Ammar Ahmed
9876c2ef6d mobile: bump version 2025-03-17 16:34:07 +05:00
Ammar Ahmed
fba29e3c80 mobile: fix sharing image from ios share sheet 2025-03-17 16:25:08 +05:00
Ammar Ahmed
b8275f5a3f Fix creating backups ios (#7777)
* mobile: fix copying file from app group during backup

* mobile: catch and show error if copying file fails during backup
2025-03-17 16:24:04 +05:00
01zulfi
b7e8365117 web: increase reminder dialog width (#7767)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-14 12:24:21 +05:00
Ammar Ahmed
8df7b5c56b web: fix editor rerendering on session type change from new to default (#7759)
* web: fix editor rerendering on session type change from new to default

* web: fix multiple notes created on typing new note
2025-03-14 11:57:16 +05:00
01zulfi
04519d1db7 editor: fix scroll into view when used by toc (#7764)
* add scroll-margin-top: 35px

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-14 11:55:55 +05:00
Ammar Ahmed
7b808bea0b web: fix ui flicker in editor when creating new note (#7761) 2025-03-12 13:16:13 +05:00
Ammar Ahmed
2878f1d9cf editor: ensure consistent gap between heading and first element in callout (#7757) 2025-03-12 13:11:14 +05:00
Ammar Ahmed
651f6011b8 desktop: release v3.0.30 2025-03-06 15:56:02 +05:00
Ammar Ahmed
f17cbb5aa3 web: release v3.0.30 2025-03-06 15:55:51 +05:00
Ammar Ahmed
a2a4a165a5 editor: fix item margins in editor (#7744) 2025-03-06 15:49:51 +05:00
Ammar Ahmed
9c7ff38f99 editor: fix image padding in readonly mode (#7743)
* editor: fix image padding in readonly mode

* editor: fix class name
2025-03-06 15:46:03 +05:00
01zulfi
76f27adbea web: command palette fixes (#7669)
* web: command palette fixes
* core: add getIdentifier in fuzzy; this allows the caller to provide a custom indentifier instead of relying on id field in the items list
* fix crashing when hitting enter when no results found
* fix commands list not refreshing when removing recent command
* fix recent command not showing up in the commands list
* fix searched commands showing up under wrong group label

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

* web: improve getCommandById && improve remove from recents logic
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-04 13:11:26 +05:00
01zulfi
85a5f38590 web: adding notes in tag/color should also add it to default notebook (#7732)
* when default notebook is set, adding notes in tag and color context should also add that note to default notebook
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-04 13:00:43 +05:00
01zulfi
dcdd876a4c web: align note title with rest of ui (#7731)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-04 09:54:57 +05:00
Abdullah Atta
d055908809 ci: build in beta mode when release track is beta 2025-03-01 15:31:28 +05:00
Abdullah Atta
89806148b2 ci: run apt-get update before installing flatpak etc 2025-03-01 14:55:39 +05:00
Abdullah Atta
433be12b2b desktop: hide release track selector for flatpak/snap builds 2025-03-01 14:49:38 +05:00
Abdullah Atta
9ff235e767 ci: change working-directory to ./apps/web before deployment 2025-03-01 13:19:18 +05:00
Abdullah Atta
9b6f7ff225 web: fix release track switching 2025-03-01 13:13:25 +05:00
Abdullah Atta
99f97ac565 web: fix release-track cookie not getting set 2025-03-01 11:41:37 +05:00
Abdullah Atta
7e598098fe ci: add support for publishing web app beta 2025-03-01 11:15:03 +05:00
Abdullah Atta
02afbf6c90 web: only redirect to beta if env.BETA_BASE_URL is present 2025-03-01 11:07:40 +05:00
Abdullah Atta
b464fafc18 web: bump version to 3.0.29 2025-03-01 11:05:26 +05:00
Abdullah Atta
e06320108b web: fix gap between title and editor on entering focus mode 2025-03-01 10:50:38 +05:00
Abdullah Atta
a377b13e2b editor: fix single & double spaced paragraph margins 2025-03-01 10:40:27 +05:00
Abdullah Atta
4728212531 core: fix tags & notebooks search 2025-03-01 10:23:58 +05:00
01zulfi
71fc2a3cac web: add keyboard shortcut for opening settings (#7701)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-01 09:31:23 +05:00
01zulfi
303282624e editor: hide hover popup if right click occurs before popup is shown (#7687)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-01 09:23:26 +05:00
01zulfi
2ca02937df editor: open internal links in new tab for middle mouse click (#7688)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-01 09:22:47 +05:00
Abdullah Atta
00b62df2e8 desktop: fix tests on windows 2025-02-28 18:03:05 +05:00
Abdullah Atta
6d377ded9a desktop: set APPIMAGE to true when testing linux to allow auto updater to work 2025-02-28 18:03:05 +05:00
Abdullah Atta
087c404225 desktop: fix tests on linux 2025-02-28 18:03:05 +05:00
Abdullah Atta
4932763468 desktop: extend test timeout to 2 minutes 2025-02-28 18:03:05 +05:00
Abdullah Atta
abcc19d5da desktop: skip tsc build during tests 2025-02-28 18:03:05 +05:00
Abdullah Atta
f1ce1e9b60 desktop: enable staging mode during tests to disable code signing 2025-02-28 18:03:05 +05:00
Abdullah Atta
5a6f99a41b desktop: double test timeout on CI 2025-02-28 18:03:05 +05:00
Abdullah Atta
1862802052 desktop: pass --config arg to electron-builder everywhere 2025-02-28 18:03:05 +05:00
Abdullah Atta
1adddbdb4c desktop: add more tests for auto updater 2025-02-28 18:03:05 +05:00
Abdullah Atta
15f4f2accb desktop: add support for switching release tracks
this also includes relevant tests for the auto updater
2025-02-28 18:03:05 +05:00
Abdullah Atta
725f3c3522 web: add support for switching release tracks
we make use of Cloudflare Pages Functions to redirect to the beta site
if a certain cookie is present. This ensures seamless switching between
release tracks.
2025-02-28 18:03:05 +05:00
01zulfi
efa73a1c32 web: update toast position (#7665)
* current position would overlap slightly with the status bar, this commit moves the toast a little higher so it doesn't overlap
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-25 13:21:57 +05:00
Abdullah Atta
00250c6669 web: bump version to v3.0.28 2025-02-25 11:41:46 +05:00
01zulfi
d11afda1a8 web: fix attachment table height for few items (#7662)
* assign min-height to container
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-25 11:39:44 +05:00
Abdullah Atta
1350784c33 editor: allow other permissions to iframe embeds 2025-02-25 11:00:52 +05:00
Abdullah Atta
1852a216a2 editor: fix Applying mismatched transaction errors on editor load
fixes #7656
2025-02-25 11:00:31 +05:00
Abdullah Atta
422c2492c4 web: fix react error on feature dialog 2025-02-25 10:10:06 +05:00
Abdullah Atta
c9c72741b3 desktop: fix app icon size on macos 2025-02-24 12:58:44 +05:00
Abdullah Atta
99dbcaff7d web: make tabs tests more resilient 2025-02-24 12:19:21 +05:00
Abdullah Atta
8d83f5474d web: fix editor tests 2025-02-24 11:53:51 +05:00
Abdullah Atta
a912a1a22e web: fix invalid usage of sessionId as noteId 2025-02-24 11:53:41 +05:00
Abdullah Atta
ea7afbe93e web: fix invalid usage of closeSessions 2025-02-24 11:53:18 +05:00
Abdullah Atta
7b2a8c3e20 web: fix executing recent command does nothing
fixes #7650
2025-02-24 11:16:38 +05:00
Abdullah Atta
911adfc151 web: fix reloading app loses the expanded size of a collapsed pane 2025-02-24 11:11:03 +05:00
Abdullah Atta
23f710856e web: fix collapsed pane cannot be resized 2025-02-24 11:10:37 +05:00
Abdullah Atta
7f6274821c web: fix title auto resizing on firefox 2025-02-24 10:48:11 +05:00
Abdullah Atta
4700843517 web: fix note content loading in editor
this broke when we migrated to the new tabs logic and was fixed by
using `editor.updateContent` which is significantly slower than
creating a new editor/editor view. This commit fixes that by
depending on the `getContent` function which changes whenever the
content is changed in a session handling all kinds of different
cases.
2025-02-24 10:38:37 +05:00
Abdullah Atta
8823054227 editor: disable suspiciousAttributes check in prosemirror
this check causes a significant slowdown when loading notes with huge
amount of nodes as it recursively scans the attributes of all the
nodes. Disabling this speeds up loading significantly but might
cause issues in some special cases. Fortunately, this error has never
been triggered by anyone in Notesnook. I wouldn't recommend anyone to
disable this without first testing it properly as it might expose you
to cross site scripting attacks.
2025-02-24 10:36:18 +05:00
Abdullah Atta
7c63a3e004 editor: optimize @tiptap/core
this optimizes the following functions in @tiptap/core:

- `mergeAttributes`
- `getRenderedAttributes`
- `injectExtensionAttributesToParseRule`

These functions stood out the most in the profiler.
2025-02-24 10:33:13 +05:00
Abdullah Atta
2a59154268 editor: improve note loading performance
this commit gets rid of the custom logic when loading a new note in
the same editor. Instead, we just create a new view in the same
editor which seems to be much faster then just replacing the state
of the old view.
2025-02-24 10:31:35 +05:00
Abdullah Atta
ed15d53484 editor: fix block-id extension slowing down pasting
we were using `tr.setNodeMarkup` which created a new step for each
node. If there were a lot of nodes, it'd cause an exponential slow
down. As an alternative, we are directly updating the node's blockId
attribute which is against the spec but it works and it's fast.
2025-02-22 09:42:33 +05:00
Abdullah Atta
fc8a4bb55d ci: run desktop build in ubuntu-24.04 2025-02-21 15:07:53 +05:00
Abdullah Atta
dd1c1bb11d ci: use apt-get with --quiet 2025-02-21 15:03:10 +05:00
Abdullah Atta
3ffbad5727 theme-builder: make theme builder compile again 2025-02-21 14:52:38 +05:00
Abdullah Atta
207ae39d13 ci: run web tests on 5 shards (instead of 4) 2025-02-21 14:34:02 +05:00
Abdullah Atta
ae141ac9c5 web: fix control + alt + right/left arrow tests 2025-02-21 14:26:50 +05:00
Abdullah Atta
40f058b14b web: bump version to v3.0.27 2025-02-21 13:13:53 +05:00
Abdullah Atta
865c9f3e32 web: remove check that compares note title with session title 2025-02-21 13:10:27 +05:00
Abdullah Atta
26df9bfdd8 web: fix editing a note and switching immediately to another note and editing the title shouldn't overlap both notes test 2025-02-21 13:04:53 +05:00
Abdullah Atta
1e52e01fb8 web: fix debug strings showing in production builds 2025-02-21 12:39:44 +05:00
01zulfi
5fece5faaa web: fix note title sync if note is opened in multiple tabs (#7636)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-21 12:36:07 +05:00
01zulfi
d7fdaf6c46 ui: fix submenu positioning (#7632)
position.top wasn't calculated correctly for the out of screen height case for non-mouse target

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-21 12:34:10 +05:00
Abdullah Atta
8e1faf9f60 web: command palette improvements
This includes:

- Full refactoring of the command palette
- Remove command transformation logic for dynamic
commands
- Replace note, notebook & tag specific commands
with a simple menu item -> command conversion logic
- Cached filtering (with notes, notebooks etc.
refetching on sync)
- Add support for fuzzy searching on command group
- Modernize UI to look more like a command palette and less like a dialog
2025-02-21 12:29:25 +05:00
Abdullah Atta
9dce39b32d core: reuse fuzzy search logic 2025-02-21 12:29:25 +05:00
Abdullah Atta
80a0b75887 core: remove matchOnly logic from fuzzy searcher
Search should always filter out the results, not return everything.
2025-02-21 12:29:25 +05:00
Abdullah Atta
c9704e1031 core: remove titleOnly lookup tests 2025-02-21 12:29:25 +05:00
Abdullah Atta
31e32cd154 core: fix query transformer tests 2025-02-21 12:29:25 +05:00
Abdullah Atta
61c296fc37 core: improve fuzzy filter lookup api to be more robust
- add ability to wrap matching text in prefix/suffix
- return match score for further sorting etc.
- fetch everything eagerly (instead of iterating on matches) for perf
- add support for ignoring fields (i.e. ignore id field)
2025-02-21 12:29:25 +05:00
Abdullah Atta
c8d38235b2 web: close command palette if already open on Ctrl/Cmd+K/P 2025-02-21 12:29:25 +05:00
Abdullah Atta
b717a64607 web: add support for closing dialogs imperatively 2025-02-21 12:29:25 +05:00
01zulfi
0f2b1d3e14 web: fix tags not adding/removing in note header bar (#7623)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-20 13:02:49 +05:00
Ammar Ahmed
4abefe63db core: fix error can't resolve './fuzzy' 2025-02-20 10:49:03 +05:00
Abdullah Atta
fc31b2a8d7 web: pressing escape key shouldn't exit fullscreen mode in firefox 2025-02-19 16:04:21 +05:00
01zulfi
2708bac4fb web: wrap & remove auto-resize of note title (#7290)
* web: wrap & remove auto-resize of note title
* wrap note title if it overflows
* remove auto-resizing of note title based on its length
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: remove newlines in title textarea

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2025-02-19 15:56:36 +05:00
01zulfi
a47967dd53 web: add command palette (#7314)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-19 15:48:44 +05:00
01zulfi
53ba5562d3 intl: fix account casing (#7624)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-19 14:13:49 +05:00
three-b0dy
347df32943 monograph: fix docker build for aarch64 (#7622)
--platform=$BUILDPLATFORM will overwrite buildx settings

Signed-off-by: three-b0dy <120573565+three-b0dy@users.noreply.github.com>
2025-02-19 14:03:52 +05:00
01zulfi
ac8c63526b editor: add scroll margin & threshold props (#7292)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-18 12:27:55 +05:00
01zulfi
a48bd360ca editor: improve hr & p styling (#7489)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-17 12:10:10 +05:00
Ammar Ahmed
f476969e9e mobile: pinned tab changes 2025-02-17 11:58:26 +05:00
Abdullah Atta
c45539dea6 web: add tests for pinned tabs 2025-02-17 11:58:26 +05:00
Abdullah Atta
d8f5815b84 web: disable opening new note in pinned tab 2025-02-17 11:58:26 +05:00
Abdullah Atta
2eaf07118a web: open note history in new tab if active tab is pinned 2025-02-17 11:58:26 +05:00
Abdullah Atta
b636b09a96 web: disable all kinds of navigation in pinned tabs 2025-02-17 11:58:26 +05:00
Abdullah Atta
c16bbcaad5 editor: add support for pasting as markdown 2025-02-17 11:57:24 +05:00
Abdullah Atta
49ffcbea03 editor: fix youtube embeds 2025-02-17 10:30:12 +05:00
Waqar Ahmed
f772cf0f14 editor: fix search in complex elements like tables (#7605)
The current search algorithm while accumulating text assumes that the
text nodes are always followed by a paragraph node with endPos ==
textNode.endPos + 1. This is not true for complex text elements like
tables where the endPos can be higher. Thus the start,end offsets of
TextNodesWithPosition start getting off and results in incorrectly
highlighted search results.

To fix this, simplify the TextNodesWithPosition to only hold start
position and offset since startPos is always correct. We can calculate
the end position on the fly by simply adding text length to startPos.

Issue #7431

Signed-off-by: Waqar Ahmed <waqar.17a@gmail.com>
2025-02-17 10:16:55 +05:00
Abdullah Atta
3f1406a05b monograph: bump version to v1.2.4 2025-02-15 23:39:37 +05:00
Abdullah Atta
21760b2b74 monograph: add a strict csp 2025-02-15 23:38:52 +05:00
Ammar Ahmed
d8011f0239 mobile: release v3.0.31 2025-02-14 15:07:12 +05:00
Ammar Ahmed
b9a8b054e2 Fix sharing screenshot on iOS does not work (#7589) 2025-02-14 15:05:18 +05:00
Abdullah Atta
2ab321869e web: bump version to v3.0.26 2025-02-14 10:08:43 +05:00
Abdullah Atta
fcb45a24d6 monograph: use latest version of bun in prod 2025-02-14 09:57:07 +05:00
Abdullah Atta
7cdb13def6 Merge pull request #7588 from streetwriters/fix/tighten-iframe-security
This fixes a potential security vulnerability where pasting unknown content into the editor could create an RCE risk.

This PR fixes two issues:

1. Potential RCE when pasting/inserting an `iframe` containing a `javascript` link.
2. Potential RCE when pasting/inserting an `svg` containing JavaScript (why do SVGs allow JS in the first place?).

Mitigations include disallowing all execution of JS inside an SVG by rendering it in a sandboxed `iframe`. While we cannot disallow JS execution in embeds (that would break all embeds like YouTube videos), we have disallowed access to the parent window to all `iframe`s, again, by using a sandboxed `iframe` and by disallowing embedding of `javascript:` links.

To be clear, both of these issues can only be triggered when pasting/importing untrusted content (which you shouldn't be doing anyway).

**These cannot be used to steal or access your notes or any other data. They could be used to access what's shown in the window or do automated clicks etc. but since everything is stored and access from an encrypted SQLite database, your data would be 100% safe and isolated from such an attack.**
2025-02-14 09:50:43 +05:00
Abdullah Atta
2ab58f9203 editor: use sandboxed iframe to render SVGs 2025-02-14 09:31:06 +05:00
Abdullah Atta
df74448e17 editor: disallow embedding javascript code in iframes 2025-02-13 20:27:21 +05:00
01zulfi
ada98fca93 web: hide undo & redo buttons for readonly notes (#7541)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-13 10:13:44 +05:00
Yash Kumar
1d3650659f editor: select language by pressing enter (#7484)
Signed-off-by: Yash Kumar <kyash03@student.ubc.ca>
2025-02-13 10:12:37 +05:00
Ammar Ahmed
9b43af475b mobile: release v3.0.31 2025-02-11 14:01:11 +05:00
Ammar Ahmed
d9c592c7fb common: always remove the last session from history (#7555) 2025-02-11 13:31:39 +05:00
Ammar Ahmed
f10ac9fbb5 Merge pull request #7556 from streetwriters/fix-tabs-mobile
Fix tabs on mobile
2025-02-11 13:12:12 +05:00
Ammar Ahmed
b7334c09d4 Merge branch 'master' into fix-tabs-mobile
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-02-11 13:11:55 +05:00
Ammar Ahmed
0cd3bd5739 mobile: fix dialog calling onClose after pressing positive button 2025-02-11 13:11:13 +05:00
Ammar Ahmed
33d69480eb mobile: fix ts errors 2025-02-11 13:11:02 +05:00
Ammar Ahmed
5cae4eb0d9 mobile: add null checks before setting items on storage 2025-02-11 12:18:28 +05:00
Ammar Ahmed
840c7fda5e mobile: fix loading placeholder 2025-02-11 12:17:45 +05:00
Ammar Ahmed
4ce24ac8fd mobile: do not show sheet in uncaught errors in editor 2025-02-11 12:17:11 +05:00
Ammar Ahmed
ae9e80e12e mobile: fix stuck at loading in editor 2025-02-11 12:16:51 +05:00
luis-411
43356e5b6a core: escape special characters in SQL search query (#7418)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-02-11 11:00:51 +05:00
xa4hf8
7b13f35424 desktop: replace corrupt Mac icons bundle (#7542)
There is a long-standing bug in Electron builder that generates corrupt Macintosh icon bundles. The issue is visible in Finder "list" view.  This file is correct and was created on Mac with https://github.com/alptugan/icns-creator/

Signed-off-by: xa4hf8 <174972649+xa4hf8@users.noreply.github.com>
2025-02-11 10:54:50 +05:00
145 changed files with 12093 additions and 5370 deletions

View File

@@ -33,11 +33,19 @@ on:
required: true
default: true
description: "Build for macOS?"
release-track:
type: choice
required: true
default: stable
description: "Select the release track"
options:
- stable
- beta
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -61,9 +69,14 @@ jobs:
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate desktop build
- name: Generate desktop build (stable)
if: ${{ inputs.release-track == 'stable' }}
run: npx nx build:desktop @notesnook/web
- name: Generate desktop build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: BETA=true npx nx build:desktop @notesnook/web
- name: Build desktop bundle
working-directory: ./apps/desktop
run: npm run bundle
@@ -99,7 +112,8 @@ jobs:
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
run: |
sudo apt-get install -y flatpak flatpak-builder pipx
sudo apt-get update
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
cd /tmp/flatpak-builder-tools/node
pipx install .
@@ -185,7 +199,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npx nx run release --project @notesnook/desktop -- --variant=mas
yarn electron-builder --mac mas --universal -p never
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
working-directory: ./apps/desktop
- name: Build zip and dmg
@@ -199,9 +213,9 @@ jobs:
run: |
npx nx run release --project @notesnook/desktop
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --mac zip dmg --arm64 --x64 -p always
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p always
else
yarn electron-builder --mac zip dmg --arm64 --x64 -p never
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p never
fi
working-directory: ./apps/desktop
@@ -263,7 +277,7 @@ jobs:
- name: Build snap
if: inputs.publish-snap
run: |
yarn electron-builder --linux snap:x64 -p never
yarn electron-builder --config=electron-builder.config.js --linux snap:x64 -p never
working-directory: ./apps/desktop
- name: Build AppImage
@@ -271,9 +285,9 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p always
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -p always
else
yarn electron-builder --linux AppImage:x64 AppImage:arm64 -p never
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
@@ -333,8 +347,8 @@ jobs:
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: |
if ($${{ inputs.publish-github }} -eq $true) {
yarn electron-builder --win --publish always
yarn electron-builder --config=electron-builder.config.js --win --publish always
} else {
yarn electron-builder --win --publish never
yarn electron-builder --config=electron-builder.config.js --win --publish never
}
working-directory: ./apps/desktop

View File

@@ -69,12 +69,11 @@ jobs:
- name: Build app
run: |
yarn electron-builder --mac --dir --x64
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
working-directory: ./apps/desktop
- name: Run tests x64
run: |
EXECUTABLE_PATH=output/mac/Notesnook.app/Contents/MacOS/Notesnook npm run test
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results
@@ -114,12 +113,11 @@ jobs:
- name: Build app
run: |
yarn electron-builder --mac --dir --arm64
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
working-directory: ./apps/desktop
- name: Run tests arm64
run: |
EXECUTABLE_PATH=output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook npm run test
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results
@@ -165,12 +163,11 @@ jobs:
- name: Build app
run: |
yarn electron-builder --linux --dir --arm64 --x64
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: |
EXECUTABLE_PATH=output/linux-unpacked/notesnook xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
run: xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
working-directory: ./apps/desktop
- name: Upload test results
@@ -216,12 +213,11 @@ jobs:
- name: Build app
run: |
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --win --dir --arm64 --x64
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: |
npx cross-env EXECUTABLE_PATH=output/win-unpacked/Notesnook.exe npm run test
run: npm run test
working-directory: ./apps/desktop
- name: Upload test results

View File

@@ -9,7 +9,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -47,7 +47,7 @@ jobs:
ccache -p
- name: Cache Pods
uses: actions/cache@v2
uses: actions/cache@v3
id: pods-cache
with:
path: apps/mobile/native/ios/Pods

View File

@@ -1,6 +1,16 @@
name: Publish @notesnook/web
on: workflow_dispatch
on:
workflow_dispatch:
inputs:
release-track:
type: choice
required: true
default: stable
description: "Select the release track"
options:
- stable
- beta
jobs:
build:
@@ -24,8 +34,20 @@ jobs:
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate build
- name: Generate build (stable)
if: ${{ inputs.release-track == 'stable' }}
run: npm run build:web
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/
- name: Publish to Cloudflare Pages (stable)
if: ${{ inputs.release-track == 'stable' }}
working-directory: ./apps/web
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./build/
- name: Generate build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: npm run build:beta:web
- name: Publish to Cloudflare Pages (beta)
if: ${{ inputs.release-track == 'beta' }}
working-directory: ./apps/web
run: npx --yes wrangler pages deploy --branch=beta --project-name=notesnook-app-beta ./build/

View File

@@ -49,7 +49,7 @@ jobs:
name: 🧪 Test (${{ matrix.shard }}/${{ strategy.job-total }})
strategy:
matrix:
shard: [1, 2, 3, 4]
shard: [1, 2, 3, 4, 5]
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@@ -2,4 +2,6 @@ node_modules
build
output
dist
_catalog
_catalog
test-results
test-artifacts

View File

@@ -58,13 +58,13 @@ This will compile and run the app in production mode but it won't generate any p
npm run release -- --rebuild
# For macOS
npx electron-builder --mac dmg --arm64 --x64 -publish never
npx electron-builder --config=electron-builder.config.js --mac dmg --arm64 --x64 -publish never
# For Linux (AppImage)
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
npx electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -publish never
# For Windows
npx electron-builder --win --publish never
npx electron-builder --config=electron-builder.config.js --win --publish never
```
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.

View File

@@ -0,0 +1,246 @@
/*
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 } from "vitest";
import { harness } from "./utils.js";
import { writeFile } from "fs/promises";
import { Page } from "playwright";
import { gt, lt } from "semver";
test("update starts downloading if version is outdated", async (t) => {
await harness(
t,
async ({ page }) => {
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: /updating/i })
.waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
});
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
})
);
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: /available/i })
.waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
});
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"
})
);
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");
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" }
);
});
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 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" }
);
});
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();
});
}

View File

@@ -17,16 +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 test from "node:test";
import { launchApp } from "./utils.mjs";
import { test } from "vitest";
import { harness } from "./utils.js";
import assert from "assert";
import slugify from "slugify";
import path from "path";
import { mkdir } from "fs/promises";
test("make sure app loads", async (t) => {
const { app, page } = await launchApp();
try {
await harness(t, async ({ page }) => {
await page.waitForSelector("#authForm");
assert.ok(
@@ -38,16 +34,5 @@ test("make sure app loads", async (t) => {
.click();
await page.waitForSelector(".ProseMirror");
} catch (e) {
await mkdir("test-results", { recursive: true });
await page.screenshot({
path: path.join(
"test-results",
`${slugify(t.name)}-${process.platform}-${process.arch}-error.png`
)
});
throw e;
} finally {
await app.close();
}
});
});

View File

@@ -0,0 +1,172 @@
/*
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 { execSync } from "child_process";
import { mkdir } from "fs/promises";
import { fileURLToPath } from "node:url";
import path from "path";
import { _electron as electron } from "playwright";
import slugify from "slugify";
import { TaskContext } from "vitest";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
interface AppContext {
app: import("playwright").ElectronApplication;
page: import("playwright").Page;
configPath: string;
relaunch: () => Promise<void>;
}
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);
}
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> {
const productName = makeid(10);
const executablePath = await buildApp({ ...options, productName });
const { app, page, configPath } = await launchApp(executablePath);
const ctx: AppContext = {
app,
page,
configPath,
relaunch: async () => {
const { app, page, configPath } = await launchApp(executablePath);
ctx.app = app;
ctx.page = page;
ctx.configPath = configPath;
}
};
return ctx;
}
async function launchApp(executablePath: string) {
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
env:
process.platform === "linux"
? {
...(process.env as Record<string, string>),
APPIMAGE: "true"
}
: (process.env as Record<string, string>)
});
const page = await app.firstWindow();
const userDataDirectory = await app.evaluate((a) => {
return a.app.getPath("userData");
});
const configPath = path.join(userDataDirectory, "config.json");
return {
app,
page,
configPath
};
}
async function buildApp({
version,
productName
}: {
version?: string;
productName: 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"
});
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(
__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
);
}
function makeid(length: number) {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result.toLowerCase();
}

Binary file not shown.

View File

@@ -0,0 +1,200 @@
/*
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/>.
*/
const path = require("path");
const buildRoot = process.env.NN_BUILD_ROOT || ".";
const buildFiles = [
`${buildRoot}/build/`,
`!${buildRoot}/build/screenshots\${/*}`,
`!${buildRoot}/build/banner.jpg`,
`!${buildRoot}/build/*.ico`,
`!${buildRoot}/build/*.png`
];
const productName = process.env.NN_PRODUCT_NAME || "Notesnook";
const appId = process.env.NN_APP_ID || "org.streetwriters.notesnook";
const outputDir = process.env.NN_OUTPUT_DIR || "output";
const linuxExecutableName = process.env.NN_PRODUCT_NAME
? process.env.NN_PRODUCT_NAME.toLowerCase().replace(/\s+/g, "-")
: "notesnook";
const year = new Date().getFullYear();
module.exports = {
appId: appId,
productName: productName,
copyright: `Copyright © ${year} Streetwriters (Private) Limited`,
artifactName: "notesnook_${os}_${arch}.${ext}",
generateUpdatesFilesForAllChannels: true,
asar: false,
files: [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
...buildFiles,
"!node_modules${/*}",
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
"node_modules/better-sqlite3-multiple-ciphers/lib",
"node_modules/better-sqlite3-multiple-ciphers/package.json",
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sqlite-better-trigram",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
{
from: "node_modules/sqlite-better-trigram-linux-${arch}",
to: "node_modules/sqlite-better-trigram-linux-${arch}"
},
{
from: "node_modules/sqlite-better-trigram-darwin-${arch}",
to: "node_modules/sqlite-better-trigram-darwin-${arch}"
},
{
from: "node_modules/sqlite-better-trigram-windows-${arch}",
to: "node_modules/sqlite-better-trigram-windows-${arch}"
},
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
],
afterPack: "./scripts/removeLocales.js",
mac: {
bundleVersion: "240",
minimumSystemVersion: "10.12.0",
target: [
{
target: "dmg",
arch: ["arm64", "x64"]
},
{
target: "zip",
arch: ["arm64", "x64"]
}
],
category: "public.app-category.productivity",
darkModeSupport: true,
type: "distribution",
hardenedRuntime: true,
entitlements: "assets/entitlements.mac.plist",
entitlementsInherit: "assets/entitlements.mac.plist",
gatekeeperAssess: false,
icon: "assets/icons/app.icns",
notarize: true
},
dmg: {
contents: [
{
x: 130,
y: 220
},
{
x: 410,
y: 220,
type: "link",
path: "/Applications"
}
],
icon: "assets/icons/app.icns",
title: "Install Notesnook"
},
mas: {
entitlements: "assets/entitlements.mas.plist",
entitlementsInherit: "assets/entitlements.mas.inherit.plist",
entitlementsLoginHelper: "assets/entitlements.mas.loginhelper.plist",
hardenedRuntime: true
},
win: {
target: [
{
target: "nsis",
arch: ["x64", "arm64"]
},
{
target: "portable",
arch: ["x64", "arm64"]
}
],
signtoolOptions: {
signingHashAlgorithms: ["sha256"],
sign: "./scripts/sign.js"
},
icon: "assets/icons/app.ico"
},
portable: {
artifactName: "notesnook_${os}_${arch}_portable.${ext}"
},
nsis: {
oneClick: true,
createDesktopShortcut: "always",
deleteAppDataOnUninstall: true
},
linux: {
target: [
{
target: "AppImage",
arch: ["x64", "arm64"]
},
{
target: "snap",
arch: ["x64", "arm64"]
}
],
category: "Office",
icon: "assets/icons/app.icns",
description: "Your private note taking space",
executableName: linuxExecutableName,
desktop: {
actions: [
{
id: "new-note",
name: "New note",
args: "new note"
},
{
id: "new-notebook",
name: "New notebook",
args: "new notebook"
},
{
id: "new-reminder",
name: "New reminder",
args: "new reminder"
}
]
}
},
snap: {
autoStart: false,
confinement: "strict",
allowNativeWayland: true
},
extraResources: ["app-update.yml", "./assets/**"],
extraMetadata: {
main: path.join(buildRoot, "build", "electron.js")
},
directories: {
buildResources: "assets",
output: outputDir
},
publish: [
{
provider: "github",
repo: "notesnook",
owner: "streetwriters"
}
]
};

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.25",
"version": "3.0.31",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -30,7 +30,7 @@
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"better-sqlite3-multiple-ciphers": "11.5.0",
"electron-trpc": "0.6.1",
"electron-trpc": "0.7.1",
"electron-updater": "^6.3.4",
"icojs": "^0.19.4",
"sqlite-better-trigram": "0.0.2",
@@ -46,14 +46,14 @@
"electron": "^31.7.4",
"electron-builder": "^25.1.8",
"esbuild": "0.21.5",
"vitest": "2.1.8",
"node-abi": "^3.68.0",
"node-gyp-build": "^4.8.2",
"playwright": "^1.48.2",
"prebuildify": "^6.0.1",
"slugify": "1.6.6",
"tree-kill": "^1.2.2",
"undici": "^6.19.8"
"undici": "^6.19.8",
"vitest": "^2.1.8"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
@@ -63,203 +63,14 @@
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "node ../../scripts/build.mjs",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
"postinstall": "patch-package",
"test": "node --test --test-timeout 30000 --test-force-exit"
"test": "vitest run"
},
"author": {
"name": "Streetwriters (Private) Limited",
"email": "support@streetwriters.co",
"url": "https://streetwriters.co"
},
"build": {
"appId": "org.streetwriters.notesnook",
"productName": "Notesnook",
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
"artifactName": "notesnook_${os}_${arch}.${ext}",
"generateUpdatesFilesForAllChannels": true,
"asar": false,
"files": [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
"build/",
"!build/screenshots${/*}",
"!build/banner.jpg",
"!build/*.ico",
"!build/*.png",
"!node_modules${/*}",
"node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node",
"node_modules/better-sqlite3-multiple-ciphers/lib",
"node_modules/better-sqlite3-multiple-ciphers/package.json",
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sqlite-better-trigram",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
{
"from": "node_modules/sqlite-better-trigram-linux-${arch}",
"to": "node_modules/sqlite-better-trigram-linux-${arch}"
},
{
"from": "node_modules/sqlite-better-trigram-darwin-${arch}",
"to": "node_modules/sqlite-better-trigram-darwin-${arch}"
},
{
"from": "node_modules/sqlite-better-trigram-windows-${arch}",
"to": "node_modules/sqlite-better-trigram-windows-${arch}"
},
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
],
"afterPack": "./scripts/removeLocales.js",
"mac": {
"bundleVersion": "240",
"minimumSystemVersion": "10.12.0",
"target": [
{
"target": "dmg",
"arch": [
"arm64",
"x64"
]
},
{
"target": "zip",
"arch": [
"arm64",
"x64"
]
}
],
"category": "public.app-category.productivity",
"darkModeSupport": true,
"type": "distribution",
"hardenedRuntime": true,
"entitlements": "assets/entitlements.mac.plist",
"entitlementsInherit": "assets/entitlements.mac.plist",
"gatekeeperAssess": false,
"icon": "assets/icons/app.icns",
"notarize": true
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
],
"icon": "assets/icons/app.icns",
"title": "Install Notesnook"
},
"mas": {
"entitlements": "assets/entitlements.mas.plist",
"entitlementsInherit": "assets/entitlements.mas.inherit.plist",
"entitlementsLoginHelper": "assets/entitlements.mas.loginhelper.plist",
"hardenedRuntime": true
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
},
{
"target": "portable",
"arch": [
"x64",
"arm64"
]
}
],
"signtoolOptions": {
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js"
},
"icon": "assets/icons/app.ico"
},
"portable": {
"artifactName": "notesnook_${os}_${arch}_portable.${ext}"
},
"nsis": {
"oneClick": true,
"createDesktopShortcut": "always",
"deleteAppDataOnUninstall": true
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64",
"arm64"
]
},
{
"target": "snap",
"arch": [
"x64",
"arm64"
]
}
],
"category": "Office",
"icon": "assets/icons/app.icns",
"description": "Your private note taking space",
"executableName": "notesnook",
"desktop": {
"actions": [
{
"id": "new-note",
"name": "New note",
"args": "new note"
},
{
"id": "new-notebook",
"name": "New notebook",
"args": "new notebook"
},
{
"id": "new-reminder",
"name": "New reminder",
"args": "new reminder"
}
]
}
},
"snap": {
"autoStart": false,
"confinement": "strict",
"allowNativeWayland": true
},
"extraResources": [
"app-update.yml",
"./assets/**"
],
"extraMetadata": {
"main": "./build/electron.js"
},
"directories": {
"buildResources": "assets",
"output": "./output/"
},
"publish": [
{
"provider": "github",
"repo": "notesnook",
"owner": "streetwriters"
}
]
}
}

View File

@@ -29,10 +29,12 @@ import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = args.root || path.join(__dirname, "..");
const skipTscBuild = args.skipTscBuild || false;
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
await fs.rm("./build/", { force: true, recursive: true });
await fs.rm(path.join(root, "build"), { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(
@@ -44,21 +46,25 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
// temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3();
await fs.cp(path.join(webAppPath, "build"), "build", {
await fs.cp(path.join(webAppPath, "build"), path.join(root, "build"), {
recursive: true,
force: true
});
if (args.variant === "mas") {
await exec(`yarn run bundle:mas`);
await exec(`yarn run bundle:mas --outdir=${path.join(root, "build")}`);
} else {
await exec(`yarn run bundle`);
await exec(`yarn run bundle --outdir=${path.join(root, "build")}`);
}
await exec(`yarn run build`);
if (!skipTscBuild) {
await exec(`yarn run build`);
}
if (args.run) {
await exec(`yarn electron-builder --dir --${process.arch}`);
await exec(
`yarn electron-builder --dir --${process.arch} --config=electron-builder.config.js`
);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {

View File

@@ -29,7 +29,7 @@ import { dirname } from "path";
import { resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils";
import { isFlatpak, isSnap } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
@@ -49,6 +49,7 @@ const NotificationOptions = z.object({
export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
isSnap: t.procedure.query(() => isSnap()),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {

View File

@@ -23,6 +23,7 @@ import { CancellationToken, autoUpdater } from "electron-updater";
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
import { z } from "zod";
import { config } from "../utils/config";
import { app } from "electron";
type UpdateInfo = { version: string };
type Progress = { percent: number };
@@ -33,6 +34,7 @@ let downloadTimeout: NodeJS.Timeout | undefined = undefined;
export const updaterRouter = t.router({
autoUpdates: t.procedure.query(() => config.automaticUpdates),
releaseTrack: t.procedure.query(() => config.releaseTrack),
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
download: t.procedure.query(async () => {
if (cancellationToken) return;
@@ -67,7 +69,13 @@ export const updaterRouter = t.router({
.mutation(({ input: { enabled } }) => {
config.automaticUpdates = enabled;
}),
changeReleaseTrack: t.procedure
.input(z.object({ track: z.string() }))
.mutation(({ input: { track } }) => {
config.releaseTrack = track;
app.relaunch();
app.exit();
}),
onChecking: createSubscription("checking-for-update"),
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
"update-downloaded"

View File

@@ -85,6 +85,8 @@ async function createWindow() {
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
paintWhenInitiallyHidden: cliOptions.hidden,
skipTaskbar: cliOptions.hidden,
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
@@ -101,7 +103,10 @@ async function createWindow() {
...(config.desktopSettings.nativeTitlebar
? {}
: {
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
titleBarStyle:
process.platform === "win32" || process.platform === "darwin"
? "hidden"
: "default",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,

View File

@@ -20,19 +20,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { autoUpdater } from "electron-updater";
import { config } from "./config";
const CHANNEL = autoUpdater.currentVersion.raw.endsWith("-beta")
? "beta"
: "latest";
async function configureAutoUpdater() {
const releaseTrack =
config.releaseTrack === "stable" ? "latest" : config.releaseTrack;
autoUpdater.setFeedURL({
provider: "generic",
url: `https://notesnook.com/api/v1/releases/${process.platform}/${CHANNEL}`,
url: `https://notesnook.com/api/v1/releases/${process.platform}/${releaseTrack}`,
useMultipleRangeRequest: false,
channel: CHANNEL
channel: releaseTrack
});
autoUpdater.autoDownload = config.automaticUpdates;
autoUpdater.allowDowngrade = false;
autoUpdater.allowDowngrade =
// only allow downgrade if the current version is a prerelease
// and the user has changed the release track to stable
config.releaseTrack === "stable" &&
autoUpdater.currentVersion.prerelease.length > 0;
autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.disableWebInstaller = true;

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { nativeTheme } from "electron";
import { JSONStorage } from "./json-storage";
import { z } from "zod";
import { autoUpdater } from "electron-updater";
export const DesktopIntegration = z.object({
autoStart: z.boolean().optional(),
@@ -46,6 +47,9 @@ export const config = {
automaticUpdates: true,
proxyRules: "",
customDns: true,
releaseTrack: autoUpdater.currentVersion.raw.includes("-beta")
? "beta"
: "stable",
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor:

View File

@@ -29,3 +29,7 @@ export function isDevelopment() {
export function isFlatpak() {
return existsSync("/.flatpak-info");
}
export function isSnap() {
return process.env.SNAP !== undefined;
}

View File

@@ -0,0 +1,40 @@
/*
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 { defineConfig } from "vitest/config";
export default defineConfig({
test: {
testTimeout: process.env.CI ? 120 * 1000 : 120 * 1000,
sequence: {
concurrent: true,
shuffle: true
},
dir: "./__tests__/",
exclude: [
"**/node_modules/**",
"**/dist/**",
"**/cypress/**",
"**/.{idea,git,cache,output,temp}/**",
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
"**/test-results/**",
"**/test-artifacts/**"
]
}
});

View File

@@ -231,6 +231,26 @@ export async function deleteDCacheFiles() {
}
}
export async function getCachePathForFile(filename: string) {
const path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
const exists = await RNFetchBlob.fs.exists(path);
// Check if file is present in app group path.
let existsInAppGroup = false;
if (!exists && Platform.OS === "ios") {
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
}
return existsInAppGroup ? appGroupPath : path;
}
export async function exists(filename: string) {
const path = `${cacheDir}/${filename}`;

View File

@@ -80,8 +80,9 @@ export const Dialog = ({ context = "global" }) => {
return;
}
}
hide();
setChecked(false);
values.current.inputValue = undefined;
setVisible(false);
};
const show = useCallback(
@@ -104,14 +105,14 @@ export const Dialog = ({ context = "global" }) => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, [show]);
}, [hide, show]);
const hide = () => {
const hide = React.useCallback(() => {
setChecked(false);
values.current.inputValue = undefined;
setVisible(false);
dialogInfo.onClose();
};
dialogInfo.onClose?.();
}, [dialogInfo]);
const onNegativePress = async () => {
if (dialogInfo.onClose) {

View File

@@ -76,7 +76,8 @@ const NoteItem = ({
}: NoteItemProps) => {
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
state.tabs.find((t) => t.id === state.currentTab)?.session?.noteId ===
item.id
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(

View File

@@ -28,7 +28,8 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
const { colors } = useThemeColors();
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
state.tabs.find((t) => t.id === state.currentTab)?.session?.noteId ===
item.id
);
const [selected] = useIsSelected(item);

View File

@@ -509,7 +509,8 @@ const onChangeTab = async (event) => {
});
} else {
if (
useTabStore.getState().getTab(useTabStore.getState().currentTab).locked
useTabStore.getState().getTab(useTabStore.getState().currentTab).session
?.locked
) {
eSendEvent(eUnlockNote);
}
@@ -523,7 +524,7 @@ const onChangeTab = async (event) => {
// Lock all tabs with locked notes...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
if (!noteId) continue;
const note = await db.notes.note(noteId);
const locked = note && (await db.vaults.itemExists(note));

View File

@@ -176,13 +176,13 @@ class Commands {
await this.sendCommand("clearTags", tabId);
};
insertAttachment = async (attachment: Attachment, tabId: number) => {
insertAttachment = async (attachment: Attachment, tabId: string) => {
await this.sendCommand("insertAttachment", attachment, tabId);
};
setAttachmentProgress = async (
attachmentProgress: Partial<Attachment>,
tabId: number
tabId: string
) => {
await this.sendCommand("setAttachmentProgress", attachmentProgress, tabId);
};
@@ -191,7 +191,7 @@ class Commands {
image: Omit<ImageAttributes, "bloburl"> & {
dataurl: string;
},
tabId: number
tabId: string
) => {
await this.sendCommand("insertImage", image, tabId);
};

View File

@@ -61,7 +61,7 @@ const santizeUri = (uri: string) => {
type PickerOptions = {
noteId?: string;
tabId?: number;
tabId?: string;
type: "image" | "camera" | "file";
reupload: boolean;
hash?: string;

View File

@@ -127,15 +127,8 @@ class LocalTabState {
tabSession?.readonly !== readonly ||
!noteId
) {
console.log("tab is refreshing...");
return true;
}
console.log(
"Tab refreshing...",
state.editedAt < this.noteEditedTime[noteId]
);
console.log(state.editedAt, this.noteEditedTime[noteId]);
return !state.editedAt || state.editedAt < this.noteEditedTime[noteId];
}
@@ -522,7 +515,12 @@ export const useEditor = (
}
if (event.newNote && !currentLoadingNoteId.current) {
let tabId;
if (useTabStore.getState().tabs.length === 0) {
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
if (useTabStore.getState().tabs.length === 0 || currentTab?.pinned) {
tabId = useTabStore.getState().newTab();
} else {
tabId = useTabStore.getState().currentTab;
@@ -547,14 +545,14 @@ export const useEditor = (
const item = event.item;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
// If note is already open in a tab, focus that tab.
if (useTabStore.getState().hasTabForNote(item.id) && !event.newTab) {
const tabId = useTabStore.getState().getTabForNote(item.id);
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
if (
currentTab?.session?.noteId !== item.id &&
tabId !== useTabStore.getState().currentTab
@@ -624,10 +622,16 @@ export const useEditor = (
const tab = useTabStore.getState().getTab(tabId!);
if (useTabStore.getState().tabs.length === 0 || event.newTab) {
if (
useTabStore.getState().tabs.length === 0 ||
event.newTab ||
(currentTab?.pinned &&
event.item.id !== currentTab?.session?.noteId)
) {
useTabStore.getState().newTab({
session: session
});
return;
} else {
// A new session is created if the note is changed.
// If the note is already opened, the session is updated.
@@ -1040,6 +1044,17 @@ export const useEditor = (
}, [isDefaultEditor, restoreEditorState]);
const onLoad = useCallback(async () => {
const isAppLoading = useSettingStore.getState().isAppLoading;
if (isAppLoading) {
const sub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
sub();
onLoad();
}
});
return;
}
setTimeout(() => {
postMessage(NativeEvents.theme, theme);
});
@@ -1082,9 +1097,8 @@ export const useEditor = (
});
}
}
state.current.initialLoadCalled = true;
}
state.current.initialLoadCalled = true;
overlay(false);
}
}, [

View File

@@ -97,15 +97,16 @@ class TabSessionStorage {
static storage: typeof TabSessionStorageKV = TabSessionStorageKV;
static get(id: string): TabSessionItem | null {
if (!id) return null;
return TabSessionStorage.storage.getMap(id);
}
static set(id: string, session: TabSessionItem): void {
if (!id) return;
TabSessionStorage.storage.setMap(id, session);
}
static update(id: string, session: Partial<TabSessionItem>) {
if (!id) throw new Error("Session ID is required");
const currentSession = TabSessionStorage.get(id);
const newSession = {
...currentSession,
@@ -118,6 +119,7 @@ class TabSessionStorage {
}
static remove(id: string) {
if (!id) return;
TabSessionStorageKV.removeItem(id);
}
}
@@ -267,6 +269,7 @@ export const useTabStore = create<TabStore>(
TabSessionStorage.set(sessionId, session as TabSessionItem);
} else {
session = {
id: sessionId,
...TabSessionStorage.get(oldSessionId),
...options
};
@@ -317,13 +320,15 @@ export const useTabStore = create<TabStore>(
const sessionLoaded = await get().loadSession(id);
if (!sessionLoaded) {
tabSessionHistory.remove(currentTab, id);
TabSessionStorage.remove(id);
if (!tabSessionHistory.canGoBack(currentTab)) {
const canGoBack = tabSessionHistory.canGoBack(currentTab);
if (!canGoBack) {
tabSessionHistory.forward(currentTab);
syncTabs();
} else {
return get().goBack();
get().goBack();
tabSessionHistory.remove(currentTab, id);
TabSessionStorage.remove(id);
return;
}
} else {
syncTabs();
@@ -336,13 +341,15 @@ export const useTabStore = create<TabStore>(
if (!tabSessionHistory.canGoForward(currentTab)) return;
const id = tabSessionHistory.forward(currentTab) as string;
if (!(await get().loadSession(id))) {
tabSessionHistory.remove(currentTab, id);
TabSessionStorage.remove(id);
if (!tabSessionHistory.canGoForward(currentTab)) {
const canGoForward = tabSessionHistory.canGoForward(currentTab);
if (!canGoForward) {
tabSessionHistory.back(currentTab);
syncTabs();
} else {
return get().goForward();
get().goForward();
tabSessionHistory.remove(currentTab, id);
TabSessionStorage.remove(id);
return;
}
} else {
syncTabs();
@@ -399,7 +406,7 @@ export const useTabStore = create<TabStore>(
set({
tabs: [{ id: id }]
});
get().newTabSession(id, {});
get().newTabSession(id);
get().focusTab(id);
} else {
set({

View File

@@ -41,7 +41,10 @@ import { RouteParams, SettingSection } from "./types";
const _SectionItem = ({ item }: { item: SettingSection }) => {
const { colors } = useThemeColors();
const settings = useSettingStore((state) => state.settings);
const [settings, itemProperty] = useSettingStore((state) => [
state.settings,
item.property ? state.settings[item.property] : null
]);
const navigation = useNavigation<NavigationProp<RouteParams>>();
const current = item.useHook && item.useHook(item);
const [isHidden, setIsHidden] = useState(
@@ -106,7 +109,7 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
useEffect(() => {
setIsHidden(item.hidden && item.hidden(item.property || current));
setIsDisabled(item.disabled && item.disabled(item.property || current));
}, [current, item]);
}, [current, item, itemProperty]);
return isHidden ? null : (
<Pressable

View File

@@ -39,6 +39,7 @@ import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
import { getCachePathForFile } from "../common/filesystem/io";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -169,7 +170,11 @@ async function run(
progress = false,
context?: string,
backupType: "full" | "partial" = "partial"
) {
): Promise<{
path?: string;
error?: Error;
report?: boolean;
}> {
if (backupRunning) {
if (progress) {
startProgress({
@@ -179,7 +184,7 @@ async function run(
canHideProgress: true
});
}
return;
return {};
}
backupRunning = true;
const androidBackupDirectory = (await checkBackupDirExists(
@@ -255,10 +260,14 @@ async function run(
progress: `Saving attachments in backup... ${file.hash}`
});
if (await FileStorage.exists(file.hash)) {
await RNFetchBlob.fs.cp(
`${cacheDir}/${file.hash}`,
`${attachmentsDir}/${file.hash}`
);
await RNFetchBlob.fs
.cp(
await getCachePathForFile(file.hash),
`${attachmentsDir}/${file.hash}`
)
.catch((e) =>
DatabaseLogger.error(e, "Error saving attachment to backup file")
);
}
}
}
@@ -343,7 +352,7 @@ async function run(
endProgress();
}
return {
error: e,
error: e as Error,
report: true
};
}

View File

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

View File

@@ -1,6 +1,3 @@
- New and improved editor tabs with history
- Fixed a crash on app launch faced by some users
- Fixed homescreen note widget does not open correct note
- Many small bug fixes and improvements
- Small bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -1063,7 +1063,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2127;
CURRENT_PROJECT_VERSION = 2130;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1137,7 +1137,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.30;
MARKETING_VERSION = 3.0.33;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1168,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2127;
CURRENT_PROJECT_VERSION = 2130;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1242,7 +1242,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.30;
MARKETING_VERSION = 3.0.33;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1401,7 +1401,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2127;
CURRENT_PROJECT_VERSION = 2130;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1413,7 +1413,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.30;
MARKETING_VERSION = 3.0.33;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1444,7 +1444,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2127;
CURRENT_PROJECT_VERSION = 2130;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1457,7 +1457,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.30;
MARKETING_VERSION = 3.0.33;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1487,7 +1487,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2127;
CURRENT_PROJECT_VERSION = 2130;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1561,7 +1561,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.30;
MARKETING_VERSION = 3.0.33;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1592,7 +1592,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2127;
CURRENT_PROJECT_VERSION = 2130;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1667,7 +1667,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.30;
MARKETING_VERSION = 3.0.33;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -82,6 +82,7 @@ target 'Make Note' do
pod "RNDeviceInfo", :path => '../../node_modules/react-native-device-info'
pod "RNPrivacySnapshot", :path => '../../node_modules/react-native-privacy-snapshot'
pod "react-native-theme-switch-animation", :path => '../../node_modules/react-native-theme-switch-animation'
pod "react-native-image-resizer", :path => '../../node_modules/@bam.tech/react-native-image-resizer'
end

View File

@@ -1019,7 +1019,7 @@ PODS:
- react-native-screenguard (1.0.0):
- React-Core
- SDWebImage (~> 5.11.1)
- react-native-share-extension (2.8.0):
- react-native-share-extension (2.9.0):
- React
- react-native-sodium (1.6.1):
- React
@@ -1831,7 +1831,7 @@ SPEC CHECKSUMS:
MMKV: 817ba1eea17421547e01e087285606eb270a8dcb
MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9
pop: d582054913807fd11fd50bfe6a539d91c7e1a55a
RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47
RCT-Folly: 5dc73daec3476616d19e8a53f0156176f7b55461
RCTDeprecation: 3afceddffa65aee666dafd6f0116f1d975db1584
RCTRequired: ec1239bc9d8bf63e10fb92bd8b26171a9258e0c1
RCTTypeSafety: f5ecbc86c5c5fa163c05acb7a1c5012e15b5f994
@@ -1877,7 +1877,7 @@ SPEC CHECKSUMS:
react-native-quick-sqlite: 80b5b3405bfc92e97b6041d2c2a446800bea6ad7
react-native-safe-area-context: b72c4611af2e86d80a59ac76279043d8f75f454c
react-native-screenguard: 82437eeb0086a90b5e5d7e54130bb04fb406373e
react-native-share-extension: 7528322fe621b683e6b48ab3418207ed45a70983
react-native-share-extension: bcb7e466390a9e50c742f4b1019d6f181aedd7ad
react-native-sodium: ed020d5f50ab1886f7bf23e32732d0d10e9373ac
react-native-theme-switch-animation: 64f2688c2e3489ababc7b35f436fd3b5ed7c0a98
react-native-webview: a71525b1ab760230fbf37303d8371fbe72051c7d
@@ -1936,6 +1936,6 @@ SPEC CHECKSUMS:
toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018
Yoga: 950bbfd7e6f04790fdb51149ed51df41f329fcc8
PODFILE CHECKSUM: aa07aca16c1d92394d8ce12f2c5fcaa8a3ae4997
PODFILE CHECKSUM: 95f23a5abb88f81f8ca90e24a0a8ceae0a2d1028
COCOAPODS: 1.16.2

View File

@@ -73,7 +73,7 @@
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
"@ammarahmed/react-native-share-extension": "^2.8.0"
"@ammarahmed/react-native-share-extension": "^2.9.0"
},
"devDependencies": {
"detox": "^20.27.6",

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.0.28",
"version": "3.0.31",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.0.28",
"version": "3.0.31",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -14,6 +14,7 @@
"app/"
],
"dependencies": {
"@ammarahmed/react-native-share-extension": "^2.9.0",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
@@ -7742,7 +7743,7 @@
},
"../../packages/editor-mobile/node_modules/@types/prop-types": {
"version": "15.7.11",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/q": {
@@ -7762,7 +7763,7 @@
},
"../../packages/editor-mobile/node_modules/@types/react": {
"version": "18.2.39",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -7793,7 +7794,7 @@
},
"../../packages/editor-mobile/node_modules/@types/scheduler": {
"version": "0.16.8",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/semver": {
@@ -12618,7 +12619,7 @@
},
"../../packages/editor-mobile/node_modules/immer": {
"version": "9.0.21",
"dev": true,
"devOptional": true,
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -23090,7 +23091,6 @@
},
"../../packages/editor/node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
"license": "MIT"
},
"../../packages/editor/node_modules/jsesc": {
@@ -23141,7 +23141,6 @@
},
"../../packages/editor/node_modules/loose-envify": {
"version": "1.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -23653,7 +23652,6 @@
},
"../../packages/editor/node_modules/react": {
"version": "18.3.1",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -23672,7 +23670,6 @@
},
"../../packages/editor/node_modules/react-dom": {
"version": "18.3.1",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
@@ -23811,7 +23808,6 @@
},
"../../packages/editor/node_modules/scheduler": {
"version": "0.23.2",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -28319,7 +28315,6 @@
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
"@ammarahmed/react-native-share-extension": "^2.8.0",
"@ammarahmed/react-native-sodium": "^1.6.1",
"@bam.tech/react-native-image-resizer": "3.0.5",
"@callstack/repack": "^4.1.1",
@@ -28457,9 +28452,10 @@
}
},
"node_modules/@ammarahmed/react-native-share-extension": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.8.0.tgz",
"integrity": "sha512-eDFVKiJJxjnIORveMUKVJGIhKfRNjqJWIoKS+JM88q8I8e3jHys9moyJAr9QM5oW/tZx4yyItl08yRXGnr7Grg==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.0.tgz",
"integrity": "sha512-iEkpJQFZL5/WDPh7iRCLZREANgaaAHthDsPaAYT6sI4wieQcHZMBAnfRMwBEnC0BZ0WVxbQZdzJYez+dmwpr2g==",
"license": "MIT",
"dependencies": {
"react-native": "^0.63.1"
}
@@ -28594,7 +28590,6 @@
},
"node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -28714,7 +28709,6 @@
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -28974,7 +28968,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
"integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
@@ -28989,7 +28982,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz",
"integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
@@ -29138,7 +29130,6 @@
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
},
@@ -29151,7 +29142,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz",
"integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==",
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
@@ -29186,7 +29176,6 @@
},
"node_modules/@babel/plugin-syntax-class-properties": {
"version": "7.12.13",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.12.13"
@@ -29199,7 +29188,6 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
"integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
},
@@ -29237,7 +29225,6 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz",
"integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.3"
},
@@ -29262,7 +29249,6 @@
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz",
"integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29277,7 +29263,6 @@
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz",
"integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29292,7 +29277,6 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
"integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
},
@@ -29304,7 +29288,6 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
"integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
},
@@ -29404,7 +29387,6 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
"integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
},
@@ -29432,7 +29414,6 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
@@ -29461,7 +29442,6 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz",
"integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29492,7 +29472,6 @@
},
"node_modules/@babel/plugin-transform-block-scoped-functions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29521,7 +29500,6 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz",
"integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==",
"dev": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29537,7 +29515,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz",
"integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==",
"dev": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29602,7 +29579,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz",
"integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29618,7 +29594,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz",
"integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29633,7 +29608,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz",
"integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
@@ -29646,7 +29620,6 @@
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
@@ -29663,7 +29636,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz",
"integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-export-namespace-from": "^7.8.3"
@@ -29691,7 +29663,6 @@
},
"node_modules/@babel/plugin-transform-for-of": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29722,7 +29693,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz",
"integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-json-strings": "^7.8.3"
@@ -29751,7 +29721,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz",
"integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
@@ -29765,7 +29734,6 @@
},
"node_modules/@babel/plugin-transform-member-expression-literals": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29781,7 +29749,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz",
"integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==",
"dev": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29812,7 +29779,6 @@
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz",
"integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==",
"dev": true,
"dependencies": {
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-module-transforms": "^7.22.5",
@@ -29830,7 +29796,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz",
"integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==",
"dev": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29860,7 +29825,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz",
"integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29890,7 +29854,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz",
"integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-numeric-separator": "^7.10.4"
@@ -29906,7 +29869,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz",
"integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
@@ -29923,7 +29885,6 @@
},
"node_modules/@babel/plugin-transform-object-super": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29940,7 +29901,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz",
"integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
@@ -30014,7 +29974,6 @@
},
"node_modules/@babel/plugin-transform-property-literals": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30084,7 +30043,6 @@
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -30101,7 +30059,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz",
"integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30194,7 +30151,6 @@
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz",
"integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30225,7 +30181,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz",
"integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30240,7 +30195,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz",
"integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30270,7 +30224,6 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz",
"integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30286,7 +30239,6 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz",
"integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
@@ -30380,7 +30332,6 @@
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz",
"integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
@@ -30396,7 +30347,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -33700,8 +33650,7 @@
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
@@ -33807,14 +33756,14 @@
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -34170,7 +34119,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
"integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
"dev": true,
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6"
@@ -34179,26 +34127,22 @@
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
"dev": true
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
"dev": true
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==",
"dev": true
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
"integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
"dev": true,
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
"@webassemblyjs/helper-api-error": "1.11.6",
@@ -34208,14 +34152,12 @@
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
"dev": true
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
"integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34227,7 +34169,6 @@
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
"integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
"dev": true,
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
@@ -34236,7 +34177,6 @@
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
"integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
"dev": true,
"dependencies": {
"@xtuc/long": "4.2.2"
}
@@ -34244,14 +34184,12 @@
"node_modules/@webassemblyjs/utf8": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
"dev": true
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
"integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34267,7 +34205,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
"integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
@@ -34280,7 +34217,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
"integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34292,7 +34228,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
"integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-api-error": "1.11.6",
@@ -34306,7 +34241,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
"integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@xtuc/long": "4.2.2"
@@ -34364,14 +34298,12 @@
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
},
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
@@ -35370,7 +35302,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"dev": true,
"engines": {
"node": ">=6.0"
}
@@ -35801,7 +35732,7 @@
},
"node_modules/csstype": {
"version": "3.1.2",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/date-fns": {
@@ -36401,7 +36332,6 @@
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -36521,8 +36451,7 @@
"node_modules/es-module-lexer": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw=="
},
"node_modules/es-object-atoms": {
"version": "1.0.0",
@@ -36867,7 +36796,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -36880,7 +36808,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"engines": {
"node": ">=4.0"
}
@@ -37044,7 +36971,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -37056,7 +36982,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"engines": {
"node": ">=4.0"
}
@@ -37065,7 +36990,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -37825,8 +37749,7 @@
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
},
"node_modules/global": {
"version": "4.4.0",
@@ -39915,8 +39838,7 @@
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-ref-resolver": {
"version": "1.0.1",
@@ -40392,7 +40314,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
"dev": true,
"engines": {
"node": ">=6.11.5"
}
@@ -42622,7 +42543,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -42644,6 +42564,30 @@
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/react-dom/node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/react-freeze": {
"version": "1.0.3",
"license": "MIT",
@@ -43563,7 +43507,6 @@
},
"node_modules/regenerator-transform": {
"version": "0.15.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.4"
@@ -43916,7 +43859,6 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
@@ -44574,7 +44516,6 @@
},
"node_modules/tapable": {
"version": "2.2.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -44652,7 +44593,6 @@
"version": "5.3.10",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
"jest-worker": "^27.4.5",
@@ -44686,7 +44626,6 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
@@ -44700,7 +44639,6 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -45277,7 +45215,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
@@ -45301,7 +45238,6 @@
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0",
@@ -45417,7 +45353,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true,
"engines": {
"node": ">=10.13.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.30",
"version": "3.0.33",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -21,13 +21,11 @@ import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from "react";
import { Linking, Platform, TextInput, View } from "react-native";
import { WebView } from "react-native-webview";
import Commands from "../app/screens/editor/tiptap/commands";
import {
eSubscribeEvent,
eUnSubscribeEvent
@@ -57,7 +55,6 @@ export async function post(ref, type, value = null) {
const useEditor = () => {
const ref = useRef();
const { colors } = useThemeColors("editor");
const commands = useMemo(() => new Commands(ref), [ref]);
const currentNote = useRef();
const postMessage = useCallback(
@@ -106,7 +103,7 @@ const useEditor = () => {
}, 1);
};
return { ref, onLoad, currentNote, commands };
return { ref, onLoad, currentNote };
};
const useEditorEvents = (editor, onChange) => {

View File

@@ -546,7 +546,7 @@ const ShareView = () => {
</Paragraph>
<ScrollView horizontal>
{rawFiles.map((item) =>
isImage(item.type) ? (
isImage(item.type) || item.value?.endsWith(".png") ? (
<TouchableOpacity
onPress={() => onRemoveFile(item)}
key={item.name}
@@ -604,7 +604,8 @@ const ShareView = () => {
paddingRight: 8
}}
>
{item.name} ({formatBytes(item.size)})
{item.name || item.value.split("/").pop()}
{item.size ? `(${formatBytes(item.size)})` : ""}
</Paragraph>
</TouchableOpacity>
)

View File

@@ -1,5 +1,5 @@
FROM --platform=$BUILDPLATFORM oven/bun:1.1.36-alpine
FROM oven/bun:1.2.2-alpine
RUN mkdir -p /home/bun/app && chown -R bun:bun /home/bun/app
@@ -11,4 +11,4 @@ COPY --chown=bun:bun output .
RUN bun install
CMD [ "bun", "run", "start" ]
CMD [ "bun", "run", "start" ]

View File

@@ -62,8 +62,42 @@ export default function handleRequest(
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
const nonce =
remixContext.staticHandlerContext?.loaderData?.root?.cspScriptNonce;
if (nonce)
responseHeaders.set(
"Content-Security-Policy",
getContentSecurityPolicy(nonce)
);
return new Response(html, {
status: responseStatusCode,
headers: responseHeaders
});
}
function getContentSecurityPolicy(nonce?: string) {
let script_src: string;
if (typeof nonce === "string") {
script_src = `'self' 'report-sample' 'nonce-${nonce}'`;
} else if (process.env.NODE_ENV === "development") {
// Allow the <LiveReload /> component to load without a nonce in the error pages
script_src = "'self' 'report-sample'";
} else {
script_src = "'self' 'report-sample'";
}
const connect_src =
process.env.NODE_ENV === "development"
? "'self' ws://localhost:*"
: "'self'";
return (
`script-src ${script_src} 'strict-dynamic'; ` +
`connect-src ${connect_src}; ` +
"form-action 'self'; " +
"object-src 'none'; " +
"block-all-mixed-content; " +
"base-uri 'self'; manifest-src 'self'"
);
}

View File

@@ -22,14 +22,22 @@ import {
Meta,
Outlet,
Scripts,
ScrollRestoration
ScrollRestoration,
useLoaderData
} from "@remix-run/react";
import { BaseThemeProvider } from "./components/theme-provider";
import { Buffer } from "buffer";
import { ThemeDark, themeToCSS } from "@notesnook/theme";
import { LoaderFunction } from "@remix-run/node";
globalThis.Buffer = Buffer;
type RootLoaderData = { cspScriptNonce: string };
export const loader: LoaderFunction = async () => {
const crypto = await import("node:crypto");
return { cspScriptNonce: crypto.randomBytes(16).toString("hex") };
};
export function Head() {
return (
<>
@@ -40,13 +48,17 @@ export function Head() {
<Links />
<style
id="theme-colors"
dangerouslySetInnerHTML={{ __html: colorsToCSS() }}
dangerouslySetInnerHTML={{ __html: themeToCSS(ThemeDark) }}
/>
</>
);
}
export default function App() {
const data = useLoaderData<RootLoaderData>();
const cspScriptNonce =
typeof document === "undefined" ? data.cspScriptNonce : "";
return (
<>
<BaseThemeProvider
@@ -56,29 +68,8 @@ export default function App() {
>
<Outlet />
</BaseThemeProvider>
<ScrollRestoration />
<Scripts />
<ScrollRestoration nonce={cspScriptNonce} />
<Scripts nonce={cspScriptNonce} />
</>
);
}
function colorsToCSS() {
// const colorScheme = JSON.parse(
// window.localStorage.getItem("colorScheme") || '"light"'
// );
// const root = document.querySelector("html");
// if (root) root.setAttribute("data-theme", colorScheme);
// const theme =
// colorScheme === "dark"
// ? JSON.parse(window.localStorage.getItem("theme:dark") || "false") ||
// ThemeDark
// : JSON.parse(window.localStorage.getItem("theme:light") || "false") ||
// ThemeLight;
// const stylesheet = document.getElementById("theme-colors");
// if (theme) {
// const css = ;
// if (stylesheet) stylesheet.innerHTML = css;
// } else stylesheet?.remove();
return themeToCSS(ThemeDark);
}

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/monograph",
"version": "1.2.2",
"version": "1.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/monograph",
"version": "1.2.2",
"version": "1.2.4",
"dependencies": {
"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.1",
@@ -60,9 +60,6 @@
"vite-plugin-static-copy": "^2.1.0",
"vite-tsconfig-paths": "^5.0.1",
"wrangler": "3.78.11"
},
"engines": {
"node": ">=20.0.0"
}
},
"../../packages/core": {
@@ -2945,6 +2942,22 @@
"@styled-system/css": "^5.1.5"
}
},
"node_modules/@theme-ui/color-modes": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@theme-ui/color-modes/-/color-modes-0.16.2.tgz",
"integrity": "sha512-jWEWx53lxNgWCT38i/kwLV2rsvJz8lVZgi5oImnVwYba9VejXD23q1ckbNFJHosQ8KKXY87ht0KPC6BQFIiHtQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@theme-ui/core": "^0.16.2",
"@theme-ui/css": "^0.16.2",
"deepmerge": "^4.2.2"
},
"peerDependencies": {
"@emotion/react": "^11.11.1",
"react": ">=18"
}
},
"node_modules/@theme-ui/components": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@theme-ui/components/-/components-0.16.2.tgz",
@@ -2990,6 +3003,22 @@
"@emotion/react": "^11.11.1"
}
},
"node_modules/@theme-ui/theme-provider": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@theme-ui/theme-provider/-/theme-provider-0.16.2.tgz",
"integrity": "sha512-LRnVevODcGqO0JyLJ3wht+PV3ZoZcJ7XXLJAJWDoGeII4vZcPQKwVy4Lpz/juHsZppQxKcB3U+sQDGBnP25irQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@theme-ui/color-modes": "^0.16.2",
"@theme-ui/core": "^0.16.2",
"@theme-ui/css": "^0.16.2"
},
"peerDependencies": {
"@emotion/react": "^11.11.1",
"react": ">=18"
}
},
"node_modules/@types/acorn": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz",
@@ -3119,14 +3148,14 @@
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz",
"integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -12212,7 +12241,7 @@
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/monograph",
"version": "1.2.2",
"version": "1.2.4",
"private": true,
"sideEffects": false,
"type": "module",
@@ -64,8 +64,5 @@
"vite-plugin-static-copy": "^2.1.0",
"vite-tsconfig-paths": "^5.0.1",
"wrangler": "3.78.11"
},
"engines": {
"node": ">=20.0.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/theme-builder",
"description": "Your private note taking space",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -12,35 +12,25 @@
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.1",
"@notesnook/common": "file:../../packages/common",
"@notesnook/intl": "file:../../packages/intl",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/web": "file:../web",
"@streetwriters/kysely": "^0.27.4",
"@theme-ui/components": "^0.14.7",
"@theme-ui/core": "^0.14.7",
"@trpc/client": "10.45.2",
"@types/react-dom": "^18.3.0",
"clipboard-polyfill": "^4.0.0",
"dayjs": "1.11.13",
"file-saver": "^2.0.5",
"katex": "^0.16.2",
"react": "18.2.0",
"react-dom": "^18.2.0"
"react-dom": "18.2.0",
"react-modal": "3.16.3",
"@lingui/core": "5.1.2"
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@types/babel__core": "^7.20.1",
"@types/file-saver": "^2.0.5",
"@types/react": "^18.2.39",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.14",
"buffer": "^6.0.3",
"chalk": "^4.1.0",
"happy-dom": "^8.9.0",
"rollup": "^3.27.0",
"rollup-plugin-visualizer": "^5.9.2",
"swc-plugin-react-remove-properties": "^0.1.4",
"vite": "^4.3.9",
"vite-plugin-env-compatible": "^1.1.1",
"vite-plugin-svgr": "^3.2.0",
"vitest": "^0.32.0"
"vite": "5.4.11"
},
"overrides": {
"@theme-ui/components@>0": "0.14.7",

View File

@@ -0,0 +1,13 @@
#root > .theme-scope-base {
display: flex;
height: 100%;
}
#app {
flex: 1;
height: unset;
}
#root > .theme-scope-base > :first-child:not(#menu-wrapper) {
flex: 1;
}

View File

@@ -17,60 +17,44 @@ 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 { init } from "@notesnook/web/src/bootstrap";
import { createRoot } from "react-dom/client";
import {
ErrorBoundary,
ErrorComponent
} from "@notesnook/web/src/components/error-boundary";
import { BaseThemeProvider } from "@notesnook/web/src/components/theme-provider";
import "./index.css";
import "@notesnook/web/src/polyfills";
import "@notesnook/web/src/app.css";
import { ThemeDark, ThemeLight, themeToCSS } from "@notesnook/theme";
import Config from "@notesnook/web/src/utils/config";
import { setI18nGlobal, Messages } from "@notesnook/intl";
import { i18n } from "@lingui/core";
import { App } from "./app";
renderApp();
const colorScheme = JSON.parse(
window.localStorage.getItem("colorScheme") || '"light"'
);
const root = document.querySelector("html");
if (root) root.setAttribute("data-theme", colorScheme);
async function renderApp() {
const rootElement = document.getElementById("root");
if (!rootElement) return;
const root = createRoot(rootElement);
try {
const { component, props } = await init();
const theme =
colorScheme === "dark"
? Config.get("theme:dark", ThemeDark)
: Config.get("theme:light", ThemeLight);
const stylesheet = document.getElementById("theme-colors");
if (theme) {
const css = themeToCSS(theme);
if (stylesheet) stylesheet.innerHTML = css;
} else stylesheet?.remove();
const { useKeyStore } = await import(
"@notesnook/web/src/interfaces/key-store"
);
await useKeyStore.getState().init();
const locale = import.meta.env.DEV
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
: import("@notesnook/intl/locales/$en.json");
locale.then(({ default: locale }) => {
i18n.load({
en: locale.messages as unknown as Messages
});
i18n.activate("en");
const { default: Component } = await component();
const { default: AppLock } = await import(
"@notesnook/web/src/views/app-lock"
);
root.render(
<ErrorBoundary>
<BaseThemeProvider
onRender={() => document.getElementById("splash")?.remove()}
sx={{
display: "flex",
"#app": { flex: 1, height: "unset" },
"& > :first-child:not(#menu-wrapper)": { flex: 1 },
height: "100%"
}}
>
<AppLock>
<Component route={props?.route || "login:email"} />
</AppLock>
<App />
</BaseThemeProvider>
</ErrorBoundary>
);
} catch (e) {
root.render(
<>
<ErrorComponent
error={e}
resetErrorBoundary={() => window.location.reload()}
/>
</>
);
}
}
performance.mark("import:root");
import("@notesnook/web/src/root.js").then(({ startApp }) => {
performance.mark("start:app");
startApp(<App />);
});
});
setI18nGlobal(i18n);

3
apps/web/.gitignore vendored
View File

@@ -30,4 +30,5 @@ dist
public/workbox
scripts/secrets
test-results
.swc
.swc
.wrangler

View File

@@ -258,6 +258,21 @@ test("creating a new note and toggling read-only mode should not empty editor co
expect(await notes.editor.getContent("text")).toBe(NOTE.content);
});
test("toggling read-only should hide undo & redo buttons", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await notes.editor.undoButton.waitFor({ state: "visible" });
await notes.editor.redoButton.waitFor({ state: "visible" });
await note?.properties.readonly();
expect(await note?.properties.isReadonly()).toBeTruthy();
expect(notes.editor.undoButton).not.toBeVisible();
expect(notes.editor.redoButton).not.toBeVisible();
});
test("#1468 count words separated by newlines", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
@@ -347,7 +362,7 @@ test("when autosave is disabled, creating a new note should save the note", asyn
await notes.newNote();
await note?.openNote();
await expect(notes.editor.savedIcon).toBeVisible();
// await expect(notes.editor.savedIcon).toBeVisible();
expect(await notes.editor.getContent("text")).toBe(content.trim());
});
@@ -385,7 +400,7 @@ test("control + alt + right arrow should go to next note", async ({ page }) => {
});
await note1?.openNote();
await note2?.openNote();
await note2?.openNote(true);
await page.keyboard.press("Control+Alt+ArrowRight");
expect(await notes.editor.getTitle()).toBe("Note 1");
@@ -413,7 +428,7 @@ test("control + alt + left arrow should go to previous note", async ({
});
await note1?.openNote();
await note2?.openNote();
await note2?.openNote(true);
await page.keyboard.press("Control+Alt+ArrowLeft");
expect(await notes.editor.getTitle()).toBe("Note 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

@@ -41,6 +41,8 @@ export class EditorModel {
private readonly newTabButton: Locator;
readonly savedIcon: Locator;
readonly notSavedIcon: Locator;
readonly undoButton: Locator;
readonly redoButton: Locator;
constructor(page: Page) {
this.page = page;
@@ -65,6 +67,8 @@ export class EditorModel {
this.goBackButton = page.locator(getTestId("go-back"));
this.goForwardButton = page.locator(getTestId("go-forward"));
this.newTabButton = page.locator(getTestId("New tab"));
this.undoButton = page.locator(getTestId("Undo"));
this.redoButton = page.locator(getTestId("Redo"));
}
async waitForLoading(title?: string, content?: string) {
@@ -93,8 +97,8 @@ export class EditorModel {
await this.searchButton.isDisabled();
await this.page
.locator(".active")
.locator(getTestId("tags"))
.waitFor({ state: "hidden" });
.locator(getTestId("editor-tag-input"))
.isDisabled();
await this.dateEditedText.waitFor({ state: "hidden" });
await this.wordCountText.waitFor();
await this.waitForLoading("", "");
@@ -109,7 +113,7 @@ export class EditorModel {
async isUnloaded() {
return (
(await this.tagInput.isHidden()) &&
(await this.tagInput.isDisabled()) &&
(await this.enterFullscreenButton.isHidden()) &&
(await this.dateEditedText.isHidden())
);
@@ -124,7 +128,8 @@ export class EditorModel {
async typeTitle(text: string, delay = 0) {
await this.editAndWait(async () => {
await this.title.focus();
await this.title.press("End");
await this.title.selectText();
await this.title.press("ArrowRight");
await this.title.type(text, { delay });
});
}
@@ -196,6 +201,16 @@ export class EditorModel {
return tags;
}
async removeTags(tags: string[]) {
for (const tag of tags) {
const item = this.tags
.filter({ hasText: tag })
.getByTitle("Remove")
.first();
await item.click();
}
}
async getTitle() {
return this.title.inputValue();
}

View File

@@ -98,4 +98,9 @@ export class NotebookItemModel extends BaseItemModel {
await this.contextMenu.close();
return state;
}
async setAsDefault() {
await this.contextMenu.open(this.locator);
await new ToggleModel(this.page, "menu-button-set-as-default").on();
}
}

View File

@@ -19,11 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { ContextMenuModel } from "./context-menu.model";
export class TabItemModel {
private readonly closeButton: Locator;
constructor(private readonly locator: Locator, page: Page) {
readonly contextMenu: TabContextMenuModel;
readonly titleElement: Locator;
constructor(readonly locator: Locator, page: Page) {
this.closeButton = locator.locator(getTestId("tab-close-button"));
this.contextMenu = new TabContextMenuModel(page, locator);
this.titleElement = locator.locator(getTestId("tab-title"));
}
async getId() {
@@ -36,7 +42,7 @@ export class TabItemModel {
}
async title() {
return this.locator.locator(getTestId("tab-title")).textContent();
return this.titleElement.textContent();
}
async isActive() {
@@ -48,3 +54,19 @@ export class TabItemModel {
return this.closeButton.click();
}
}
class TabContextMenuModel {
private readonly menu: ContextMenuModel;
constructor(page: Page, private readonly tabLocator: Locator) {
this.menu = new ContextMenuModel(page);
}
async pin() {
await this.open();
await this.menu.clickOnItem("pin");
}
async open() {
await this.menu.open(this.tabLocator);
}
}

View File

@@ -271,3 +271,84 @@ test(`sort notebooks`, async ({ page }, info) => {
}
}
});
test("when default notebook is set, created note in notes context should go to default notebook", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
let notebooks = await app.goToNotebooks();
let notebook = await notebooks.createNotebook(NOTEBOOK);
await notebook?.setAsDefault();
const notes = await app.goToNotes();
await notes?.createNote(NOTE);
notebooks = await app.goToNotebooks();
notebook = await notebooks.findNotebook(NOTEBOOK);
const openedNotebook = await notebook?.openNotebook();
expect(await openedNotebook?.notes.findNote(NOTE)).toBeDefined();
});
test("when default notebook is set, created note in other notebook's context should not go to default notebook", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
let notebooks = await app.goToNotebooks();
let notebook = await notebooks.createNotebook(NOTEBOOK);
await notebook?.setAsDefault();
const otherNotebook = await notebooks.createNotebook({
title: "Other Notebook"
});
const openedOtherNotebook = await otherNotebook?.openNotebook();
await openedOtherNotebook?.notes.createNote(NOTE);
notebooks = await app.goToNotebooks();
notebook = await notebooks.findNotebook(NOTEBOOK);
const openedNotebook = await notebook?.openNotebook();
expect(await openedNotebook?.notes.findNote(NOTE)).toBeUndefined();
});
test("when default notebook is set, created note in tags context should go to default notebook", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
let notebooks = await app.goToNotebooks();
let notebook = await notebooks.createNotebook(NOTEBOOK);
await notebook?.setAsDefault();
const tags = await app.goToTags();
const tag = await tags.createItem({ title: "TestTag" });
const openedTag = await tag?.open();
await openedTag?.createNote(NOTE);
notebooks = await app.goToNotebooks();
notebook = await notebooks.findNotebook(NOTEBOOK);
const openedNotebook = await notebook?.openNotebook();
expect(await openedNotebook?.notes.findNote(NOTE)).toBeDefined();
});
test("when default notebook is set, created note in colors context should go to default notebook", async ({
page
}) => {
const coloredNote = { title: "Red note", content: NOTE.content };
const app = new AppModel(page);
await app.goto();
let notebooks = await app.goToNotebooks();
let notebook = await notebooks.createNotebook(NOTEBOOK);
await notebook?.setAsDefault();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.newColor({ color: "#ff0000", title: "red" });
const color = await app.goToColor("red");
await color?.createNote(coloredNote);
notebooks = await app.goToNotebooks();
notebook = await notebooks.findNotebook(NOTEBOOK);
const openedNotebook = await notebook?.openNotebook();
expect(await openedNotebook?.notes.findNote(coloredNote)).toBeDefined();
});

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { createHistorySession } from "./utils";
import { createHistorySession, NOTE } from "./utils";
test("notes should open in the same tab", async ({ page }) => {
const app = new AppModel(page);
@@ -63,7 +63,7 @@ test("open note in new tab (using context menu)", async ({ page }) => {
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(2);
expect(await tabs[1].title()).toBe("Note 2");
await expect(tabs[1].titleElement).toHaveText("Note 2");
});
test("open note in new tab (using middle click)", async ({ page }) => {
@@ -80,7 +80,7 @@ test("open note in new tab (using middle click)", async ({ page }) => {
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(2);
expect(await tabs[1].title()).toBe("Note 2");
await expect(tabs[1].titleElement).toHaveText("Note 2");
});
test("go back should open previous note", async ({ page }) => {
@@ -93,9 +93,9 @@ test("go back should open previous note", async ({ page }) => {
await notes.createNote({ title: "Note 3" });
await notes.editor.goBack();
expect(await tabs[0].title()).toBe("Note 2");
await expect(tabs[0].titleElement).toHaveText("Note 2");
await notes.editor.goBack();
expect(await tabs[0].title()).toBe("Note 1");
await expect(tabs[0].titleElement).toHaveText("Note 1");
});
test("go forward should open next note", async ({ page }) => {
@@ -110,9 +110,9 @@ test("go forward should open next note", async ({ page }) => {
await notes.editor.goBack();
await notes.editor.goBack();
await notes.editor.goForward();
expect(await tabs[0].title()).toBe("Note 2");
await expect(tabs[0].titleElement).toHaveText("Note 2");
await notes.editor.goForward();
expect(await tabs[0].title()).toBe("Note 3");
await expect(tabs[0].titleElement).toHaveText("Note 3");
});
test("new tab button should open a new tab", async ({ page }) => {
@@ -124,11 +124,11 @@ test("new tab button should open a new tab", async ({ page }) => {
await notes.editor.newTab();
const tabs = await notes.editor.getTabs();
expect(await tabs[0].title()).toBe("Note 1");
expect(await tabs[1].title()).toBe("Untitled");
await expect(tabs[0].titleElement).toHaveText("Note 1");
await expect(tabs[1].titleElement).toHaveText("Untitled");
});
test("changes in a note opened in multiple tabs should sync", async ({
test("content changes in a note opened in multiple tabs should sync", async ({
page
}) => {
const app = new AppModel(page);
@@ -173,6 +173,7 @@ test("reloading with a note diff open in a tab", async ({ page }) => {
const history = await note?.properties.getSessionHistory();
const preview = await history?.[0].open();
await preview!.firstEditor.waitFor({ state: "visible" });
await page.waitForTimeout(1000);
await page.reload();
await preview!.firstEditor.waitFor({ state: "visible" });
@@ -227,7 +228,7 @@ test("clicking on a note that's already opened in another tab should focus the t
await note?.openNote();
const tabs = await notes.editor.getTabs();
expect(await tabs[0].isActive()).toBe(true);
await expect(tabs[0].locator).toHaveClass(/active/);
});
test("open a note in 2 tabs then open another note and navigate back", async ({
@@ -248,8 +249,134 @@ test("open a note in 2 tabs then open another note and navigate back", async ({
await notes.editor.goBack();
const tabs = await notes.editor.getTabs();
expect(await tabs[1].isActive()).toBe(true);
expect(await tabs[1].title()).toBe("Note 1");
await expect(tabs[1].locator).toHaveClass(/active/);
await expect(tabs[1].titleElement).toHaveText("Note 1");
});
test("shouldn't be possible to open a note in a pinned tab", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: "Note 1"
});
await notes.createNote({
title: "Note 2"
});
let tabs = await notes.editor.getTabs();
await tabs[0].contextMenu.pin();
await note?.click();
await notes.editor.waitForLoading();
tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(2);
await expect(tabs[1].locator).toHaveClass(/active/);
await expect(tabs[1].titleElement).toHaveText("Note 1");
});
test("shouldn't be possible to create a new note in a pinned tab", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({
title: "Note 1"
});
let tabs = await notes.editor.getTabs();
await tabs[0].contextMenu.pin();
await notes.newNote();
tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(2);
await expect(tabs[1].locator).toHaveClass(/active/);
});
test("notes open in multiple tabs should sync tags when tags are added", async ({
page
}) => {
const tags = ["toAdd", "todo"];
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.openInNewTab();
await notes.editor.setTags(tags);
const tabs = await notes.editor.getTabs();
await tabs[0].click();
const noteTags = await notes.editor.getTags();
expect(noteTags).toHaveLength(tags.length);
expect(noteTags.every((t, i) => t === tags[i])).toBe(true);
});
test("notes open in multiple tabs should sync tags when tags are removed", async ({
page
}) => {
const tags = ["removeme", "dontremoveme", "removemex2", "todo"];
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await notes.editor.setTags(tags);
await note?.contextMenu.openInNewTab();
await notes.editor.removeTags(["removeme", "removemex2"]);
const tabs = await notes.editor.getTabs();
await tabs[0].click();
const noteTags = await notes.editor.getTags();
expect(noteTags).toHaveLength(2);
expect(noteTags.every((t, i) => t === ["dontremoveme", "todo"][i])).toBe(
true
);
});
test("notes open in multiple tabs should sync color in note properties when color is changed", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.newColor({ title: "red", color: "#ff0000" });
await note?.contextMenu.newColor({ title: "blue", color: "#0000ff" });
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
const tabs = await notes.editor.getTabs();
await tabs[0].click();
expect(await note?.properties.isColored("blue")).toBe(true);
await note?.properties.color("red");
await tabs[1].click();
expect(await note?.properties.isColored("red")).toBe(true);
});
test("notes open in multiple tabs should sync title when title is changed", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await notes.editor.setTitle("Todo");
await note?.contextMenu.openInNewTab();
expect(await notes.editor.getTitle()).toBe("Todo");
await notes.editor.setTitle("Todo - Today");
const tabs = await notes.editor.getTabs();
await tabs[0].click();
expect(await notes.editor.getTitle()).toBe("Todo - Today");
});
test.skip("TODO: open a locked note, switch to another note and navigate back", () => {});

View File

@@ -0,0 +1,57 @@
/*
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/>.
*/
function isBeta(cookie: string) {
console.log("Checking cookie:", cookie);
const releaseTrack = cookie
.split("; ")
.find((row) => row.startsWith("release-track="))
?.split("=")[1];
return releaseTrack === "beta";
}
interface Env {
BETA_BASE_URL: string;
}
export const onRequest: PagesFunction<Env> = async ({ request, env, next }) => {
try {
const url = new URL(request.url);
const response = await (async () => {
if (isBeta(request.headers.get("Cookie") || "") && env.BETA_BASE_URL) {
const betaUrl = new URL(env.BETA_BASE_URL);
betaUrl.pathname = url.pathname;
betaUrl.search = url.search;
console.log("Fetching asset from beta URL:", betaUrl.toString());
const asset = await fetch(betaUrl, {
headers: request.headers,
method: request.method,
body: request.body
});
return new Response(asset.body, asset);
} else {
return await next();
}
})();
return response;
} catch (thrown) {
console.error("Error occurred:", thrown);
return new Response(thrown);
}
};

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"types": ["@cloudflare/workers-types"],
"resolveJsonModule": true,
"esModuleInterop": true
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.0.25",
"version": "3.0.31",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -85,6 +85,7 @@
},
"devDependencies": {
"@babel/core": "7.22.5",
"@cloudflare/workers-types": "^4.20250224.0",
"@playwright/test": "1.48.2",
"@swc/plugin-react-remove-properties": "^6.0.2",
"@trpc/server": "10.45.2",
@@ -131,6 +132,7 @@
"start:test": "serve -s build/ -p 3000",
"build": "cross-env PLATFORM=web vite build",
"build:test": "cross-env PLATFORM=web TEST=true vite build",
"build:beta": "cross-env PLATFORM=web BETA=true vite build",
"build:desktop": "cross-env PLATFORM=desktop vite build",
"analyze": "cross-env ANALYZING=true PLATFORM=web vite build",
"test": "playwright test -u",

View File

@@ -249,3 +249,26 @@ textarea,
background-color: color-mix(in srgb, var(--accent) 5%, transparent);
}
}
kbd {
background: var(--background);
border-radius: 3px;
padding: 2px 5px;
color: var(--paragraph-secondary);
}
.ping {
animation: ping 1s infinite;
}
@keyframes ping {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}

View File

@@ -46,6 +46,7 @@ import { AppEventManager, AppEvents } from "./common/app-events";
import { TITLE_BAR_HEIGHT } from "./components/title-bar";
import { getFontSizes } from "@notesnook/theme/theme/font/fontsize.js";
import { useWindowControls } from "./hooks/use-window-controls";
import { STATUS_BAR_HEIGHT } from "./common/constants";
new WebExtensionRelay();
@@ -130,7 +131,10 @@ function App() {
) : (
<DesktopAppContents setShow={setShow} show={show} />
)}
<Toaster containerClassName="toasts-container" />
<Toaster
containerClassName="toasts-container"
containerStyle={{ bottom: STATUS_BAR_HEIGHT + 10 }}
/>
</Flex>
</>
);

View File

@@ -25,3 +25,5 @@ export const SUBSCRIPTION_STATUS = {
PREMIUM_EXPIRED: 6,
PREMIUM_CANCELED: 7
} as const;
export const STATUS_BAR_HEIGHT = 24 as const;

View File

@@ -70,6 +70,7 @@ function attachListeners() {
function attachListener(event: string) {
return {
onData(...args: any[]) {
console.log("Received data:", args);
AppEventManager.publish(event, ...args);
}
};

View File

@@ -25,7 +25,8 @@ export interface BaseDialogProps<T = unknown> {
}
class _DialogManager {
private openedDialogs: Set<React.JSXElementConstructor<any>> = new Set();
private openedDialogs: Map<React.JSXElementConstructor<any>, () => void> =
new Map();
open<
Props extends BaseDialogProps<any>,
@@ -39,19 +40,21 @@ class _DialogManager {
const container = document.createElement("div");
const root = createRoot(container);
const close = () => {
this.openedDialogs.delete(component);
root.unmount();
container.remove();
};
const Dialog = component as React.FunctionComponent<
Omit<Props, "onClose"> & {
onClose: (result: any) => void;
}
>;
this.openedDialogs.add(component);
return new Promise<Result>((resolve, reject) =>
return new Promise<Result>((resolve, reject) => {
const close = () => {
this.openedDialogs.delete(component);
root.unmount();
container.remove();
};
this.openedDialogs.set(component, () => {
close();
reject(new Error("Dialog force closed."));
});
root.render(
<Dialog
{...props}
@@ -64,8 +67,8 @@ class _DialogManager {
}
}}
/>
)
);
);
});
}
closeAll() {
@@ -80,7 +83,12 @@ class _DialogManager {
component: React.ComponentType<Props>
) {
return {
show: (props: Omit<Props, "onClose">) => this.open(component, props)
show: (props: Omit<Props, "onClose">) => this.open(component, props),
close: () => {
const dialog = this.openedDialogs.get(component);
if (!dialog) return;
dialog();
}
};
}
}

View File

@@ -21,6 +21,8 @@ import hotkeys from "hotkeys-js";
import { useEditorStore } from "../stores/editor-store";
import { useStore as useSearchStore } from "../stores/search-store";
import { useEditorManager } from "../components/editor/manager";
import { CommandPaletteDialog } from "../dialogs/command-palette";
import { hashNavigate } from "../navigation";
function isInEditor(e: KeyboardEvent) {
return (
@@ -123,7 +125,7 @@ const KEYMAP = [
useSearchStore.setState({ isSearching: true, searchType: "notes" });
}
}
},
// {
// keys: ["alt+n"],
// description: "Go to Notes",
@@ -187,6 +189,22 @@ const KEYMAP = [
// 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(() => {});
}
},
{
keys: ["ctrl+,", "command+,"],
description: "Open settings",
action: () => hashNavigate("/settings", { replace: true })
}
];
export function registerKeyMap() {

View File

@@ -33,6 +33,7 @@ type DialogButtonProps = ButtonProps & {
disabled?: boolean;
text: JSX.Element | string;
loading?: boolean;
role?: string;
};
type DialogProps = SxProp & {
@@ -113,6 +114,13 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
...props.sx
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
props.onClose?.();
}
}}
>
{props.showCloseButton && (
<Close
@@ -183,6 +191,7 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
{...props.negativeButton}
color="paragraph"
data-test-id="dialog-no"
role="negative-button"
/>
)}
{props.positiveButton && (
@@ -190,6 +199,7 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
{...props.positiveButton}
color="accent"
data-test-id="dialog-yes"
role="positive-button"
/>
)}
</Flex>
@@ -202,13 +212,21 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
export default BaseDialog;
export function DialogButton(props: DialogButtonProps) {
export function DialogButton({
disabled,
onClick,
loading,
text,
role,
...props
}: DialogButtonProps) {
return (
<Button
{...props}
variant="dialog"
disabled={props.disabled}
onClick={props.disabled ? undefined : props.onClick}
disabled={disabled}
onClick={disabled ? undefined : onClick}
data-role={role}
sx={{
maxWidth: "100%",
textOverflow: "ellipsis",
@@ -216,7 +234,7 @@ export function DialogButton(props: DialogButtonProps) {
whiteSpace: "nowrap"
}}
>
{props.loading ? <Loading size={16} color="accent" /> : props.text}
{loading ? <Loading size={16} color="accent" /> : text}
</Button>
);
}
@@ -225,6 +243,7 @@ function onAfterOpen(
e: ReactModal.OnAfterOpenCallbackOptions | undefined,
props: DialogProps
) {
if (props.onOpen) props.onOpen();
if (!props.onClose || !e) return;
// we need this work around because ReactModal content spreads over the overlay
const child = e.contentEl.firstElementChild as HTMLElement;
@@ -241,5 +260,4 @@ function onAfterOpen(
if (props.onClose) props.onClose();
}
};
if (props.onOpen) props.onOpen();
}

View File

@@ -117,13 +117,15 @@ export function EditorActionBar() {
title: strings.undo(),
icon: Undo,
enabled: editorManager?.canUndo,
onClick: () => editorManager?.editor?.undo()
onClick: () => editorManager?.editor?.undo(),
hidden: activeSession?.type === "readonly"
},
{
title: strings.redo(),
icon: Redo,
enabled: editorManager?.canRedo,
onClick: () => editorManager?.editor?.redo()
onClick: () => editorManager?.editor?.redo(),
hidden: activeSession?.type === "readonly"
},
{
title: isNotePublished ? strings.published() : strings.publish(),
@@ -408,15 +410,7 @@ const TabStrip = React.memo(function TabStrip() {
)
: undefined
}
onPin={() => {
const tabs = useEditorStore.getState().tabs.slice();
const index = tabs.findIndex((t) => t.id === tab.id);
tabs[index].pinned = !tabs[index].pinned;
tabs.sort((a, b) =>
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
);
useEditorStore.setState({ tabs });
}}
onPin={() => useEditorStore.getState().pinTab(tab.id)}
/>
);
}}

View File

@@ -44,6 +44,7 @@ import { MAX_AUTO_SAVEABLE_WORDS } from "./types";
import { strings } from "@notesnook/intl";
import { EDITOR_ZOOM } from "./common";
import { useWindowControls } from "../../hooks/use-window-controls";
import { exitFullscreen } from "../../utils/fullscreen";
const SAVE_STATE_ICON_MAP = {
"-1": NotSaved,
@@ -96,13 +97,7 @@ function EditorFooter() {
icon: isFocusMode ? FocusMode : NormalMode,
enabled: true,
hideOnMobile: true,
onClick: () => {
useAppStore.getState().toggleFocusMode();
if (document.fullscreenElement) exitFullscreen();
const editor =
session && useEditorManager.getState().getEditor(session.id);
if (editor) editor.editor?.focus();
}
onClick: () => useAppStore.getState().toggleFocusMode()
}
];
@@ -149,9 +144,15 @@ function EditorFooter() {
>
<Minus size={13} />
</Button>
<Text variant="subBody" sx={{ color: "paragraph" }}>
{editorConfig.zoom}%
</Text>
<Button
title={strings.clickToReset(strings.zoom().toLocaleLowerCase())}
variant="statusitem"
onClick={() => setEditorConfig({ zoom: EDITOR_ZOOM.DEFAULT })}
>
<Text variant="subBody" sx={{ color: "paragraph" }}>
{editorConfig.zoom}%{" "}
</Text>
</Button>
<Button
variant="icon"
onClick={() =>
@@ -224,8 +225,3 @@ export default EditorFooter;
function enterFullscreen(elem: HTMLElement) {
elem.requestFullscreen();
}
function exitFullscreen() {
if (!document.fullscreenElement) return;
document.exitFullscreen();
}

View File

@@ -35,9 +35,11 @@ import { strings } from "@notesnook/intl";
type HeaderProps = { readonly: boolean; id: string };
function Header(props: HeaderProps) {
const { readonly, id } = props;
const tags = useEditorStore(
(store) => store.getActiveSession(["default", "readonly"])?.tags
const activeSession = useEditorStore((store) =>
store.getActiveSession(["default", "readonly"])
);
const tags = activeSession?.tags || [];
const noteId = activeSession?.note.id;
const setTag = useCallback(async function (
noteId: string,
@@ -62,7 +64,12 @@ function Header(props: HeaderProps) {
return (
<>
<Flex
sx={{ lineHeight: 2.5, alignItems: "center", flexWrap: "wrap" }}
sx={{
lineHeight: 2.5,
alignItems: "center",
flexWrap: "wrap",
opacity: !noteId ? 0.5 : 1
}}
data-test-id="tags"
>
{tags?.map((tag) => (
@@ -72,7 +79,11 @@ function Header(props: HeaderProps) {
text={tag.title}
icon={TagIcon}
onClick={() => navigate(`/tags/${tag.id}`)}
onDismiss={readonly ? undefined : () => setTag(id, tags, tag.title)}
onDismiss={
readonly || !noteId
? undefined
: () => setTag(noteId, tags, tag.title)
}
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
/>
))}
@@ -80,8 +91,10 @@ function Header(props: HeaderProps) {
<Autosuggest
sessionId={id}
filter={(query) => db.lookup.tags(query).items(10)}
disabled={!noteId}
toMenuItems={(filtered, reset, query) => {
const items: MenuItem[] = [];
if (!noteId) return items;
const isExactMatch =
!!query && filtered.some((item) => item.title === query);
if (query && !isExactMatch) {
@@ -90,7 +103,7 @@ function Header(props: HeaderProps) {
key: "new",
title: `Create "${query}" tag`,
icon: Plus.path,
onClick: () => setTag(id, tags, query).finally(reset)
onClick: () => setTag(noteId, tags, query).finally(reset)
});
}
@@ -101,17 +114,22 @@ function Header(props: HeaderProps) {
key: item.id,
title: item.title,
icon: TagIcon.path,
onClick: () => setTag(id, tags, item.title).finally(reset)
onClick: () =>
setTag(noteId, tags, item.title).finally(reset)
}))
);
}
return items;
}}
onAdd={(value) => setTag(id, tags, value)}
onAdd={(value) => {
if (!noteId) return;
setTag(noteId, tags, value);
}}
onRemove={() => {
if (!noteId) return;
if (tags.length <= 0) return;
setTag(id, tags, tags[tags.length - 1].title);
setTag(noteId, tags, tags[tags.length - 1].title);
}}
defaultItems={() =>
db.tags.all.limit(10).items(undefined, {
@@ -134,6 +152,7 @@ type AutosuggestProps<T> = {
onAdd: (text: string) => void;
toMenuItems: (filtered: T[], reset: () => void, query?: string) => MenuItem[];
defaultItems: () => Promise<T[]>;
disabled?: boolean;
};
export function Autosuggest<T>(props: AutosuggestProps<T>) {
const { sessionId, filter, onRemove, onAdd, defaultItems, toMenuItems } =
@@ -188,6 +207,7 @@ export function Autosuggest<T>(props: AutosuggestProps<T>) {
ref={inputRef}
tabIndex={-1}
variant="clean"
disabled={props.disabled}
sx={{
width: "auto",
border: "none",

View File

@@ -23,7 +23,8 @@ import React, {
useRef,
PropsWithChildren,
Suspense,
useLayoutEffect
useLayoutEffect,
useCallback
} from "react";
import ReactDOM from "react-dom";
import { Box, Button, Flex, Progress, Text } from "@theme-ui/components";
@@ -223,13 +224,13 @@ export default function TabsView() {
);
}
const MemoizedEditorView = React.memo(
EditorView,
(prev, next) =>
const MemoizedEditorView = React.memo(EditorView, (prev, next) => {
return (
prev.session.id === next.session.id &&
prev.session.type === next.session.type &&
prev.session.needsHydration === next.session.needsHydration
);
);
});
function EditorView({
session
}: {
@@ -298,11 +299,11 @@ function EditorView({
};
}, [editor]);
useEffect(() => {
if (!session.needsHydration && session.content) {
editor?.updateContent(session.content.data);
}
}, [editor, session]);
const getContent = useCallback(() => {
return session.content?.data;
}, [session.content?.data]);
if (session.needsHydration) return null;
return (
<Flex
@@ -321,7 +322,7 @@ function EditorView({
<Editor
id={session.id}
nonce={1}
content={() => session.content?.data}
content={getContent}
session={session}
onPreviewDocument={(preview) =>
useEditorStore.setState({ documentPreview: preview })
@@ -665,10 +666,12 @@ function EditorChrome(props: PropsWithChildren<EditorProps>) {
parent.left - child.left - CONTAINER_MARGIN
);
editor.style.marginLeft = `-${negativeSpace}px`;
editor.style.marginRight = `-${negativeSpace}px`;
editor.style.paddingLeft = `${negativeSpace}px`;
editor.style.paddingRight = `${negativeSpace}px`;
requestAnimationFrame(() => {
editor.style.marginLeft = `-${negativeSpace}px`;
editor.style.marginRight = `-${negativeSpace}px`;
editor.style.paddingLeft = `${negativeSpace}px`;
editor.style.paddingRight = `${negativeSpace}px`;
});
}
const observer = new ResizeObserver(debounce(onResize, 500));
observer.observe(editorScrollRef.current);
@@ -862,7 +865,7 @@ function restoreScrollPosition(session: EditorSession) {
function restoreSelection(editor: IEditor, id: string) {
setTimeout(() => {
editor.focus({
position: Config.get(`${id}:selection`, { from: 0, to: 0 })
position: Config.get(`${id}:selection`)
});
});
}

View File

@@ -148,7 +148,9 @@ function TableOfContents(props: TableOfContentsProps) {
? "accent-selected"
: "paragraph"
}}
onClick={() => scrollIntoViewById(t.id)}
onClick={() =>
scrollIntoViewById(t.id, "scroll-margin-top: 35px;")
}
>
{t.title}
</Button>

View File

@@ -297,18 +297,19 @@ function TipTap(props: TipTapProps) {
previewAttachment: onPreviewAttachment,
createInternalLink: onInsertInternalLink,
getAttachmentData: onGetAttachmentData,
openLink: (url) => {
openLink: (url, openInNewTab) => {
const link = parseInternalLink(url);
if (link && link.type === "note") {
useEditorStore.getState().openSession(link.id, {
activeBlockId: link.params?.blockId || undefined
activeBlockId: link.params?.blockId || undefined,
openInNewTab: openInNewTab
});
} else window.open(url, "_blank");
}
};
}, [
content,
readonly,
nonce,
doubleSpacedLines,
dateFormat,
timeFormat,

View File

@@ -17,25 +17,17 @@ 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 React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from "react";
import { Input } from "@theme-ui/components";
import React, { useEffect, useLayoutEffect, useMemo, useRef } from "react";
import { Textarea } from "@theme-ui/components";
import { useEditorStore } from "../../stores/editor-store";
import { debounceWithId } from "@notesnook/common";
import useMobile from "../../hooks/use-mobile";
import useTablet from "../../hooks/use-tablet";
import { useEditorConfig, useEditorManager } from "./manager";
import { getFontById } from "@notesnook/editor";
import { replaceDateTime } from "@notesnook/editor";
import { useStore as useSettingsStore } from "../../stores/setting-store";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { strings } from "@notesnook/intl";
import { NEWLINE_STRIP_REGEX } from "@notesnook/core";
type TitleBoxProps = {
id: string;
@@ -44,12 +36,10 @@ type TitleBoxProps = {
function TitleBox(props: TitleBoxProps) {
const { readonly, id } = props;
const inputRef = useRef<HTMLInputElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const pendingChanges = useRef(false);
// const id = useStore((store) => store.session.id);
const sessionType = useEditorStore((store) => store.getSession(id)?.type);
const isMobile = useMobile();
const isTablet = useTablet();
const { editorConfig } = useEditorConfig();
const dateFormat = useSettingsStore((store) => store.dateFormat);
const timeFormat = useSettingsStore((store) => store.timeFormat);
@@ -59,18 +49,6 @@ function TitleBox(props: TitleBoxProps) {
[editorConfig.fontFamily]
);
const updateFontSize = useCallback(
(length: number) => {
if (!inputRef.current) return;
const fontSize = textLengthToFontSize(
length,
isMobile || isTablet ? 1.625 : 2.625
);
inputRef.current.style.fontSize = `${fontSize}em`;
},
[isMobile, isTablet]
);
useLayoutEffect(() => {
const session = useEditorStore.getState().getSession(id);
if (!session || !("note" in session) || !session.note || !inputRef.current)
@@ -80,17 +58,11 @@ function TitleBox(props: TitleBoxProps) {
const { title } = session.note;
if (inputRef.current.value === title) return;
withSelectionPersist(
inputRef.current,
(input) => (input.value = title || "")
);
updateFontSize(title?.length || 0);
}, [sessionType, id, updateFontSize]);
useEffect(() => {
if (!inputRef.current) return;
updateFontSize(inputRef.current.value.length);
}, [isTablet, isMobile, updateFontSize]);
withSelectionPersist(inputRef.current, (input) => {
input.value = title || "";
resizeTextarea(input);
});
}, [sessionType, id]);
useEffect(() => {
const { unsubscribe } = AppEventManager.subscribe(
@@ -105,11 +77,10 @@ function TitleBox(props: TitleBoxProps) {
sessionId: string;
}) => {
if (!inputRef.current || sessionId !== id) return;
withSelectionPersist(
inputRef.current,
(input) => (input.value = title)
);
updateFontSize(title.length);
withSelectionPersist(inputRef.current, (input) => {
input.value = title;
resizeTextarea(input);
});
if (!preventSave) {
pendingChanges.current = true;
debouncedOnTitleChange(sessionId, sessionId, title, pendingChanges);
@@ -120,10 +91,10 @@ function TitleBox(props: TitleBoxProps) {
return () => {
unsubscribe();
};
}, [updateFontSize, id]);
}, [id]);
return (
<Input
<Textarea
ref={inputRef}
variant="clean"
id="editor-title"
@@ -132,32 +103,46 @@ function TitleBox(props: TitleBoxProps) {
placeholder={strings.noteTitle()}
readOnly={readonly}
dir="auto"
wrap="soft"
rows={1}
sx={{
m: 0,
p: 0,
fontFamily,
fontSize: ["1.625em", "1.625em", "2.625em"],
fontWeight: "heading",
width: "100%",
fieldSizing: "content",
whiteSpace: "pre-wrap",
resize: "none",
overflow: "hidden",
"::placeholder": {
color: "placeholder"
}
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
}
}}
onKeyUp={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const context = useEditorManager.getState().getEditor(id);
if (!context) return;
context.editor?.focus({ scrollIntoView: true });
}
}}
onChange={(e) => {
if (!(e.target instanceof HTMLTextAreaElement)) return;
pendingChanges.current = true;
e.target.value = replaceDateTime(
e.target.value,
dateFormat,
timeFormat
);
).replace(NEWLINE_STRIP_REGEX, " ");
debouncedOnTitleChange(id, id, e.target.value, pendingChanges);
updateFontSize(e.target.value.length);
resizeTextarea(e.target);
}}
/>
);
@@ -167,6 +152,15 @@ export default React.memo(TitleBox, (prevProps, nextProps) => {
return prevProps.readonly === nextProps.readonly;
});
function resizeTextarea(input: HTMLTextAreaElement) {
requestAnimationFrame(() => {
input.style.height = "auto";
requestAnimationFrame(() => {
input.style.height = input.scrollHeight + "px";
});
});
}
function onTitleChange(
noteId: string,
title: string,
@@ -178,16 +172,9 @@ function onTitleChange(
const debouncedOnTitleChange = debounceWithId(onTitleChange, 100);
function textLengthToFontSize(length: number, max: number) {
const stepLength = 35;
const decreaseStep = 0.5;
const steps = length / stepLength;
return Math.max(1.2, Math.min(max, max - steps * decreaseStep));
}
function withSelectionPersist(
input: HTMLInputElement,
action: (input: HTMLInputElement) => void
input: HTMLTextAreaElement,
action: (input: HTMLTextAreaElement) => void
) {
const selection = {
start: input.selectionStart,

View File

@@ -220,7 +220,8 @@ import {
mdiTagOutline,
mdiChatQuestionOutline,
mdiNoteRemoveOutline,
mdiTabPlus
mdiTabPlus,
mdiRadar
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -286,7 +287,7 @@ export type Icon = {
path: string;
};
function createIcon(path: string, rotate = false) {
export function createIcon(path: string, rotate = false) {
const NNIcon: Icon = function Icon(props) {
const { sx, rotate: _rotate = rotate, size, ...restProps } = props;
return (
@@ -563,3 +564,4 @@ export const OpenInNew = createIcon(mdiOpenInNew);
export const Coupon = createIcon(mdiTagOutline);
export const Support = createIcon(mdiChatQuestionOutline);
export const NewTab = createIcon(mdiTabPlus);
export const Radar = createIcon(mdiRadar);

View File

@@ -26,7 +26,12 @@ export type NotebookContext = {
};
export type Context =
| {
type: "tag" | "color";
type: "tag";
id: string;
item?: Tag;
}
| {
type: "color";
id: string;
}
| NotebookContext

View File

@@ -143,7 +143,7 @@ function Note(props: NoteProps) {
background: "background"
}}
context={{ color, locked }}
menuItems={menuItems}
menuItems={noteMenuItems}
onClick={() => useEditorStore.getState().openSession(note)}
onMiddleClick={() =>
useEditorStore.getState().openSession(note, { openInNewTab: true })
@@ -320,9 +320,7 @@ const formats = [
}
] as const;
const notFullySyncedText =
"Cannot perform this action because note is not fully synced.";
const menuItems: (
export const noteMenuItems: (
note: NoteType,
ids?: string[],
context?: { color?: Color; locked?: boolean }

View File

@@ -54,6 +54,7 @@ type PaneOptions = {
nextSize?: number;
initialSize: number;
collapsed: boolean;
expandedSize?: number;
};
export type SplitPaneImperativeHandle = {
@@ -97,7 +98,6 @@ export const SplitPane = React.forwardRef<
const wrapSize = useRef(0);
const childrenLength = childrenToArray(children).length;
const autoSaveKey = autoSaveId ? `csp:${autoSaveId}` : undefined;
const lastCollapsedPaneSize = useRef(0);
const { sizeName, splitPos, splitAxis } = useMemo(
() =>
@@ -151,6 +151,19 @@ export const SplitPane = React.forwardRef<
else Config.set(`${autoSaveKey}-${id}`, v);
}
});
Object.defineProperty(limits, "expandedSize", {
get() {
return Config.get(
`${autoSaveKey}-${id}:expandedSize`,
assertsSize(initialSize, wrapSize.current)
);
},
set(v) {
if (v === null || v === undefined || v === Infinity)
Config.remove(`${autoSaveKey}-${id}:expandedSize`);
else Config.set(`${autoSaveKey}-${id}:expandedSize`, v);
}
});
}
return limits;
}) || [];
@@ -257,15 +270,19 @@ export const SplitPane = React.forwardRef<
() => {
return {
collapse: (index: number) => {
paneSizes.current[index].collapsed = true;
lastCollapsedPaneSize.current = paneSizes.current[index].size;
const currentPane = paneSizes.current[index];
if (currentPane.collapsed) return;
currentPane.collapsed = true;
currentPane.expandedSize = paneSizes.current[index].size;
setSizes(paneSizes.current, wrapSize.current);
},
expand: (index: number) => {
paneSizes.current[index].collapsed = false;
paneSizes.current[index].size = lastCollapsedPaneSize.current
? lastCollapsedPaneSize.current
: paneSizes.current[index].initialSize;
const currentPane = paneSizes.current[index];
if (!currentPane.collapsed) return;
currentPane.collapsed = false;
currentPane.size =
currentPane.expandedSize ?? currentPane.initialSize;
currentPane.expandedSize = undefined;
setSizes(paneSizes.current, wrapSize.current);
},
isCollapsed: (index: number) => {
@@ -308,6 +325,8 @@ export const SplitPane = React.forwardRef<
const nextPane = paneSizes.current[i + 1];
const rightBorder = sashPosSizes.current[i + 2];
if (currentPane.collapsed) currentPane.collapsed = false;
if (currentPane.size + distanceX >= rightBorder)
distanceX = rightBorder - currentPane.size;

View File

@@ -44,6 +44,7 @@ import { showUpdateAvailableNotice } from "../../dialogs/confirm";
import { strings } from "@notesnook/intl";
import { useVault } from "../../hooks/use-vault";
import { useKeyStore } from "../../interfaces/key-store";
import { STATUS_BAR_HEIGHT } from "../../common/constants";
function StatusBar() {
const user = useUserStore((state) => state.user);
@@ -64,7 +65,7 @@ function StatusBar() {
justifyContent: "space-between",
display: ["none", "flex", "flex"],
flexShrink: 0,
height: 24
height: STATUS_BAR_HEIGHT
}}
px={2}
>

View File

@@ -60,7 +60,7 @@ function Tag(props: TagProps) {
);
}
}}
menuItems={menuItems}
menuItems={tagMenuItems}
onClick={() => {
navigate(`/tags/${id}`);
}}
@@ -73,7 +73,7 @@ function Tag(props: TagProps) {
}
export default Tag;
const menuItems: (tag: TagType, ids?: string[]) => MenuItem[] = (
export const tagMenuItems: (tag: TagType, ids?: string[]) => MenuItem[] = (
tag,
ids = []
) => {

View File

@@ -61,7 +61,7 @@ function TrashItem(props: TrashItemProps) {
</Text>
</Flex>
}
menuItems={menuItems}
menuItems={trashMenuItems}
onClick={async () => {
if (item.itemType === "note")
useEditorStore.getState().openSession(item);
@@ -71,10 +71,10 @@ function TrashItem(props: TrashItemProps) {
}
export default TrashItem;
const menuItems: (item: TrashItemType, ids?: string[]) => MenuItem[] = (
item,
ids = []
) => {
export const trashMenuItems: (
item: TrashItemType,
ids?: string[]
) => MenuItem[] = (item, ids = []) => {
return [
{
type: "button",
@@ -100,7 +100,7 @@ const menuItems: (item: TrashItemType, ids?: string[]) => MenuItem[] = (
];
};
async function deleteTrash(ids: string[]) {
export async function deleteTrash(ids: string[]) {
if (!(await showMultiPermanentDeleteConfirmation(ids.length))) return;
await store.delete(...ids);
showToast("success", `${pluralize(ids.length, "item")} permanently deleted`);

View File

@@ -156,6 +156,7 @@ export const AddReminderDialog = DialogManager.register(
testId="add-reminder-dialog"
onClose={() => props.onClose(false)}
sx={{ fontFamily: "body" }}
width={600}
positiveButton={{
text: reminder ? strings.save() : strings.add(),
disabled:

View File

@@ -189,7 +189,7 @@ export const AttachmentsDialog = DialogManager.register(
overflow: "hidden"
}}
>
<FlexScrollContainer>
<FlexScrollContainer style={{ minHeight: 300 }}>
<Flex sx={{ justifyContent: "space-between" }}>
<Flex sx={{ gap: 1 }}>
{TOOLBAR_ACTIONS.map((tool) => (

View File

@@ -0,0 +1,559 @@
/*
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 { debounce, 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";
import {
GroupedVirtuoso,
GroupedVirtuosoHandle,
CalculateViewLocation
} from "react-virtuoso";
import { db } from "../../common/db";
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
import Dialog from "../../components/dialog";
import { Cross } from "../../components/icons";
import { CustomScrollbarsVirtualList } from "../../components/list-container";
import { useEditorStore } from "../../stores/editor-store";
import { strings } from "@notesnook/intl";
import { isMac } from "../../utils/platform";
import {
getDefaultCommands,
Command,
getRecentCommands,
resolveRecentCommand,
commandActions,
commandIcons,
removeRecentCommand,
addRecentCommand
} from "./commands";
type CommandPaletteDialogProps = BaseDialogProps<boolean> & {
isCommandMode: boolean;
};
const COMMAND_PALETTE_STICKY_HEADER_HEIGHT = 32;
export const CommandPaletteDialog = DialogManager.register(
function CommandPaletteDialog(props: CommandPaletteDialogProps) {
const [selected, setSelected] = useState<number>(0);
const [query, setQuery] = useState<string>("");
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null);
const { search: dbSearch } = useDatabaseFuzzySearch();
const defaultCommands = useRef<Command[]>();
const select = useCallback((index: number) => {
setSelected(index);
virtuosoRef.current?.scrollIntoView({
index: index,
calculateViewLocation: calculateCommandItemLocation
});
}, []);
const commands = usePromise(async () => {
select(0);
if (!defaultCommands.current)
defaultCommands.current = await getDefaultCommands();
const commands = props.isCommandMode
? sortCommands(commandSearch(query, defaultCommands.current))
: await dbSearch(query);
const groups = commands.reduce(
(acc, command) => {
const index = acc.keys.indexOf(command.group);
if (index === -1) {
acc.keys.push(command.group);
acc.counts.push(1);
} else {
acc.counts[index]++;
}
return acc;
},
{ counts: [], keys: [] } as {
counts: number[];
keys: string[];
}
);
return { commands, groups };
}, [dbSearch, query]);
return (
<Dialog
isOpen={true}
width={650}
onClose={() => {
props.onClose(false);
}}
noScroll
sx={{
fontFamily: "body"
}}
>
<Flex
variant="columnFill"
sx={{
overflow: "hidden",
height: 400,
'[data-viewport-type="element"]': {
width: "calc(100% - 10px) !important",
px: 1
}
}}
onKeyDown={(e) => {
if (commands.status !== "fulfilled") return;
if (e.key == "Enter") {
e.preventDefault();
const command = commands.value.commands[selected];
if (!command) return;
command.action?.(command, {
openInNewTab: e.ctrlKey || e.metaKey
});
addRecentCommand(command);
props.onClose(false);
}
if (e.key === "ArrowDown") {
e.preventDefault();
select(getNextCommandIndex(selected, commands.value.commands));
}
if (e.key === "ArrowUp") {
e.preventDefault();
select(
getPreviousCommandIndex(selected, commands.value.commands)
);
}
}}
>
<Input
autoFocus
variant="clean"
placeholder={
props.isCommandMode
? strings.executeACommand()
: strings.searchForNotesNotebooksAndTags()
}
sx={{
m: 0,
marginTop: 0,
mr: 0,
px: 2,
borderRadius: 0,
borderBottom: "1px solid var(--border)"
}}
onChange={debounce((e) => {
setQuery(e.target.value);
}, 100)}
/>
<GroupedVirtuoso
ref={virtuosoRef}
style={{ overflow: "hidden" }}
components={{
Scroller: CustomScrollbarsVirtualList,
Footer: () => (
<div
className="footer"
style={{
height: "5px"
}}
/>
),
EmptyPlaceholder: () => (
<Text
sx={{
px: 1
}}
variant="subBody"
>
{query ? strings.noResultsFound(query) : ""}
</Text>
),
TopItemList: ({ style, ...props }) => (
<div
className="top-item-list"
{...props}
style={{
...style,
width: "calc(100% - 10px) !important",
padding: "0px 5px"
}}
/>
)
}}
groupCounts={
commands.status === "fulfilled"
? commands.value.groups.counts
: []
}
groupContent={(groupIndex) => {
if (commands.status !== "fulfilled") return null;
const label =
commands.value.groups.keys[groupIndex] === "recent"
? strings.recents()
: commands.value.groups.keys[groupIndex];
return (
<Box
sx={{
width: "100%",
py: 1,
bg: "background",
px: 1
}}
>
<Text
variant="subBody"
dangerouslySetInnerHTML={{ __html: label }}
/>
</Box>
);
}}
itemContent={(index) => {
if (commands.status !== "fulfilled") return null;
const command = commands.value.commands[index];
if (!command) return null;
return (
<Flex
key={index}
onClick={(e) => {
command.action?.(command, {
openInNewTab: e.ctrlKey || e.metaKey
});
addRecentCommand(command);
props.onClose(false);
}}
sx={{
cursor: "pointer",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
py: 1,
px: 1,
borderRadius: "default",
bg:
index === selected
? "background-selected"
: "transparent",
":hover:not(:disabled):not(:active)": {
bg: index === selected ? "hover-selected" : "hover"
}
}}
>
<Flex
sx={{
gap: 1
}}
>
{command.icon && (
<command.icon
size={14}
color={index === selected ? "icon-selected" : "icon"}
/>
)}
<Text
variant="body"
sx={{
textOverflow: "ellipsis",
overflow: "hidden"
}}
dangerouslySetInnerHTML={{
__html: command.title
}}
/>
</Flex>
{command.group === "recent" && (
<Button
title={strings.removeFromRecents()}
onClick={async (e) => {
e.stopPropagation();
removeRecentCommand(command.id);
defaultCommands.current = await getDefaultCommands();
commands.refresh();
}}
variant="secondary"
sx={{
bg: "transparent",
p: "small",
borderRadius: 100
}}
>
<Cross size={12} />
</Button>
)}
</Flex>
);
}}
/>
</Flex>
<Flex
sx={{
flexDirection: "row",
bg: "background-secondary",
px: 2,
py: 2,
gap: 3,
borderTop: "1px solid var(--border)",
justifyContent: "end",
alignItems: "center"
}}
>
{getCommandPaletteHelp(props.isCommandMode).map((key) => {
return (
<Flex
key={key.key}
sx={{
alignItems: "center",
justifyContent: "center",
gap: 1
}}
>
<Text variant="body">{key.description}</Text>
<Text variant="subBody">{key.key}</Text>
</Flex>
);
})}
</Flex>
</Dialog>
);
}
);
function getNextCommandIndex(selected: number, commands: Command[]) {
return (selected + 1) % commands.length;
}
function getPreviousCommandIndex(selected: number, commands: Command[]) {
return (selected - 1 + commands.length) % commands.length;
}
const RESULT_PREFIX_SUFFIX = {
prefix: "<b style='color: var(--accent-foreground)'>",
suffix: "</b>"
};
function commandSearch(query: string, commands: Command[]) {
if (!query) return commands;
return fuzzy(
query,
commands,
(command) => command.id + command.group,
{
title: 10,
group: 5
},
RESULT_PREFIX_SUFFIX
);
}
function useDatabaseFuzzySearch() {
const notes = useRef<Note[]>();
const notebooks = useRef<Notebook[]>();
const tags = useRef<Tag[]>();
const reminders = useRef<Reminder[]>();
const updateCollections = useCallback(async (force = false) => {
if (force || !notes.current)
notes.current = await db.notes.all
.fields(["notes.id", "notes.title"])
.items();
if (force || !notebooks.current)
notebooks.current = await db.notebooks.all
.fields(["notebooks.id", "notebooks.title"])
.items();
if (force || !tags.current)
tags.current = await db.tags.all
.fields(["tags.id", "tags.title"])
.items();
if (force || !reminders.current)
reminders.current = await db.reminders.all
.fields(["reminders.id", "reminders.title"])
.items();
}, []);
useEffect(() => {
const event = db.eventManager.subscribe(EVENTS.syncCompleted, async () => {
await updateCollections(true);
});
return () => {
event.unsubscribe();
reminders.current = undefined;
notebooks.current = undefined;
tags.current = undefined;
notes.current = undefined;
};
}, [updateCollections]);
const search = useCallback(
async (query: string) => {
if (!query) return await getSessionsAsCommands();
await updateCollections();
const list: Command[] = [];
const collections: Record<
"note" | "notebook" | "tag" | "reminder",
{ id: string; title: string }[] | undefined
> = {
note: notes.current,
notebook: notebooks.current,
tag: tags.current,
reminder: reminders.current
};
for (const _type in collections) {
const type = _type as keyof typeof collections;
const items = collections[type];
if (!items) continue;
for (const item of fuzzy(
query,
items,
(item) => item.id,
{
title: 10
},
RESULT_PREFIX_SUFFIX
)) {
list.push({
id: item.id,
title: item.title,
group: strings.dataTypesPluralCamelCase[type](),
type: type,
action: commandActions[type],
icon: commandIcons[type]
});
}
}
return list;
},
[updateCollections]
);
return { search };
}
async function getSessionsAsCommands() {
const commands: Command[] = [];
for (const recentCommand of getRecentCommands()) {
if (recentCommand.type === "command") continue;
const resolvedCommand = await resolveRecentCommand(recentCommand);
if (resolvedCommand) commands.push(resolvedCommand);
}
const sessions = useEditorStore.getState().get().sessions;
for (const session of sessions) {
if (
session.type === "new" ||
commands.find((c) => c.id === session.note.id)
)
continue;
commands.push({
id: session.note.id,
title: session.note.title,
group: strings.dataTypesPluralCamelCase.note(),
type: "note" as const,
action: commandActions.note,
icon: commandIcons.note
});
}
return commands;
}
/**
* This override is required to ensure smooth scrolling when moving between
* command palette items using the keyboard. Without this the selected item
* is not visible when moving up/down in the command palette.
*/
const calculateCommandItemLocation: CalculateViewLocation = ({
itemTop,
itemBottom,
viewportTop,
viewportBottom,
locationParams: { behavior, align, ...rest }
}) => {
const topOffset = viewportTop + COMMAND_PALETTE_STICKY_HEADER_HEIGHT;
const itemInView = itemTop >= topOffset && itemBottom <= viewportBottom;
if (itemInView) return null;
return {
...rest,
offset:
itemBottom > viewportBottom
? itemBottom - viewportBottom
: itemTop - topOffset,
behavior,
align:
align ??
(itemBottom > viewportBottom
? "end"
: itemTop < topOffset
? "start"
: "center")
};
};
function getCommandPaletteHelp(isCommandMode: boolean) {
return [
{
key: "⏎",
description: isCommandMode ? strings.execute() : strings.open()
},
...(isCommandMode
? [
{
key: isMac() ? "⌘P" : "Ctrl+P",
description: strings.quickOpen()
}
]
: [
{
key: isMac() ? "⌘⏎" : "Ctrl+⏎",
description: strings.openInNewTab()
},
{
key: isMac() ? "⌘K" : "Ctrl+K",
description: strings.commandPalette()
}
])
];
}
/**
* commands need to be sorted wrt groups,
* meaning commands of same group should be next to each other,
* and recent commands should be at the top
*/
function sortCommands(commands: Command[]) {
const recent: Command[] = [];
const sortedWrtGroups: Command[][] = [];
for (const command of commands) {
const group = command.group;
if (group === "recent") {
recent.push(command);
continue;
}
const index = sortedWrtGroups.findIndex((c) => c[0].group === group);
if (index === -1) {
sortedWrtGroups.push([command]);
} else {
sortedWrtGroups[index].push(command);
}
}
return recent.concat(sortedWrtGroups.flat());
}

View File

@@ -0,0 +1,529 @@
/*
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 { strings } from "@notesnook/intl";
import { db } from "../../common/db";
import {
ArrowLeft,
ArrowRight,
ArrowTopRight,
createIcon,
Icon,
Plus,
Radar,
Redo,
Undo,
Notebook as NotebookIcon,
Note as NoteIcon,
Reminder as ReminderIcon,
Tag as TagIcon,
FocusMode,
NormalMode
} from "../../components/icons";
import { trashMenuItems } from "../../components/trash-item";
import { hashNavigate, navigate } from "../../navigation";
import { useEditorStore } from "../../stores/editor-store";
import { useStore as useNoteStore } from "../../stores/note-store";
import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useThemeStore } from "../../stores/theme-store";
import { AttachmentsDialog } from "../attachments-dialog";
import { CreateColorDialog } from "../create-color-dialog";
import { noteMenuItems } from "../../components/note";
import { MenuItem } from "@notesnook/ui";
import { notebookMenuItems } from "../../components/notebook";
import { tagMenuItems } from "../../components/tag";
import { useEditorManager } from "../../components/editor/manager";
import Config from "../../utils/config";
export interface BaseCommand {
id: string;
type: "command" | "note" | "notebook" | "tag" | "reminder";
}
export interface Command extends BaseCommand {
title: string;
group: string;
icon?: Icon;
action: (command: Command, options?: { openInNewTab?: boolean }) => void;
excludeFromRecents?: boolean;
}
export interface RecentCommand extends BaseCommand {
id: string;
group: "recent";
}
export const commandActions = {
command: (command: Command) => getCommandById(command.id)?.action(command),
note: (command: Command, options?: { openInNewTab?: boolean }) =>
useEditorStore.getState().openSession(command.id, options),
notebook: (command: Command) => navigate(`/notebooks/${command.id}`),
tag: (command: Command) => navigate(`/tags/${command.id}`),
reminder: (command: Command) => hashNavigate(`/reminders/${command.id}/edit`)
};
export const commandIcons = {
command: (command: BaseCommand) => getCommandById(command.id)?.icon,
note: NoteIcon,
notebook: NotebookIcon,
tag: TagIcon,
reminder: ReminderIcon
};
const RECENT_COMMANDS_SAVE_KEY = "commandPalette:recent";
const staticCommands: Command[] = [
{
id: "notes",
title: strings.dataTypesPluralCamelCase.note(),
icon: ArrowTopRight,
action: () => navigate("/"),
group: strings.navigate(),
type: "command"
},
{
id: "notebooks",
title: strings.dataTypesPluralCamelCase.notebook(),
icon: ArrowTopRight,
action: () => navigate("/notebooks"),
group: strings.navigate(),
type: "command"
},
{
id: "tags",
title: strings.dataTypesPluralCamelCase.tag(),
icon: ArrowTopRight,
action: () => navigate("/tags"),
group: strings.navigate(),
type: "command"
},
{
id: "favorites",
title: strings.dataTypesPluralCamelCase.favorite(),
icon: ArrowTopRight,
action: () => navigate("/favorites"),
group: strings.navigate(),
type: "command"
},
{
id: "reminders",
title: strings.dataTypesPluralCamelCase.reminder(),
icon: ArrowTopRight,
action: () => navigate("/reminders"),
group: strings.navigate(),
type: "command"
},
{
id: "monographs",
title: strings.dataTypesPluralCamelCase.monograph(),
icon: ArrowTopRight,
action: () => navigate("/monographs"),
group: strings.navigate(),
type: "command"
},
{
id: "trash",
title: strings.trash(),
icon: ArrowTopRight,
action: () => navigate("/trash"),
group: strings.navigate(),
type: "command"
},
{
id: "settings",
title: strings.settings(),
icon: ArrowTopRight,
action: () => hashNavigate("/settings", { replace: true }),
group: strings.navigate(),
type: "command"
},
{
id: "help",
title: strings.helpAndSupport(),
icon: ArrowTopRight,
action: () => (window.location.href = "https://help.notesnook.com"),
group: strings.navigate(),
type: "command"
},
{
id: "attachment-manager",
title: strings.attachmentManager(),
icon: ArrowTopRight,
action: () => AttachmentsDialog.show({}),
group: strings.navigate(),
type: "command"
},
{
id: "new-note",
title: strings.newNote(),
icon: Plus,
action: () => useEditorStore.getState().newSession(),
group: strings.create(),
type: "command"
},
{
id: "new-notebook",
title: strings.newNotebook(),
icon: Plus,
action: () => hashNavigate("/notebooks/create", { replace: true }),
group: strings.create(),
type: "command"
},
{
id: "new-tag",
title: strings.newTag(),
icon: Plus,
action: () => hashNavigate("/tags/create", { replace: true }),
group: strings.create(),
type: "command"
},
{
id: "new-reminder",
title: strings.newReminder(),
icon: Plus,
action: () => hashNavigate(`/reminders/create`, { replace: true }),
group: strings.create(),
type: "command"
},
{
id: "new-color",
title: strings.newColor(),
icon: Plus,
action: () => CreateColorDialog.show(true),
group: strings.create(),
type: "command"
},
{
id: "toggle-theme",
title: strings.toggleTheme(),
icon: Radar,
action: () => useThemeStore.getState().toggleColorScheme(),
group: strings.general(),
type: "command"
}
];
export async function getDefaultCommands(): Promise<Command[]> {
const recentCommands: Command[] = [];
for (const recentCommand of getRecentCommands()) {
if (recentCommand.type !== "command") continue;
const resolvedCommand = await resolveRecentCommand(recentCommand);
if (resolvedCommand) recentCommands.push(resolvedCommand);
}
return [
...recentCommands,
...(await getActiveNoteCommands()),
...(await getActiveNotebookCommands()),
...(await getActiveTagCommands()),
...getEditorCommands(),
...staticCommands
];
}
export function getCommandById(id: string): Command | undefined {
return (
staticCommands.find((command) => command.id === id) ||
getEditorCommands().find((command) => command.id === id)
);
}
export async function resolveRecentCommand(
recentCommand: RecentCommand
): Promise<Command | undefined> {
const title = await getCommandTitle(recentCommand);
const action = commandActions[recentCommand.type];
if (!title || !action) return;
return {
id: recentCommand.id,
group: "recent",
title: title,
type: recentCommand.type,
icon:
recentCommand.type === "command"
? commandIcons.command(recentCommand)
: commandIcons[recentCommand.type],
action: action
};
}
export function getRecentCommands() {
return Config.get<RecentCommand[]>(RECENT_COMMANDS_SAVE_KEY, []);
}
export function addRecentCommand(command: Command) {
if (command.excludeFromRecents) return;
const commands = getRecentCommands();
const index = commands.findIndex((c) => c.id === command.id);
if (index > -1) commands.splice(index, 1);
commands.unshift({
group: "recent",
id: command.id,
type: command.type
});
Config.set(RECENT_COMMANDS_SAVE_KEY, commands);
}
export function removeRecentCommand(id: string) {
const commands = getRecentCommands();
const index = commands.findIndex((c) => c.id === id);
if (index > -1) {
commands.splice(index, 1);
Config.set(RECENT_COMMANDS_SAVE_KEY, commands);
}
}
async function getActiveNoteCommands(): Promise<Command[]> {
const note = useEditorStore.getState().getActiveNote();
if (!note) return [];
const group = strings.actionsForNote(note.title);
const commands: Command[] = [];
const menuItems =
note.type !== "trash"
? noteMenuItems(note, [note.id], {
locked: !!(
await db
.sql()
.selectFrom("content")
.where("noteId", "in", [note.id])
.select(["noteId", "locked"])
.executeTakeFirst()
)?.locked
})
: trashMenuItems(note);
for (const menuItem of menuItems) {
commands.push(...menuItemToCommands(menuItem, group, "active-note"));
}
return commands;
}
async function getActiveNotebookCommands() {
const context = useNoteStore.getState().context;
if (context?.type !== "notebook") return [];
const notebook = await db.notebooks.notebook(context.id);
if (!notebook) return [];
const group = strings.actionsForNotebook(notebook.title);
const commands: Command[] = [];
const menuItems = notebookMenuItems(notebook, [notebook.id]);
for (const menuItem of menuItems) {
commands.push(...menuItemToCommands(menuItem, group, "active-notebook"));
}
return commands;
}
async function getActiveTagCommands() {
const context = useNoteStore.getState().context;
if (context?.type !== "tag") return [];
const tag = await db.tags.tag(context.id);
if (!tag) return [];
const group = strings.actionsForTag(tag.title);
const commands: Command[] = [];
const menuItems = tagMenuItems(tag, [tag.id]);
for (const menuItem of menuItems) {
commands.push(...menuItemToCommands(menuItem, group, "active-tag"));
}
return commands;
}
function getEditorCommands(): Command[] {
const session = useEditorStore.getState().getActiveSession();
if (!session) return [];
const editor = useEditorManager.getState().editors[session.id];
const isFocusMode = useAppStore.getState().isFocusMode;
const commands: Command[] = [
{
id: "new-tab",
title: strings.newTab(),
icon: Plus,
action: () => useEditorStore.getState().addTab(),
group: strings.editor(),
type: "command"
},
{
id: "next-tab",
title: strings.nextTab(),
icon: ArrowTopRight,
action: () => useEditorStore.getState().focusNextTab(),
group: strings.editor(),
type: "command"
},
{
id: "previous-tab",
title: strings.previousTab(),
icon: ArrowTopRight,
action: () => useEditorStore.getState().focusPreviousTab(),
group: strings.editor(),
type: "command"
},
{
id: "close-tab",
title: strings.closeCurrentTab(),
icon: Radar,
action: () => useEditorStore.getState().closeActiveTab(),
group: strings.editor(),
type: "command"
},
{
id: "close-all-tabs",
title: strings.closeAllTabs(),
icon: Radar,
action: () => useEditorStore.getState().closeAllTabs(),
group: strings.editor(),
type: "command"
}
];
if (session.type !== "readonly" && (editor.canUndo || editor.canRedo)) {
commands.push(
{
id: "undo",
title: strings.undo(),
icon: Undo,
action: () => editor.editor?.undo(),
group: strings.editor(),
type: "command",
excludeFromRecents: true
},
{
id: "redo",
title: strings.redo(),
icon: Redo,
action: () => editor.editor?.redo(),
group: strings.editor(),
type: "command",
excludeFromRecents: true
}
);
}
if (useEditorStore.getState().canGoBack) {
commands.push({
id: "go-back-in-tab",
title: strings.goBackInTab(),
icon: ArrowLeft,
action: () => useEditorStore.getState().goBack(),
group: strings.editor(),
type: "command"
});
}
if (useEditorStore.getState().canGoForward) {
commands.push({
id: "go-forward-in-tab",
title: strings.goForwardInTab(),
icon: ArrowRight,
action: () => useEditorStore.getState().goForward(),
group: strings.editor(),
type: "command"
});
}
commands.push({
id: "toggle-focus-mode",
title: strings.toggleFocusMode(),
icon: isFocusMode ? FocusMode : NormalMode,
action: () => useAppStore.getState().toggleFocusMode(),
group: strings.editor(),
type: "command"
});
return commands;
}
function menuItemToCommands(
menuItem: MenuItem,
group: string,
idSuffix?: string
): Command[] {
if (menuItem.type !== "button" || menuItem.isHidden || menuItem.isDisabled)
return [];
const commands: Command[] = [];
if (menuItem.menu) {
for (const item of menuItem.menu.items) {
commands.push(
...menuItemToCommands(item, group, idSuffix).map((c) => {
c.title = `${menuItem.title}: ${c.title}`;
c.id = `${menuItem.key}-${c.id}`;
return c;
})
);
}
return commands;
}
if (!menuItem.onClick) return commands;
const _Icon = menuItem.icon ? createIcon(menuItem.icon) : undefined;
commands.push({
id: idSuffix ? `${menuItem.key}-${idSuffix}` : menuItem.key,
title: menuItem.title,
action: menuItem.onClick,
group,
type: "command",
icon: _Icon,
excludeFromRecents: true
});
return commands;
}
async function getCommandTitle({ id, type }: RecentCommand) {
switch (type) {
case "command": {
const command = getCommandById(id);
if (!command || typeof command.title !== "string") return;
return command.title;
}
case "note": {
const note = (await db.notes.all.fields(["notes.title"]).items([id])).at(
0
);
if (!note) return;
return note.title;
}
case "notebook": {
const notebook = (
await db.notebooks.all.fields(["notebooks.title"]).items([id])
).at(0);
if (!notebook) return;
return notebook.title;
}
case "tag": {
const tag = (await db.tags.all.fields(["tags.title"]).items([id])).at(0);
if (!tag) return;
return tag.title;
}
case "reminder": {
const reminder = (
await db.reminders.all.fields(["reminders.title"]).items([id])
).at(0);
if (!reminder) return;
return reminder.title;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
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/>.
*/
export * from "./command-palette-dialog";

View File

@@ -93,9 +93,15 @@ const features: Record<FeatureKeys, Feature> = {
]
: [
{
title: "Better tabs",
subtitle:
"Tabs have been overhauled to be more predictable and easier to use. Give it a try! We are sure you'll love it."
title: "Release tracks",
subtitle: (
<>
Switch to the beta version to try out upcoming new features and
provide us with feedback. Switch back to stable anytime without
losing any data. To change release tracks, go to Settings {">"}
About and select the desired release track.
</>
)
}
],
cta: {
@@ -128,12 +134,7 @@ export const FeatureDialog = DialogManager.register(function FeatureDialog(
) {
const { featureName, onClose } = props;
const feature = features[featureName];
useEffect(() => {
if (!feature || (feature.shouldShow && !feature.shouldShow())) {
onClose(false);
}
}, [feature, onClose]);
const isShown = feature && feature.shouldShow && feature.shouldShow();
return (
<Dialog
@@ -141,6 +142,9 @@ export const FeatureDialog = DialogManager.register(function FeatureDialog(
title={feature.title}
description={feature.subtitle}
textAlignment="center"
onOpen={() => {
if (!isShown) onClose(false);
}}
positiveButton={{
text: (
<Flex>

View File

@@ -186,7 +186,9 @@ export const BehaviourSettings: SettingsGroup[] = [
description: strings.automaticUpdatesDesc(),
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.autoUpdates, listener),
isHidden: () => useSettingStore.getState().isFlatpak,
isHidden: () =>
useSettingStore.getState().isFlatpak ||
useSettingStore.getState().isSnap,
components: [
{
type: "toggle",

View File

@@ -275,6 +275,7 @@ function SettingsSideBar(props: SettingsSideBarProps) {
<Input
id="search"
name="search"
autoFocus
placeholder={strings.search()}
data-test-id="settings-search"
sx={{

View File

@@ -27,6 +27,9 @@ import { clearLogs, downloadLogs } from "../../utils/logger";
import { useAutoUpdateStore } from "../../hooks/use-auto-updater";
import { IssueDialog } from "../issue-dialog";
import { strings } from "@notesnook/intl";
import { desktop } from "../../common/desktop-bridge";
import { TaskManager } from "../../common/task-manager";
import { useStore as useSettingStore } from "../../stores/setting-store";
export const AboutSettings: SettingsGroup[] = [
{
@@ -73,6 +76,76 @@ export const AboutSettings: SettingsGroup[] = [
];
}
},
{
key: "release-track",
title: strings.releaseTrack(),
description: strings.releaseTrackDesc(),
isHidden: () =>
useSettingStore.getState().isFlatpak ||
useSettingStore.getState().isSnap,
components: [
{
type: "dropdown",
options: [
{
title: strings.stable(),
value: "stable"
},
{
title: strings.beta(),
value: "beta"
}
],
selectedOption: async () => {
if (IS_DESKTOP_APP)
return (
(await desktop?.updater.releaseTrack.query()) || "stable"
);
return (
document.cookie
.split("; ")
.find((row) => row.startsWith("release-track="))
?.split("=")[1] || "stable"
);
},
async onSelectionChanged(value) {
if (IS_DESKTOP_APP) {
return await desktop?.updater.changeReleaseTrack.mutate({
track: value
});
}
const registration =
await navigator.serviceWorker.getRegistration();
if (!registration) return;
const worker =
registration.active ||
registration.waiting ||
registration.installing;
if (!worker) return;
if (worker.state === "activated") {
await switchReleaseTrack(value);
} else {
await TaskManager.startTask({
type: "modal",
title: "Changing release track",
subtitle:
"Please wait while we switch to the new release track...",
action: () =>
new Promise<void>((resolve) => {
worker.onstatechange = async function () {
if (this.state === "activated") {
await switchReleaseTrack(value);
resolve();
}
};
})
});
}
}
}
]
},
{
key: "source-code",
title: strings.sourceCode(),
@@ -338,3 +411,12 @@ export const SupportSettings: SettingsGroup[] = [
]
}
];
async function switchReleaseTrack(track: string) {
const registration = await navigator.serviceWorker.getRegistration();
if (!registration) return;
await registration.unregister();
for (const key of await caches.keys()) await caches.delete(key);
document.cookie = `release-track=${track}; Secure; Path=/`;
window.location.reload();
}

View File

@@ -168,6 +168,11 @@ const routes = defineRoutes({
title: async () => {
const tag = await db.tags.tag(tagId);
if (!tag) return;
notestore.setContext({
type: "tag",
id: tagId,
item: tag
});
return `#${tag.title}`;
},
component: Notes,

View File

@@ -36,7 +36,7 @@ import { Text } from "@theme-ui/components";
import { EV, EVENTS } from "@notesnook/core";
import { useEffect, useState } from "react";
export async function startApp() {
export async function startApp(children?: React.ReactNode) {
const rootElement = document.getElementById("root");
if (!rootElement) return;
const root = createRoot(rootElement);
@@ -72,6 +72,7 @@ export async function startApp() {
routeProps={props}
/>
</AppLock>
{children}
</BaseThemeProvider>
</GlobalErrorHandler>
</ErrorBoundary>

View File

@@ -99,7 +99,8 @@ self.addEventListener("message", (event) => {
event.source.postMessage({
type: data.type,
version: APP_VERSION,
hash: GIT_HASH
hash: GIT_HASH,
isBeta: IS_BETA
});
}
break;

View File

@@ -41,6 +41,9 @@ import {
import { NetworkCheck } from "../utils/network-check";
import { Color, Notebook, Tag } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useEditorStore } from "./editor-store";
import { useEditorManager } from "../components/editor/manager";
import { exitFullscreen } from "../utils/fullscreen";
type SyncState =
| "synced"
@@ -171,6 +174,10 @@ class AppStore extends BaseStore<AppStore> {
toggleFocusMode = () => {
this.set((state) => (state.isFocusMode = !state.isFocusMode));
if (document.fullscreenElement) exitFullscreen();
const session = useEditorStore.getState().getActiveSession();
const editor = session && useEditorManager.getState().getEditor(session.id);
if (editor) editor.editor?.focus();
};
toggleAutoSync = () => {

View File

@@ -135,7 +135,10 @@ class AttachmentStore extends BaseStore<AttachmentStore> {
if (await db.attachments.remove(attachment.hash, false)) {
await this.get().refresh();
await useNoteStore.getState().refresh();
useEditorStore.getState().closeSessions(...linkedNotes);
const sessions = linkedNotes
.map((id) => useEditorStore.getState().getSessionsForNote(id))
.flat();
useEditorStore.getState().closeTabs(...sessions.map((s) => s.id));
}
} catch (e) {
console.error(e);

View File

@@ -414,9 +414,13 @@ class EditorStore extends BaseStore<EditorStore> {
event.item.fromType === "color" &&
event.item.toType === "note"
) {
updateSession(event.item.toId, undefined, {
color: event.item.fromId
});
for (const session of sessions) {
if ("color" in session && session.note.id === event.item.toId) {
updateSession(session.id, undefined, {
color: event.item.fromId
});
}
}
} else if (
event.type === "unlink" &&
((event.reference.type === "color" &&
@@ -467,9 +471,13 @@ class EditorStore extends BaseStore<EditorStore> {
event.item.fromType === "tag" &&
event.item.toType === "note"
) {
updateSession(event.item.toId, undefined, {
tags: await db.notes.tags(event.item.toId)
});
for (const session of sessions) {
if ("note" in session && session.note.id === event.item.toId) {
updateSession(session.id, undefined, {
tags: await db.notes.tags(event.item.toId)
});
}
}
}
} else if (event.collection === "tags") {
if (event.type === "update") {
@@ -595,9 +603,18 @@ class EditorStore extends BaseStore<EditorStore> {
if (!oldContent || !currentContent) return;
const { getSession, addSession, sessions, activeTabId, tabs } = this.get();
const {
getSession,
addSession,
sessions,
activeTabId,
getActiveTab,
tabs
} = this.get();
const tabId = activeTabId ?? this.addTab();
const tabId = getActiveTab()?.pinned
? this.addTab(getId())
: activeTabId || this.addTab(getId());
const tab = tabs.find((t) => t.id === tabId);
const activeSession = tab && getSession(tab.sessionId);
const oldSession = sessions.find(
@@ -650,15 +667,19 @@ class EditorStore extends BaseStore<EditorStore> {
tabs,
activateSession,
activeTabId,
getActiveTab,
rehydrateSession,
getTabsForNote,
addTab
} = this.get();
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
const oldTabForNote = options.force ? null : getTabsForNote(noteId).at(0);
const tabId = options.openInNewTab
? addTab(getId())
: oldTabForNote?.id || activeTabId || addTab(getId());
const activeTab = getActiveTab();
const tabId =
// if a tab is pinned then always open a new tab.
options.openInNewTab || activeTab?.pinned
? addTab(getId())
: oldTabForNote?.id || activeTabId || addTab(getId());
const tab = tabs.find((t) => t.id === tabId);
const activeSession = tab && getSession(tab.sessionId);
@@ -889,8 +910,8 @@ class EditorStore extends BaseStore<EditorStore> {
ignoreEdit?: boolean;
}
) => {
const currentSession = this.getSession(id, ["new", "default"]);
if (!currentSession) return;
const session = this.getSession(id, ["new", "default"]);
if (!session) return;
// do not allow saving of readonly session
if (partial.note?.readonly) return;
@@ -898,16 +919,22 @@ class EditorStore extends BaseStore<EditorStore> {
await saveMutex.runExclusive(async () => {
this.setSaveState(id, 0);
try {
// Get session again as it might have changed to default.
const currentSession =
session.type === "new"
? this.getSession(id, ["new", "default"])
: session;
if (!currentSession) return;
const sessionId = getSessionId(currentSession);
const noteId =
("note" in currentSession
? currentSession.note.id
: partial.note?.id) || id;
let noteId =
"note" in currentSession ? currentSession.note.id : partial.note?.id;
if (isLockedSession(currentSession) && partial.content) {
logger.debug("Saving locked content", { id });
if (!noteId) return;
logger.debug("Saving locked content", { noteId });
await db.vault.save({
noteId = await db.vault.save({
content: partial.content,
sessionId,
id: noteId
@@ -915,10 +942,10 @@ class EditorStore extends BaseStore<EditorStore> {
} else {
if (partial.content)
logger.debug("Saving content", {
id,
noteId,
length: partial.content.data.length
});
await db.notes.add({
noteId = await db.notes.add({
...partial.note,
dateEdited:
partial.ignoreEdit && currentSession.type === "default"
@@ -934,24 +961,27 @@ class EditorStore extends BaseStore<EditorStore> {
});
}
const note = await db.notes.note(noteId);
const note = noteId && (await db.notes.note(noteId));
if (!note) throw new Error("Note not saved.");
if (currentSession.type === "new") {
const context = useNoteStore.getState().context;
const defaultNotebook = db.settings.getDefaultNotebook();
if (context) {
const { type } = context;
if (type === "notebook")
await db.notes.addToNotebook(context.id, noteId);
else if (type === "color" || type === "tag")
await db.notes.addToNotebook(context.id, note.id);
else if (type === "color" || type === "tag") {
await db.relations.add(
{ type, id: context.id },
{ id, type: "note" }
{ id: note.id, type: "note" }
);
if (defaultNotebook)
await db.notes.addToNotebook(defaultNotebook, note.id);
}
} else {
const defaultNotebook = db.settings.getDefaultNotebook();
if (defaultNotebook)
await db.notes.addToNotebook(defaultNotebook, noteId);
await db.notes.addToNotebook(defaultNotebook, note.id);
}
}
@@ -991,6 +1021,19 @@ class EditorStore extends BaseStore<EditorStore> {
sessionId
});
}
if (partial.note?.title !== undefined) {
const { sessions } = this.get();
for (const session of sessions) {
if ("note" in session && session.note.id === note.id) {
this.updateSession(session.id, undefined, {
note,
title: note.title
});
}
}
}
setDocumentTitle(
settingStore.get().hideNoteTitle ? undefined : note.title
);
@@ -1005,8 +1048,8 @@ class EditorStore extends BaseStore<EditorStore> {
this.setSaveState(id, SaveState.NotSaved);
console.error(err);
if (err instanceof Error) logger.error(err);
if (isLockedSession(currentSession) && "note" in currentSession) {
this.get().openSession(currentSession.note, { force: true });
if (isLockedSession(session) && "note" in session) {
this.get().openSession(session.note, { force: true });
}
}
});
@@ -1032,8 +1075,8 @@ class EditorStore extends BaseStore<EditorStore> {
};
newSession = () => {
const { activeTabId, activateSession } = this.get();
if (!activeTabId) {
const { activeTabId, activateSession, getActiveTab } = this.get();
if (!activeTabId || getActiveTab()?.pinned) {
this.addTab();
return;
}
@@ -1188,6 +1231,19 @@ class EditorStore extends BaseStore<EditorStore> {
return id;
};
pinTab = (tabId: string) => {
const { tabs: _tabs, activeTabId } = this.get();
const tabs = _tabs.slice();
const index = tabs.findIndex((t) => t.id === tabId);
if (index === -1) return;
tabs[index].pinned = !tabs[index].pinned;
tabs.sort((a, b) => (a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1));
useEditorStore.setState({
tabs
});
};
focusTab = (tabId: string | undefined, sessionId?: string) => {
if (!tabId) return;
@@ -1195,14 +1251,14 @@ class EditorStore extends BaseStore<EditorStore> {
if (history.includes(tabId)) history.splice(history.indexOf(tabId), 1);
history.push(tabId);
const tab = this.get().tabs.find((t) => t.id === tabId);
this.set({
activeTabId: tabId,
canGoBack: tabSessionHistory.canGoBack(tabId),
canGoForward: tabSessionHistory.canGoForward(tabId)
canGoBack: !tab?.pinned && tabSessionHistory.canGoBack(tabId),
canGoForward: !tab?.pinned && tabSessionHistory.canGoForward(tabId)
});
sessionId =
sessionId || this.get().tabs.find((t) => t.id === tabId)?.sessionId;
sessionId = sessionId || tab?.sessionId;
if (sessionId) this.rehydrateSession(sessionId);
};
}

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