Compare commits

...

101 Commits

Author SHA1 Message Date
Ammar Ahmed
5292f39b97 editor: fix class name 2025-03-06 14:16:36 +05:00
Ammar Ahmed
2447acca03 editor: fix image padding in readonly mode 2025-03-06 14:08:29 +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
luis-411
0517c6ce41 web: fix status-bar not visible on medium sized screens (#7493)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-02-08 19:31:37 +05:00
01zulfi
54d06328c1 web: add tab keyboard shortcuts (#7322)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-08 19:30:20 +05:00
Ammar Ahmed
c74677acd4 mobile: restore settings on logout (#7423) 2025-02-05 09:30:00 +05:00
01zulfi
7689e1a7c5 web: keep some configs in local storage on logout (#7436)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-02-05 09:29:36 +05:00
luzpaz
5ffae2a08b global: fix various typos (#7463)
Found via `codespell -q 3 -D ../dictionary.txt -S "*.patch,*.po" -L actualy,childs,modifer,ontext,ontop,ony,recieved,reciever,se,seperator,te`

Signed-off-by: Luz Paz <luzpaz@pm.me>
2025-02-05 09:28:06 +05:00
Ammar Ahmed
5fa80abdc8 mobile: update changelog 2025-02-04 19:25:16 +05:00
Ammar Ahmed
a30598fedf mobile: release v3.0.30 2025-02-04 19:23:49 +05:00
Ammar Ahmed
48ffecfb5f mobile: fix e2e tests 2025-02-04 19:23:11 +05:00
Ammar Ahmed
89fe712104 mobile: fix file size is 0 errors when downloading attachments 2025-02-04 17:36:46 +05:00
Ammar Ahmed
5dd298ee86 mobile: fix unlocking note with biometrics 2025-02-04 17:36:32 +05:00
Ammar Ahmed
275b285a8b mobile: release v3.0.29 2025-02-04 12:39:04 +05:00
Ammar Ahmed
96310a46e9 mobile: fix configure note activity not showing biometric unlock prompt 2025-02-04 12:33:05 +05:00
Ammar Ahmed
42f9bf1593 mobile: fix opening widget note on app launch 2025-02-04 12:33:05 +05:00
Ammar Ahmed
25aab6913a mobile: show progress when taking backup and allow to hide backup dialog 2025-02-04 12:32:51 +05:00
Ammar Ahmed
3bd7da68be mobile: fix crash on app launch with new tabs 2025-02-04 12:32:30 +05:00
Abdullah Atta
a218738eff web: improve unfocused window ui 2025-02-04 11:11:51 +05:00
Abdullah Atta
8bf283b3bc config: add *.patch files to .gitignore 2025-02-04 10:14:59 +05:00
luis-411
cb09430ff4 web: fix subnotebook title not updating on navigate (#7286)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-02-04 10:06:59 +05:00
luis-411
66d75492bb editor: fix task list stats 0/0 on app reload (#7327)
Signed-off-by: Luis Kriner <luis@kriner.info>
2025-02-04 10:04:16 +05:00
Abdullah Atta
d772d38dc7 web: fix collapsed pane expanding on app reload 2025-02-04 09:35:04 +05:00
123 changed files with 8812 additions and 4888 deletions

View File

@@ -37,7 +37,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -99,7 +99,7 @@ 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 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 .

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

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ nx-cloud.env
site
node_modules.backup
.nx
*.patch

View File

@@ -1,37 +0,0 @@
From af88ee05e1620afb65c76148d347cb7efc32bded Mon Sep 17 00:00:00 2001
From: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Date: Wed, 29 Jan 2025 12:22:32 +0500
Subject: [PATCH] editor: fix hover styling in toolbar color buttons
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
---
packages/editor/src/toolbar/popups/color-picker.tsx | 2 +-
packages/editor/src/toolbar/tools/colors.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/editor/src/toolbar/popups/color-picker.tsx b/packages/editor/src/toolbar/popups/color-picker.tsx
index 0abca19268..34209bd550 100644
--- a/packages/editor/src/toolbar/popups/color-picker.tsx
+++ b/packages/editor/src/toolbar/popups/color-picker.tsx
@@ -284,7 +284,7 @@ function PaletteButton(props: PaletteButtonProps) {
ml: [2, 2, 1],
bg,
":hover:not(:disabled):not(:active)": {
- bg: bg?.startsWith("#") ? colord(bg).darken(5).toRgbString() : "hover"
+ bg: bg?.startsWith("#") ? colord(bg).darken().toRgbString() : "hover"
},
...sx
}}
diff --git a/packages/editor/src/toolbar/tools/colors.tsx b/packages/editor/src/toolbar/tools/colors.tsx
index 08c6b41d8a..77ea660e75 100644
--- a/packages/editor/src/toolbar/tools/colors.tsx
+++ b/packages/editor/src/toolbar/tools/colors.tsx
@@ -137,7 +137,7 @@ export function ColorTool(props: ColorToolProps) {
mr: 0,
bg: activeColor || "transparent",
":hover:not(:disabled):not(:active)": {
- bg: tColor ? tColor.darken(5).toRgbString() : "transparent"
+ bg: tColor ? tColor.darken().toRgbString() : "transparent"
}
}}
onOpen={toggle}

Binary file not shown.

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.0.25",
"version": "3.0.28",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.0.25",
"version": "3.0.28",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.25",
"version": "3.0.28",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -39,7 +39,6 @@ import SettingsService from "./services/settings";
import { TipManager } from "./services/tip-manager";
import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import { IntentService } from "./services/intent";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
@@ -49,7 +48,6 @@ if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
IntentService.onLaunch();
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore

View File

@@ -115,7 +115,7 @@ export const FileSizeResult = {
Error: -1
};
export async function getUploadedFileSize(hash: string) {
export async function getUploadedFileSize(hash: string, retry = 0) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
@@ -123,8 +123,24 @@ export async function getUploadedFileSize(hash: string) {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
if (
!attachmentInfo.ok ||
attachmentInfo.headers?.get("content-length") === null
) {
if (retry < 3) {
DatabaseLogger.log(`Retrying file size check: ${hash}, ${retry}`);
return getUploadedFileSize(hash, retry + 1);
}
throw new Error(
`File size check failed: ${hash}, ${
attachmentInfo.status
}, ${attachmentInfo.headers?.get("content-length")}`
);
}
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length") || "0"
attachmentInfo.headers?.get("content-length") as string
);
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
} catch (e) {

View File

@@ -112,14 +112,19 @@ const AppLockedOverlay = () => {
if (
!biometricsAuthEnabled ||
!(await BiometricService.isBiometryAvailable())
)
) {
return;
}
biometricUnlockAwaitingUserInput.current = true;
if (Platform.OS === "android") {
const activityName = await NotesnookModule.getActivityName();
if (activityName !== "MainActivity") return;
if (
activityName !== "MainActivity" &&
activityName !== "NotePreviewConfigureActivity"
)
return;
}
useSettingStore.getState().setRequestBiometrics(true);
@@ -180,7 +185,6 @@ const AppLockedOverlay = () => {
DatabaseLogger.info("Locking app on entering foreground");
useUserStore.getState().lockApp(true);
}
if (
!(
biometricUnlockAwaitingUserInput.current ||

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

@@ -50,6 +50,7 @@ import { MMKV } from "../common/database/mmkv";
import { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
import ReminderSheet from "../components/sheets/reminder";
import { Walkthrough } from "../components/walkthroughs";
import {
resetTabStore,
@@ -58,7 +59,8 @@ import {
import {
clearAppState,
editorController,
editorState
editorState,
setAppState
} from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import BackupService from "../services/backup";
@@ -69,7 +71,6 @@ import {
eSubscribeEvent,
presentSheet
} from "../services/event-manager";
import { IntentService } from "../services/intent";
import {
clearMessage,
setEmailVerifyMessage,
@@ -98,7 +99,6 @@ import {
eLoginSessionExpired,
eOnLoadNote,
eOpenAnnouncementDialog,
eUnlockNote,
eUserLoggedIn,
refreshNotesPage
} from "../utils/events";
@@ -106,7 +106,6 @@ import { getGithubVersion } from "../utils/github-version";
import { tabBarRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import ReminderSheet from "../components/sheets/reminder";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -162,7 +161,6 @@ const onUserSessionExpired = async () => {
const onAppOpenedFromURL = async (event: { url: string }) => {
const url = event.url;
console.log("URL", url);
try {
if (url.startsWith("https://app.notesnook.com/account/verified")) {
await onUserEmailVerified();
@@ -751,13 +749,15 @@ export const useAppEvents = () => {
setTimeout(() => {
sub = AppState.addEventListener("change", onAppStateChanged);
if (
refValues.current.initialUrl?.startsWith(
"https://app.notesnook.com/account/verified"
)
refValues.current.initialUrl &&
!refValues.current.initialUrl?.includes("open_note")
) {
onUserEmailVerified();
onAppOpenedFromURL({
url: refValues.current.initialUrl!
});
}
}, 1000);
refValues.current.removeInternetStateListener = NetInfo.addEventListener(
onInternetStateChanged
);

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
@@ -44,10 +45,6 @@ import useNavigationStore from "../stores/use-navigation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import { strings } from "@notesnook/intl";
import { IntentService } from "../services/intent";
import ReminderSheet from "../components/sheets/reminder";
import { db } from "../common/database";
const NativeStack = createNativeStackNavigator();
const IntroStack = createNativeStackNavigator();
@@ -86,17 +83,6 @@ const _Tabs = () => {
React.useEffect(() => {
setTimeout(async () => {
useNavigationStore.getState().update(homepage);
const intent = IntentService.getLaunchIntent();
if (intent && intent["com.streetwriters.notesnook.OpenReminderId"]) {
const reminder = await db.reminders.reminder(
intent["com.streetwriters.notesnook.OpenReminderId"]
);
if (reminder) {
ReminderSheet.present(reminder);
}
} else if (intent["com.streetwriters.notesnook.NewReminder"]) {
ReminderSheet.present();
}
}, 1000);
}, [homepage]);

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

@@ -248,14 +248,13 @@ const useLockedNoteHandler = () => {
);
eSendEvent(eOnLoadNote, {
item: note
});
useTabStore.getState().updateTab(tabRef.current.id, {
session: {
locked: false
}
item: note,
refresh: true
});
} else {
if (tabRef.current && tabRef.current.session?.locked) {
editorController.current?.commands.focusPassInput();
}
}
} catch (e) {
console.error(e);
@@ -310,12 +309,8 @@ const useLockedNoteHandler = () => {
}
}
eSendEvent(eOnLoadNote, {
item: note
});
useTabStore.getState().updateTab(tabRef.current.id, {
session: {
locked: false
}
item: note,
refresh: true
});
} catch (e) {
ToastManager.show({

View File

@@ -90,8 +90,14 @@ class Commands {
const locked = useTabStore.getState().getTab(tabId)?.session?.locked;
if (Platform.OS === "android") {
//this.ref.current?.requestFocus();
setTimeout(async () => {
if (
locked &&
useTabStore.getState().biometryAvailable &&
useTabStore.getState().biometryEnrolled
)
return;
if (!this.ref) return;
textInput.current?.focus();
await this.sendCommand("focus", tabId, locked);
@@ -170,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);
};
@@ -185,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

@@ -613,7 +613,7 @@ export const useEditorEvents = (
}
case EditorEvents.tabFocused: {
eSendEvent(eEditorTabFocused, editorMessage.tabId);
if (!editor.state.current.initialLoadCalled) break;
if (editorMessage.noteId) {
if (!useSettingStore.getState().isAppLoading) {
const note = await db.notes.note(editorMessage.noteId);

View File

@@ -77,6 +77,7 @@ import {
isEditorLoaded,
post
} from "./utils";
import { Linking } from "react-native";
const loadNoteMutex = new Mutex();
@@ -126,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];
}
@@ -500,6 +494,7 @@ export const useEditor = (
blockId?: string;
session?: TabSessionItem;
newTab?: boolean;
refresh?: boolean;
}) => {
loadNoteMutex.runExclusive(async () => {
if (!event) return;
@@ -520,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;
@@ -545,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
@@ -573,11 +573,10 @@ export const useEditor = (
? event.tabId
: useTabStore.getState().currentTab;
console.log(tabId === useTabStore.getState().currentTab);
// Check if tab needs to be refreshed.
if (!event.newTab) {
if (
!event.refresh &&
tabId &&
event.item.id === useTabStore.getState().getNoteIdForTab(tabId) &&
!localTabState.current?.needsRefresh(
@@ -623,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.
@@ -1039,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);
});
@@ -1055,32 +1071,34 @@ export const useEditor = (
state.current.ready = true;
}
const appState = getAppState();
if (appState?.noteId) {
const note = await db.notes?.note(appState.noteId);
if (note) {
loadNote({
item: note
});
}
} else {
const noteId = useTabStore.getState().getCurrentNoteId();
if (!noteId) {
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false;
}
} else if (state.current?.initialLoadCalled) {
const note = currentNotes.current[noteId];
if (!state.current?.initialLoadCalled) {
const url = await Linking.getInitialURL();
let noteId = url && new URL(url).searchParams.get("id");
if (noteId) {
const note = await db.notes?.note(noteId);
if (note) {
loadNote({
item: note
});
}
tabBarRef.current?.goToPage(1);
} else {
noteId = useTabStore.getState().getCurrentNoteId() || null;
if (!noteId) {
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false;
}
} else {
const note = await db.notes.note(noteId);
if (note) {
loadNote({
item: note
});
}
}
}
if (!state.current?.initialLoadCalled) {
state.current.initialLoadCalled = true;
}
state.current.initialLoadCalled = true;
overlay(false);
}
}, [

View File

@@ -97,10 +97,12 @@ 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);
}
@@ -108,13 +110,16 @@ class TabSessionStorage {
const currentSession = TabSessionStorage.get(id);
const newSession = {
...currentSession,
...session
...session,
id: currentSession?.id || session.id || id
};
TabSessionStorage.set(id, newSession as TabSessionItem);
return newSession;
}
static remove(id: string) {
if (!id) return;
TabSessionStorageKV.removeItem(id);
}
}
@@ -164,7 +169,7 @@ const history = new TabHistory();
export type TabStore = {
tabs: TabItem[];
currentTab?: string;
currentTab: string;
updateTab: (id: string, options: Omit<Partial<TabItem>, "id">) => void;
focusPreviewTab: (
noteId: string,
@@ -264,6 +269,7 @@ export const useTabStore = create<TabStore>(
TabSessionStorage.set(sessionId, session as TabSessionItem);
} else {
session = {
id: sessionId,
...TabSessionStorage.get(oldSessionId),
...options
};
@@ -289,6 +295,7 @@ export const useTabStore = create<TabStore>(
const tabs = [...get().tabs];
const sessionId =
options.session?.id || (tabs[index].session?.id as string);
const updatedSession = !options.session
? tabs[index].session
: TabSessionStorage.update(sessionId, options.session);
@@ -313,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();
@@ -332,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();
@@ -354,8 +365,8 @@ export const useTabStore = create<TabStore>(
if (note) {
const isLocked = await db.vaults.itemExists(note);
session.noteLocked = isLocked;
session.locked = isLocked;
session.noteLocked = isLocked && !session?.noteLocked;
session.readonly = note.readonly;
} else if (session.noteId) {
@@ -395,7 +406,7 @@ export const useTabStore = create<TabStore>(
set({
tabs: [{ id: id }]
});
get().newTabSession(id, {});
get().newTabSession(id);
get().focusTab(id);
} else {
set({
@@ -474,7 +485,7 @@ export const useTabStore = create<TabStore>(
}
}),
{
name: "tabs-storage-v3",
name: "tabs-storage-v5",
getStorage: () => MMKV as unknown as StateStorage,
onRehydrateStorage: () => {
return (state) => {

View File

@@ -23,7 +23,7 @@ import {
StackActions,
useNavigation
} from "@react-navigation/native";
import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, TextInput, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ToggleSwitch from "toggle-switch-react-native";
@@ -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(
@@ -103,6 +106,11 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
}
};
useEffect(() => {
setIsHidden(item.hidden && item.hidden(item.property || current));
setIsDisabled(item.disabled && item.disabled(item.property || current));
}, [current, item, itemProperty]);
return isHidden ? null : (
<Pressable
disabled={item.type === "component" || isDisabled}

View File

@@ -451,7 +451,7 @@ export const settingsGroups: SettingSection[] = [
"local",
"partial"
);
if (result.error) throw result.error as Error;
if (result?.error) throw result.error as Error;
} catch (e) {
DatabaseLogger.error(e);
const error = e;

View File

@@ -30,12 +30,15 @@ import { DatabaseLogger, db } from "../common/database";
import filesystem, { FileStorage } from "../common/filesystem";
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
import { presentDialog } from "../components/dialog/functions";
import { endProgress, updateProgress } from "../components/dialogs/progress";
import {
endProgress,
startProgress,
updateProgress
} from "../components/dialogs/progress";
import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
import { e } from "@lingui/react/dist/shared/react.b2b749a9";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -156,6 +159,7 @@ async function updateNextBackupTime(type: "full" | "partial") {
[type === "full" ? "lastFullBackupDate" : "lastBackupDate"]: Date.now()
});
}
let backupRunning = false;
/**
* @param {boolean=} progress
* @param {string=} context
@@ -165,7 +169,23 @@ async function run(
progress = false,
context?: string,
backupType: "full" | "partial" = "partial"
) {
): Promise<{
path?: string;
error?: Error;
report?: boolean;
}> {
if (backupRunning) {
if (progress) {
startProgress({
title: strings.backingUpData(backupType),
paragraph: strings.backupDataDesc(),
progress: "Backup in progress...",
canHideProgress: true
});
}
return {};
}
backupRunning = true;
const androidBackupDirectory = (await checkBackupDirExists(
false,
context
@@ -193,10 +213,11 @@ async function run(
);
if (progress) {
presentSheet({
startProgress({
title: strings.backingUpData(backupType),
paragraph: strings.backupDataDesc(),
progress: true
progress: "Preparing backup...",
canHideProgress: true
});
}
@@ -274,14 +295,7 @@ async function run(
path = zipOutputFile;
}
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
}
cleanupAfterBackup(zipSourceFolder, zipOutputFile);
updateNextBackupTime(backupType || "partial");
@@ -292,6 +306,8 @@ async function run(
const canShowCompletionStatus =
progress && SettingsService.get().showBackupCompleteSheet;
backupRunning = false;
if (context) {
return {
path: path
@@ -302,8 +318,6 @@ async function run(
if (canShowCompletionStatus) {
presentBackupCompleteSheet(path);
} else {
eSendEvent(eCloseSheet);
}
ToastManager.show({
@@ -311,11 +325,11 @@ async function run(
type: "success",
context: "global"
});
return {
path: path
};
} catch (e) {
backupRunning = false;
ToastManager.error(e as Error, strings.backupFailed(), context || "global");
if (
@@ -326,27 +340,30 @@ async function run(
return run(progress, context, backupType);
}
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
}
cleanupAfterBackup(zipSourceFolder, zipOutputFile);
DatabaseLogger.error(e);
await sleep(300);
if (progress) {
endProgress();
}
return {
error: e,
error: e as Error,
report: true
};
}
}
function cleanupAfterBackup(zipSourceFolder: string, zipOutputFile: string) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(() => {
/* empty */
});
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(() => {
/* empty */
});
}
}
async function checkBackupRequired(
type: "daily" | "off" | "useroff" | "weekly" | "monthly" | "never",
lastBackupDateType: "lastBackupDate" | "lastFullBackupDate" = "lastBackupDate"

View File

@@ -45,7 +45,17 @@ function resetSettings() {
...defaultSettings,
introCompleted: true,
serverUrls: get().serverUrls,
backupDirectoryAndroid: undefined
backupDirectoryAndroid: get().backupDirectoryAndroid,
darkTheme: get().darkTheme,
lighTheme: get().lighTheme,
useSystemTheme: get().useSystemTheme,
colorScheme: get().colorScheme,
defaultSnoozeTime: get().defaultSnoozeTime,
defaultFontFamily: get().defaultFontFamily,
defaultFontSize: get().defaultFontSize,
privacyScreen: get().privacyScreen,
corsProxy: get().corsProxy,
showBackupCompleteSheet: get().showBackupCompleteSheet
};
MMKV.setString("appSettings", JSON.stringify(settings));

View File

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

View File

@@ -1,5 +1,3 @@
- New and improved editor tabs with history
- Fixed homescreen widget bugs
- 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 = 2125;
CURRENT_PROJECT_VERSION = 2129;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1137,7 +1137,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.28;
MARKETING_VERSION = 3.0.32;
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 = 2125;
CURRENT_PROJECT_VERSION = 2129;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1242,7 +1242,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.28;
MARKETING_VERSION = 3.0.32;
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 = 2125;
CURRENT_PROJECT_VERSION = 2129;
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.28;
MARKETING_VERSION = 3.0.32;
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 = 2125;
CURRENT_PROJECT_VERSION = 2129;
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.28;
MARKETING_VERSION = 3.0.32;
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 = 2125;
CURRENT_PROJECT_VERSION = 2129;
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.28;
MARKETING_VERSION = 3.0.32;
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 = 2125;
CURRENT_PROJECT_VERSION = 2129;
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.28;
MARKETING_VERSION = 3.0.32;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -1820,7 +1820,7 @@ SPEC CHECKSUMS:
Base64: cecfb41a004124895a7bcee567a89bae5a89d49b
BEMCheckBox: 5ba6e37ade3d3657b36caecc35c8b75c6c2b1a4e
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
callstack-repack: 2db7cbeced7cd5da360d01965e0b983f6dacfd54
callstack-repack: 721b034d0662f47388719bf26c00f838da6880d5
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
FBLazyVector: ac12dc084d1c8ec4cc4d7b3cf1b0ebda6dab85af
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
@@ -1837,103 +1837,103 @@ SPEC CHECKSUMS:
RCTTypeSafety: f5ecbc86c5c5fa163c05acb7a1c5012e15b5f994
React: fc9fa7258eff606f44d58c5b233a82dc9cf09018
React-callinvoker: e3fab14d69607fb7e8e3a57e5a415aed863d3599
React-Codegen: 49efb5917c904b5b01ef708311a2f1773f4fe73e
React-Core: 3a5fd9e781cecf87803e5b091496a606a3df774a
React-CoreModules: cbf4707dafab8f9f826ac0c63a07d0bf5d01e256
React-cxxreact: 7b188556271e3c7fdf22a04819f6a6225045b9dd
React-Codegen: ff6c425afab9288a1573337111741d30363bf962
React-Core: c3f589f104983dec3c3eeec5e70d61aa811bc236
React-CoreModules: 864932ddae3ead5af5bfb05f9bbc2cedcb958b39
React-cxxreact: bd9146108c44e6dbb99bba4568ce7af0304a2419
React-debug: d30893c49ae1bce4037ea5cd8bb2511d2a38d057
React-Fabric: 826729dd2304fda9b89ff0a579f60ba2a470bc26
React-FabricImage: 2ad1fb8ffa5778eda9ed204a7b3cdd70bc333ce7
React-Fabric: a171830e52baf8ec2b175c6a3791e01bbb92f1fb
React-FabricImage: ad154af0067f4b5dc5a41f607e48ee343641e903
React-featureflags: 4ae83e72d9a92452793601ac9ac7d2280e486089
React-graphics: 61a026e1c1e7e20d20ac9fec6f6de631732b233d
React-hermes: a7054fbcbda3957e3c5eaad06ef9bf79998d535a
React-ImageManager: 2bbd6eb2e696bc680f76f84563e4b87d241614e1
React-jserrorhandler: 56fa04d49bfbe54ddfece7916673a73ebfea286b
React-jsi: f3ce1dd2e950b6ad12b65ea3ef89168f1b94c584
React-jsiexecutor: b4df3a27973d82f9abf3c4bd0f88e042cda25f16
React-jsinspector: 97ea746c023687de7313ee289817d6991d596c7d
React-jsitracing: 3b6060bbf5317663667e1dd93560c7943ab86ccc
React-logger: 257858bd55f3a4e1bc0cf07ddc8fb9faba6f8c7c
React-Mapbuffer: 6c1cacdbf40b531f549eba249e531a7d0bfd8e7f
react-native-actions-shortcuts: 5d9cf0c9c308333dfcc1e05c3f9afa8c428e2533
react-native-background-actions: 2c251c986f23347f9c1722f05fd296938f60edb1
react-native-begin-background-task: 3b889e07458afc5822a7277cf9cbc7cd077e39ee
react-native-blob-util: 2d36383bb52c15c5451be81cb7ddf22bc34a12a6
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
react-native-date-picker: 93e43b3084cea595b4d68b1405d6d99849663bd6
react-native-document-picker: ec07866a30707f23660c0f3ae591d669d3e89096
react-native-fingerprint-scanner: be63e626b31fb951780a5fac5328b065a61a3d6e
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-gzip: c5e87ee9e359f02350e3a2ee52eb35eddc398868
react-native-html-to-pdf-lite: 21bfb169bf4cbcd7bec9f736975ee1b3f5292d4a
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4
react-native-keep-awake: caee3ff89eaa21dfe29010f0d143566874a04441
react-native-mmkv-storage: 3892853c11ab8a0c3a3f9ef3b0657aa9a5f616c9
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
react-native-notification-sounds: da78c828fe1bcbb92d8b505d5261890ed315ff39
react-native-orientation: f1caf84d65f1a4fd4511a18f2b924e634ad7a628
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
react-native-quick-sqlite: 18e1367c34faac90e37f6eb3e78c196e9b674b5d
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
react-native-screenguard: 8b36a3df84c76cd2b82c477f71c26fa1c8cc14a0
react-native-share-extension: 01b25fc15faf2510cc2529e98ca444f64e46238d
react-native-sodium: 4cb76086943a7f60c42b40ebca866695b360a196
react-native-theme-switch-animation: d3eb50365a3829ce5572628888fa514752703f61
react-native-webview: 553abd09f58e340fdc7746c9e2ae096839e99911
React-graphics: ed7d57965140168de86835946e8f1210c72c65dc
React-hermes: 177b1efdf3b8f10f4ca12b624b83fb4d4ccb2884
React-ImageManager: 3a50d0ee0bf81b1a6f23a0c5b30388293bcd6004
React-jserrorhandler: dcd62f5ca1c724c19637595ef7f45b78018e758f
React-jsi: 0abe1b0881b67caf8d8df6a57778dd0d3bb9d9a5
React-jsiexecutor: f6ca8c04f19f6a3acaa9610f7fb728f39d6e3248
React-jsinspector: db98771eae84e6f86f0ca5d9dcc572baadbfefc0
React-jsitracing: f8367edacc50bb3f9f056a5aeafb8cee5849fafb
React-logger: 780b9ee9cec7d44eabc4093de90107c379078cb6
React-Mapbuffer: f544f00b98dbdd8cbae96dd2bdb8b47f719976e0
react-native-actions-shortcuts: 65fb9cb4a58c71af7382663c1822dda061dbf1b0
react-native-background-actions: 40e09df6ea8c7d2753f6c1d75f6f1eee4a9bc35f
react-native-begin-background-task: 2191f2a84b0328932a3d44db4361e666fee781bb
react-native-blob-util: 4be49d669870f01645b63d33342718e95d0d48b1
react-native-config: 136f9755ccc991cc6438053a44363259ad4c7813
react-native-date-picker: bbf77c50dce9da2beb11e2f67f3300195ba27d81
react-native-document-picker: f4ccd6f6f369da1409e609788181eac66cc1a2d8
react-native-fingerprint-scanner: 562585260768cad51ae48e4b505d28c8731aecfa
react-native-get-random-values: 419569b6ed3d15bfb9b6781b2f2e058f8e8d2698
react-native-gzip: 8d602277c2564591f04dd1cec4043acc8350dcc3
react-native-html-to-pdf-lite: d606d3b72d9696c7a6d1e5f5b7536783faacef14
react-native-image-resizer: ff3c9ab0ce235c4c00f49d0a373043cb4b8781be
react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400
react-native-keep-awake: bda639e941ce138506c18f1b999dcbf16d4c77b8
react-native-mmkv-storage: 8dddc9d16248bc9a4d9566ac5c97c318692d299c
react-native-netinfo: 66c9ac0d0ae92a57a9ed99ab8a75429740700c73
react-native-notification-sounds: ce106d58df0dd384bccbd2e84fb53accab7cc068
react-native-orientation: db2655b95b4123f17e75dcb04ee8b8b1294ce7ee
react-native-pdf: da10473ae54555c373c62af671da8fe0eef846e2
react-native-quick-sqlite: 80b5b3405bfc92e97b6041d2c2a446800bea6ad7
react-native-safe-area-context: b72c4611af2e86d80a59ac76279043d8f75f454c
react-native-screenguard: 82437eeb0086a90b5e5d7e54130bb04fb406373e
react-native-share-extension: 7528322fe621b683e6b48ab3418207ed45a70983
react-native-sodium: ed020d5f50ab1886f7bf23e32732d0d10e9373ac
react-native-theme-switch-animation: 64f2688c2e3489ababc7b35f436fd3b5ed7c0a98
react-native-webview: a71525b1ab760230fbf37303d8371fbe72051c7d
React-nativeconfig: ba9a2e54e2f0882cf7882698825052793ed4c851
React-NativeModulesApple: 8d11ff8955181540585c944cf48e9e7236952697
React-NativeModulesApple: 84aaad2b0e546d7b839837ca537f6e72804a4cad
React-perflogger: ed4e0c65781521e0424f2e5e40b40cc7879d737e
React-RCTActionSheet: 49d53ff03bb5688ca4606c55859053a0cd129ea5
React-RCTAnimation: 07b4923885c52c397c4ec103924bf6e53b42c73e
React-RCTAppDelegate: 316e295076734baf9bdf1bfac7d92ab647aed930
React-RCTBlob: 85c57b0d5e667ff8a472163ba3af0628171a64bb
React-RCTFabric: 97c1465ded4dc92841f5376a39e43e1b2c455f40
React-RCTImage: b965c85bec820e2a9c154b1fb00a2ecdd59a9c92
React-RCTLinking: 75f04a5f27c26c4e73a39c50df470820d219df79
React-RCTNetwork: c1a9143f4d5778efc92da40d83969d03912ccc24
React-RCTSettings: c6800f91c0ecd48868cd5db754b0b0a7f5ffe039
React-RCTText: b923e24f9b7250bc4f7ab154c4168ad9f8d8fc9d
React-RCTVibration: 08c4f0c917c435b3619386c25a94ee5d64c250f0
React-rendererdebug: 3cda04217d9df67b94397ee0ead8ef3d8b7e427b
React-RCTAnimation: 3075449f26cb98a52bcbf51cccd0c7954e2a71db
React-RCTAppDelegate: 9a419c4dda9dd039ad851411546dd297b930c454
React-RCTBlob: e81ab773a8fc1e9dceed953e889f936a7b7b3aa6
React-RCTFabric: 47a87a3e3fa751674f7e64d0bcd58976b8c57db9
React-RCTImage: d570531201c6dce7b5b63878fa8ecec0cc311c4c
React-RCTLinking: af888972b925d2811633d47853c479e88c35eb4d
React-RCTNetwork: 5728a06ff595003eca628f43f112a804f4a9a970
React-RCTSettings: ba3665b0569714a8aaceee5c7d23b943e333fa55
React-RCTText: b733fa984f0336b072e47512898ba91214f66ddb
React-RCTVibration: 0cbcbbd8781b6f6123671bae9ee5dd20d621af6c
React-rendererdebug: 9fc8f7d0bd19f2a3fe3791982af550b5e1535ff7
React-rncore: 4013508a2f3fcf46c961919bbbd4bfdda198977e
React-RuntimeApple: 447844a2bdb0a03ffd24e5b4a4b96cfc50325b88
React-RuntimeCore: 9b5bffdaccee9b707b1c2694c9044e13ff0bb087
React-RuntimeApple: a852a6e06ab20711658873f39cb10b0033bea19d
React-RuntimeCore: 12e5e176c0cb09926f3e6f37403a84d2e0f203a7
React-runtimeexecutor: 0e688aefc14c6bc8601f4968d8d01c3fb6446844
React-RuntimeHermes: 4d6ef6bb0f2b0b40d59143317f6b99c82764c959
React-runtimescheduler: cfbe85c3510c541ec6dc815c7729b41304b67961
React-utils: f242eb7e7889419d979ca0e1c02ccc0ea6e43b29
ReactCommon: f7da14a8827b72704169a48c929bcde802698361
RNBackgroundFetch: 35c7183f6eeff69396d42c4b206e1ae2a21e9327
RNBootSplash: de2c568373a9c79a66e9918b8929eb6c9a35246f
RNCCheckbox: 43bcc6493611468af0e19f19f029dab3da8561c4
RNCClipboard: 3f0451a8100393908bea5c5c5b16f96d45f30bfc
RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
RNDeviceInfo: aad3c663b25752a52bf8fce93f2354001dd185aa
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
RNFlashList: e9b57a5553639f9b528cc50ab53f25831722ed62
RNGestureHandler: 4fd316f1bdb8d7f6a54efc58da5923cb83014b19
RNIap: 17583f958d35788a87948e94072ff6edd3580497
RNImageCropPicker: d9616a0cb9b72e8551ff94a7a5021fbd29050aa5
RNKeychain: 840f8e6f13be0576202aefcdffd26a4f54bfe7b5
RNNotifee: 1448fd16c2a6f3e8abbc219801bf5be2435183e5
RNPrivacySnapshot: 8eaf571478a353f2e5184f5c803164f22428b023
RNReanimated: 7212582b487bceed64b2dbc6c15777b97922be21
RNScreens: d2a12528d1abe4922e7ee5c9a00dd40c827c2ab5
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
RNShare: 22717e910836a66cb7255b3b8c4ab06cbe346e27
RNSVG: d7d7bc8229af3842c9cfc3a723c815a52cdd1105
RNTooltips: 5424d4bf0b3d441104127943b1115cc7f0616b1f
RNZipArchive: 9d90367b1573903b5561a8c0b911e5cc12626e74
React-RuntimeHermes: 80c03a5215520c9733764ba11cbe535053c9746d
React-runtimescheduler: 2cbd0f3625b30bba08e8768776107f6f0203159b
React-utils: 9fa4e5d0b5e6c6c85c958f19d6ef854337886417
ReactCommon: 9f285823dbe955099978d9bff65a7653ca029256
RNBackgroundFetch: 0c49d5892a0e2f9c139e24ba498985cf0bf070c6
RNBootSplash: 3bc12ecb62f69c5af6e6ddf0e6bef88f572ccdb5
RNCCheckbox: df88c1c78a17869da84bb031c9d8185d417398a6
RNCClipboard: efe1b27ad1ea378c60c8c8aabfd130961bbeb474
RNCMaskedView: 4b5b12efdee55966f1d68f32d4b3401c2acf05c2
RNDateTimePicker: 818460dc31b0dc5ec58289003e27dd8d022fb79c
RNDeviceInfo: d3e91ffb33ee97a7982108476edb68cb3672efa6
RNExitApp: 890cce29b4e01372c84b3b775f9a63f90d77de19
RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251
RNFlashList: fe872a2d1b4c859c64a7d7421628a1308ebb9e4f
RNGestureHandler: 61302deb50c92cba160f5a11f00f69da48116724
RNIap: aa38c2cc8a2a75ba8112d161ca084a0b611955df
RNImageCropPicker: 30d770b383d84e1067d82ea7b5ed4fff851bbbb2
RNKeychain: ffd0513e676445c637410b47249460cbf56bc9cb
RNNotifee: b98fb71f678f6644fa48ec6d515ec6fcfc5214ba
RNPrivacySnapshot: ccad3a548338c2f526bb7b1789af3fb0618b7d1d
RNReanimated: bee9626cf4920f2910bf7c4f8a3dc569161a9475
RNScreens: d0db5e9f28afd284a5193474d6be949f4cba888e
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 5d7762ded21e1fff97dda6d41e963e22dce2d025
RNSVG: d7cb8bd34550cf4c4fc7edd7ac627905e2b71f3f
RNTooltips: 50ff94bd2bff84ef74279c9532d008f5fdcddb95
RNZipArchive: 71befbdc20e7fbd4b64552a0b324ad0e8d0efefb
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SexyTooltip: 5c9b4dec52bfb317938cb0488efd9da3717bb6fd
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
toolbar-android: 2a73856e98b750d7e71ce4644d3f41cc98211719
toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018
Yoga: 950bbfd7e6f04790fdb51149ed51df41f329fcc8
PODFILE CHECKSUM: aa07aca16c1d92394d8ce12f2c5fcaa8a3ae4997

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.27",
"version": "3.0.31",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.0.27",
"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.28",
"version": "3.0.32",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -54,4 +54,4 @@
"react": "18.2.0",
"react-native": "0.74.5"
}
}
}

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);

View File

@@ -26,7 +26,7 @@ type ErrorsListProps = {
export function ErrorsList(props: ErrorsListProps) {
return (
<Accordion
title={`${props.errors.length} errors occured`}
title={`${props.errors.length} errors occurred`}
sx={{ bg: "errorBg", borderRadius: "default", mt: 2 }}
color="#E53935"
>

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) {
@@ -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

@@ -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

@@ -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

@@ -36,7 +36,7 @@ The flow will be as follows:
5. Use the database password to init the db & keep the user's encryption key in memory. (we have 2 choices here: either keep the encryption key in memory or keep the pin in memory)
6. If not enabled, we will ask the keystore to give us the database password + user's encryption key
We should store both the database password and the user's encryption key in a keychain. If user sets a pin, we can just double encrypt these values. This will keep things simple and seemless.
We should store both the database password and the user's encryption key in a keychain. If user sets a pin, we can just double encrypt these values. This will keep things simple and seamless.
Q: How to find out if app lock is enabled?

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.0.25",
"version": "3.0.28",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.0.25",
"version": "3.0.28",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -497,7 +497,7 @@
},
"../desktop": {
"name": "@notesnook/desktop",
"version": "3.0.24",
"version": "3.0.25",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

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

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();
@@ -64,8 +65,8 @@ function App() {
{isFocused ? null : (
<Global
styles={`
body {
filter: brightness(0.95) grayscale(1);
.nav-pane {
opacity: 0.7;
}
.titlebar {
background: var(--background-secondary) !important;
@@ -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>
</>
);
@@ -150,23 +154,9 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
useEffect(() => {
if (isTablet) navPane.current?.collapse(0);
else navPane.current?.expand(0);
else if (navPane.current?.isCollapsed(0)) navPane.current?.expand(0);
}, [isTablet]);
// useEffect(() => {
// if (show) middlePane.current?.expand();
// else middlePane.current?.collapse();
// }, [show]);
// useEffect(() => {
// if (isFocusMode) {
// const middlePaneSize = middlePane.current?.getSize() || 20;
// navPane.current?.collapse();
// // the middle pane has to be resized because collapsing the nav
// // pane increases the middle pane's size every time.
// middlePane.current?.resize(middlePaneSize);
// } else navPane.current?.expand();
// }, [isFocusMode]);
return (
<>
<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

@@ -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,7 @@ 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";
function isInEditor(e: KeyboardEvent) {
return (
@@ -64,10 +65,11 @@ const KEYMAP = [
"command+option+right",
"ctrl+alt+right",
"command+option+shift+right",
"ctrl+alt+shift+right"
"ctrl+alt+shift+right",
"ctrl+tab",
"command+tab"
],
description: "Go to next tab",
global: false,
action: () => useEditorStore.getState().focusNextTab()
},
{
@@ -75,12 +77,33 @@ const KEYMAP = [
"command+option+left",
"ctrl+alt+left",
"command+option+shift+left",
"ctrl+alt+shift+left"
"ctrl+alt+shift+left",
"ctrl+shift+tab",
"command+shift+tab"
],
description: "Go to next tab",
global: false,
description: "Go to previous tab",
action: () => useEditorStore.getState().focusPreviousTab()
},
{
keys: ["ctrl+t", "command+t"],
description: "Create a new tab",
action: () => useEditorStore.getState().addTab()
},
{
keys: ["ctrl+n", "command+n"],
description: "Create a new note",
action: () => useEditorStore.getState().newSession()
},
{
keys: ["ctrl+w", "command+w"],
description: "Close active tab",
action: () => useEditorStore.getState().closeActiveTab()
},
{
keys: ["ctrl+shift+w", "command+shift+w"],
description: "Close all tabs",
action: () => useEditorStore.getState().closeAllTabs()
},
{
keys: ["command+f", "ctrl+f"],
description: "Search all notes",
@@ -101,7 +124,7 @@ const KEYMAP = [
useSearchStore.setState({ isSearching: true, searchType: "notes" });
}
}
},
// {
// keys: ["alt+n"],
// description: "Go to Notes",
@@ -165,6 +188,17 @@ 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(() => {});
}
}
];
export function registerKeyMap() {

View File

@@ -165,7 +165,7 @@ export class Base {
}
/**
* Handle asynchronous operation. This implementation will be overriden on
* Handle asynchronous operation. This implementation will be overridden on
* registration by an Asyncify build.
* @param {function(): Promise<number>} f
* @returns {number}

View File

@@ -269,7 +269,7 @@ export interface SQLiteModule {
* the corresponding Javascript wrapper will throw an exception with a
* `code` property on an error.
*
* Note that a few functions return a Promise in order to accomodate
* Note that a few functions return a Promise in order to accommodate
* either a synchronous or asynchronous SQLite build, generally those
* involved with opening/closing a database or executing a statement.
*
@@ -493,7 +493,7 @@ export interface SQLiteAPI {
column(stmt: number, i: number): SQLiteCompatibleType;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
*
* The contents of the returned buffer may be invalid after the
* next SQLite call. Make a copy of the data (e.g. with `.slice()`)
@@ -523,7 +523,7 @@ export interface SQLiteAPI {
column_count(stmt: number): number;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
* @see https://www.sqlite.org/c3ref/column_blob.html
* @param stmt prepared statement pointer
* @param i column index
@@ -532,7 +532,7 @@ export interface SQLiteAPI {
column_double(stmt: number, i: number): number;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
* @see https://www.sqlite.org/c3ref/column_blob.html
* @param stmt prepared statement pointer
* @param i column index
@@ -541,7 +541,7 @@ export interface SQLiteAPI {
column_int(stmt: number, i: number): number;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
* @see https://www.sqlite.org/c3ref/column_blob.html
* @param stmt prepared statement pointer
* @param i column index
@@ -569,7 +569,7 @@ export interface SQLiteAPI {
column_names(stmt: number): Array<string>;
/**
* Extract a column value from a row after a prepared statment {@link step}
* Extract a column value from a row after a prepared statement {@link step}
* @see https://www.sqlite.org/c3ref/column_blob.html
* @param stmt prepared statement pointer
* @param i column index

View File

@@ -113,6 +113,13 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
...props.sx
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
props.onClose?.();
}
}}
>
{props.showCloseButton && (
<Close
@@ -225,6 +232,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 +249,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

@@ -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,
@@ -59,6 +61,8 @@ function Header(props: HeaderProps) {
},
[]);
if (!noteId) return null;
return (
<>
<Flex
@@ -72,7 +76,9 @@ function Header(props: HeaderProps) {
text={tag.title}
icon={TagIcon}
onClick={() => navigate(`/tags/${tag.id}`)}
onDismiss={readonly ? undefined : () => setTag(id, tags, tag.title)}
onDismiss={
readonly ? undefined : () => setTag(noteId, tags, tag.title)
}
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
/>
))}
@@ -90,7 +96,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 +107,18 @@ 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) => setTag(noteId, tags, value)}
onRemove={() => {
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, {

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";
@@ -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);

View File

@@ -307,8 +307,8 @@ function TipTap(props: TipTapProps) {
}
};
}, [
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,45 @@ function TitleBox(props: TitleBoxProps) {
placeholder={strings.noteTitle()}
readOnly={readonly}
dir="auto"
wrap="soft"
rows={1}
sx={{
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 +151,11 @@ export default React.memo(TitleBox, (prevProps, nextProps) => {
return prevProps.readonly === nextProps.readonly;
});
function resizeTextarea(input: HTMLTextAreaElement) {
input.style.height = "auto";
input.style.height = input.scrollHeight + "px";
}
function onTitleChange(
noteId: string,
title: string,
@@ -178,16 +167,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,11 +54,13 @@ type PaneOptions = {
nextSize?: number;
initialSize: number;
collapsed: boolean;
expandedSize?: number;
};
export type SplitPaneImperativeHandle = {
collapse: (index: number) => void;
expand: (index: number) => void;
isCollapsed: (index: number) => boolean;
};
export const SplitPane = React.forwardRef<
SplitPaneImperativeHandle,
@@ -96,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(
() =>
@@ -150,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;
}) || [];
@@ -165,7 +179,7 @@ export const SplitPane = React.forwardRef<
if (wrapSize.current === 0) return;
updatePaneLimitSizes(children);
setSizes(paneSizes.current, wrapSize.current, false);
setSizes(paneSizes.current, wrapSize.current, true);
}, [children, childrenLength]);
const setSizes = useCallback(
@@ -256,16 +270,23 @@ 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) => {
return paneSizes.current[index].collapsed;
}
};
},
@@ -304,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);
@@ -62,9 +63,9 @@ function StatusBar() {
borderTop: "1px solid",
borderTopColor: "separator",
justifyContent: "space-between",
display: ["none", "none", "flex"],
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

@@ -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,531 @@
/*
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
? 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];
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={(e) => {
e.stopPropagation();
removeRecentCommand(command.id);
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,
{
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,
{
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()
}
])
];
}

View File

@@ -0,0 +1,513 @@
/*
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
} 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 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);
}
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 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"
});
}
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,28 @@ 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: "Command palette",
subtitle: (
<>
You can now quickly execute commands like navigation, actions on
notes, notebooks etc. just by pressing{" "}
<Code text="Ctrl/Cmd+K" />. This will open a command palette
where you can fuzzy search for commands and execute them.
</>
)
},
{
title: "Quick open",
subtitle: (
<>
You can also open the command palette in quick open mode by
pressing <Code text="Ctrl/Cmd+P" />. This will open a command
palette where you can fuzzy search for your notes, notebooks,
tags, etc. and quickly open them by pressing{" "}
<Code text="Enter" />. Try it out and let us know what you
think!
</>
)
}
],
cta: {
@@ -128,12 +147,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 +155,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

@@ -187,7 +187,7 @@ const SettingsGroups = [
// 2. Settings will be synced so their state must be serializable
// 3. Settings will be grouped
// - Where group header is customizable
// 4. Sections/groups must be able to accomodate tips & tutorials for future.
// 4. Sections/groups must be able to accommodate tips & tutorials for future.
// 5. Settings will be stateful but independent such that any one setting
// can appear independent of others (e.g. as a search result)

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

@@ -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);
@@ -899,15 +920,14 @@ class EditorStore extends BaseStore<EditorStore> {
this.setSaveState(id, 0);
try {
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 +935,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,7 +954,7 @@ 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") {
@@ -942,16 +962,16 @@ class EditorStore extends BaseStore<EditorStore> {
if (context) {
const { type } = context;
if (type === "notebook")
await db.notes.addToNotebook(context.id, noteId);
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" }
);
} else {
const defaultNotebook = db.settings.getDefaultNotebook();
if (defaultNotebook)
await db.notes.addToNotebook(defaultNotebook, noteId);
await db.notes.addToNotebook(defaultNotebook, note.id);
}
}
@@ -991,6 +1011,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
);
@@ -1032,8 +1065,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;
}
@@ -1050,6 +1083,17 @@ class EditorStore extends BaseStore<EditorStore> {
});
};
closeActiveTab = () => {
const { activeTabId } = this.get();
if (!activeTabId) return;
this.closeTabs(activeTabId);
};
closeAllTabs = () => {
const { tabs } = this.get();
this.closeTabs(...tabs.map((t) => t.id));
};
closeTabs = (...ids: string[]) => {
this.set((state) => {
const tabs: TabItem[] = [];
@@ -1177,6 +1221,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;
@@ -1184,14 +1241,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);
};
}

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import createStore from "../common/store";
import { db } from "../common/db";
import BaseStore from "./index";
import config from "../utils/config";
import { EV, EVENTS } from "@notesnook/core";
import Config from "../utils/config";
import { hashNavigate } from "../navigation";
@@ -81,7 +80,7 @@ class UserStore extends BaseStore<UserStore> {
state.user = undefined;
state.isLoggedIn = false;
});
config.clear();
Config.logout();
if (reason) {
await ConfirmDialog.show({
title: strings.loggedOut(),

View File

@@ -57,5 +57,30 @@ function has(predicate: (key: string) => boolean) {
return false;
}
const Config = { set, get, clear, all, has, remove };
function logout() {
const toKeep = [
"editorConfig",
"backupStorageLocation",
"serverUrls",
"corsProxy",
"theme:light",
"theme:dark",
"colorScheme",
"followSystemTheme"
];
const vals = {} as Record<string, any>;
for (const keep of toKeep) {
const val = get(keep);
if (val !== undefined) vals[keep] = val;
}
clear();
for (const key in vals) {
set(key, vals[key]);
}
}
const Config = { set, get, clear, all, has, remove, logout };
export default Config;

View File

@@ -63,7 +63,7 @@ export async function postMessage(
// so all download links needs to be prefixed to avoid any other conflict
data.origin = window.location.origin;
// if we ever (in some feature versoin of streamsaver) would like to
// if we ever (in some feature version of streamsaver) would like to
// redirect back to the page of who initiated a http request
data.referrer = data.referrer || document.referrer || origin;

View File

@@ -81,12 +81,17 @@ function Notebook(props: NotebookProps) {
Promise.all([
!!notebookId && db.notebooks.exists(notebookId),
db.notebooks.exists(rootId)
]).then((exists) => {
]).then(async (exists) => {
if (exists.every((e) => !e)) {
navigate(`/notebooks`, { replace: true });
return;
}
setContext({ type: "notebook", id: notebookId || rootId });
const notebook = await db.notebooks.notebook(notebookId || rootId);
setContext({
type: "notebook",
id: notebookId || rootId,
item: notebook
});
});
}, [rootId, notebookId]);
@@ -148,7 +153,10 @@ function SubNotebooks({ rootId, isCollapsed, onClick }: SubNotebooksProps) {
const toggleSelection = useSelectionStore(
(store) => store.toggleSelectionMode
);
const rootNotebook = usePromise(() => db.notebooks.notebook(rootId));
const rootNotebook = usePromise(
() => db.notebooks.notebook(rootId),
[rootId]
);
const notebooks = useNotebookStore((store) => store.notebooks);
useEffect(() => {

View File

@@ -0,0 +1,3 @@
- Small bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,6 @@
- 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
Thank you for using Notesnook!

View File

@@ -0,0 +1,6 @@
- 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
Thank you for using Notesnook!

View File

@@ -71,5 +71,6 @@
"license": "GPL-3.0-or-later",
"volta": {
"node": "20.12.0"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -117,10 +117,10 @@ export class TabSessionHistory {
remove(tabId: string, sessionId: string) {
const backStack = this.getBackStack(tabId);
let index = backStack.findIndex((item) => item === sessionId);
let index = backStack.lastIndexOf(sessionId);
if (index === -1) {
const forwardStack = this.getForwardStack(tabId);
index = forwardStack.findIndex((item) => item === sessionId);
index = forwardStack.lastIndexOf(sessionId);
forwardStack.splice(index, 1);
this.setForwardStack(tabId, forwardStack);
} else {

View File

@@ -24,7 +24,7 @@ import {
TEST_NOTEBOOK2,
databaseTest
} from "./utils/index.ts";
import { test, expect } from "vitest";
import { test, expect, describe } from "vitest";
const content = {
...TEST_NOTE.content,
@@ -112,3 +112,17 @@ test("search should return restored notes", () =>
expect(filtered).toHaveLength(1);
}));
test("search reminders", () =>
databaseTest().then(async (db) => {
await db.reminders.add({
title: "remind me",
description: "please do",
date: Date.now()
});
const titleSearch = await db.lookup.reminders("remind me").ids();
expect(titleSearch).toHaveLength(1);
const descriptionSearch = await db.lookup.reminders("please do").ids();
expect(descriptionSearch).toHaveLength(1);
}));

View File

@@ -17,9 +17,16 @@ 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 { match } from "fuzzyjs";
import { match, surround } from "fuzzyjs";
import Database from "./index.js";
import { Item, Note, SortOptions, TrashItem } from "../types.js";
import {
Item,
Note,
Notebook,
Reminder,
SortOptions,
TrashItem
} from "../types.js";
import { DatabaseSchema, RawDatabaseSchema } from "../database/index.js";
import { AnyColumnWithTable, Kysely, sql } from "@streetwriters/kysely";
import { FilteredSelector } from "../database/sql-collection.js";
@@ -28,6 +35,7 @@ import { logger } from "../logger.js";
import { rebuildSearchIndex } from "../database/fts.js";
import { transformQuery } from "../utils/query-transformer.js";
import { getSortSelectors } from "../utils/grouping.js";
import { fuzzy } from "../utils/fuzzy.js";
type SearchResults<T> = {
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
@@ -39,11 +47,16 @@ type FuzzySearchField<T> = {
weight?: number;
name: keyof T;
column: AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>;
ignore?: boolean;
};
export default class Lookup {
constructor(private readonly db: Database) {}
notes(query: string, notes?: FilteredSelector<Note>): SearchResults<Note> {
notes(
query: string,
notes?: FilteredSelector<Note>,
opts?: { titleOnly?: boolean }
): SearchResults<Note> {
return this.toSearchResults(async (limit, sortOptions) => {
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
const excludedIds = this.db.trash.cache.notes;
@@ -61,21 +74,23 @@ export default class Lookup {
)
.where("title", "match", query)
.select(["id", sql<number>`rank * 10`.as("rank")])
.unionAll((eb) =>
eb
.selectFrom("content_fts")
.$if(!!notes, (eb) =>
eb.where("noteId", "in", notes!.filter.select("id"))
)
.$if(excludedIds.length > 0, (eb) =>
eb.where("id", "not in", excludedIds)
)
.where("data", "match", query)
.select(["noteId as id", "rank"])
.$castTo<{
id: string;
rank: number;
}>()
.$if(!opts?.titleOnly, (eb) =>
eb.unionAll((eb) =>
eb
.selectFrom("content_fts")
.$if(!!notes, (eb) =>
eb.where("noteId", "in", notes!.filter.select("id"))
)
.$if(excludedIds.length > 0, (eb) =>
eb.where("id", "not in", excludedIds)
)
.where("data", "match", query)
.select(["noteId as id", "rank"])
.$castTo<{
id: string;
rank: number;
}>()
)
)
.as("results")
)
@@ -100,26 +115,34 @@ export default class Lookup {
}
notebooks(query: string) {
return this.search(this.db.notebooks.all, query, [
{ name: "id", column: "notebooks.id", weight: -100 },
const fields: FuzzySearchField<Notebook>[] = [
{ name: "id", column: "notebooks.id", weight: -100, ignore: true },
{ name: "title", column: "notebooks.title", weight: 10 },
{ name: "description", column: "notebooks.description" }
]);
{
name: "description",
column: "notebooks.description"
}
];
return this.search(this.db.notebooks.all, query, fields);
}
tags(query: string) {
return this.search(this.db.tags.all, query, [
{ name: "id", column: "tags.id", weight: -100 },
{ name: "id", column: "tags.id", weight: -100, ignore: true },
{ name: "title", column: "tags.title" }
]);
}
reminders(query: string) {
return this.search(this.db.reminders.all, query, [
{ name: "id", column: "reminders.id", weight: -100 },
const fields: FuzzySearchField<Reminder>[] = [
{ name: "id", column: "reminders.id", weight: -100, ignore: true },
{ name: "title", column: "reminders.title", weight: 10 },
{ name: "description", column: "reminders.description" }
]);
{
name: "description",
column: "reminders.description"
}
];
return this.search(this.db.reminders.all, query, fields);
}
trash(query: string): SearchResults<TrashItem> {
@@ -168,47 +191,37 @@ export default class Lookup {
query: string,
fields: FuzzySearchField<T>[]
) {
return this.toSearchResults(
(limit, sortOptions) =>
this.filter(selector, query, fields, limit, sortOptions),
selector
);
return this.toSearchResults(async (limit, sortOptions) => {
const results = await this.filter(selector, query, fields, {
limit,
sortOptions
});
return results.map((item) => item.id);
}, selector);
}
private async filter<T extends Item>(
selector: FilteredSelector<T>,
query: string,
fields: FuzzySearchField<T>[],
limit?: number,
sortOptions?: SortOptions
fields: readonly FuzzySearchField<T>[],
options: {
limit?: number;
sortOptions?: SortOptions;
prefix?: string;
suffix?: string;
} = {}
) {
const results: Map<string, number> = new Map();
const columns = fields.map((f) => f.column);
for await (const item of selector.fields(columns).iterate()) {
if (limit && results.size >= limit) break;
const items = await selector.fields(columns).items();
for (const field of fields) {
const result = match(query, `${item[field.name]}`);
if (result.match) {
const oldScore = results.get(item.id) || 0;
results.set(item.id, oldScore + result.score * (field.weight || 1));
}
}
}
selector.fields([]);
const sorted = Array.from(results.entries());
if (!sortOptions)
// || sortOptions.sortBy === "relevance")
sorted.sort(
// sortOptions?.sortDirection === "desc"
// ? (a, b) => a[1] - b[1]
// :
(a, b) => b[1] - a[1]
);
return sorted.map((a) => a[0]);
return fuzzy(
query,
items,
Object.fromEntries(
fields.filter((f) => !f.ignore).map((f) => [f.name, f.weight || 1])
) as Record<keyof T, number>,
options
);
}
private toSearchResults<T extends Item>(

View File

@@ -0,0 +1,92 @@
/*
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 { fuzzy } from "../fuzzy.js";
import { test, expect, describe } from "vitest";
describe("lookup.fuzzy", () => {
test("should sort items by score", () => {
const items = [
{
id: "1",
title: "system"
},
{
id: "2",
title: "hello"
},
{
id: "3",
title: "items"
}
];
const query = "ems";
expect(fuzzy(query, items, { title: 1 })).toStrictEqual([items[2]]);
});
describe("opts.prefix", () => {
test("should prefix matched field with provided value when given", () => {
const items = [
{
id: "1",
title: "hello"
},
{
id: "2",
title: "world"
}
];
const query = "d";
expect(
fuzzy(
query,
items,
{ title: 1 },
{
prefix: "prefix-"
}
)
).toStrictEqual([{ id: "2", title: "worlprefix-d" }]);
});
});
describe("opt.suffix", () => {
test("should suffix matched field with provided value when given", () => {
const items = [
{
id: "1",
title: "hello"
},
{
id: "2",
title: "world"
}
];
const query = "llo";
expect(
fuzzy(
query,
items,
{ title: 1 },
{
suffix: "-suffix"
}
)
).toStrictEqual([{ id: "1", title: "hello-suffix" }]);
});
});
});

View File

@@ -21,7 +21,7 @@ import { expect, test } from "vitest";
import { transformQuery } from "../query-transformer.js";
function lt2(str: string) {
return `(">${str}" OR ${str} OR "${str}<")`;
return `(">${str}" OR "${str}" OR "${str}<")`;
}
const TRANSFORM_QUERY_TESTS = [
@@ -65,14 +65,14 @@ const TRANSFORM_QUERY_TESTS = [
["foo + OR", `foo AND ${lt2("+")}`],
['"special -phrase*"', '"special -phrase*"'],
["foo* + bar*", `"foo*" AND ${lt2("+")} AND "bar*"`],
["(foo + bar) -baz", `(foo AND ${lt2("+")} AND bar) AND "-baz"`],
["(foo + bar) -baz", `"(foo" AND ${lt2("+")} AND "bar)" AND "-baz"`],
['"phrase with "quotes""', '"phrase with ""quotes"""'],
[
'foo + "bar -baz" OR "qux*"',
`foo AND ${lt2("+")} AND "bar -baz" OR "qux*"`
],
["foo + bar + ", `foo AND ${lt2("+")} AND bar AND ${lt2("+")}`],
["+foo bar", `+foo AND bar`],
["+foo bar", `"+foo" AND bar`],
["foo*bar*", `"foo*bar*"`],
['"escaped "quotes""', '"escaped ""quotes"""'],
["-hello-world", `"-hello-world"`],

View File

@@ -0,0 +1,75 @@
/*
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 { match, surround } from "fuzzyjs";
import { clone } from "./clone";
export function fuzzy<T extends { id: string }>(
query: string,
items: T[],
fields: Partial<Record<keyof T, number>>,
options: {
limit?: number;
prefix?: string;
suffix?: string;
} = {}
): T[] {
const results: Map<
string,
{
item: T;
score: number;
}
> = new Map();
for (const item of items) {
if (options.limit && results.size >= options.limit) break;
for (const field in fields) {
const result = match(query, `${item[field]}`);
if (!result.match) continue;
const oldMatch = results.get(item.id);
const clonedItem = oldMatch?.item || clone(item);
if (options.suffix || options.prefix) {
clonedItem[field] = surround(`${clonedItem[field]}`, {
suffix: options.suffix,
prefix: options.prefix,
result
}) as T[Extract<keyof T, string>];
}
if (oldMatch) {
oldMatch.score += result.score * (fields[field] || 1);
} else {
results.set(item.id, {
item: clonedItem,
score: result.score * (fields[field] || 1)
});
}
}
}
if (results.size === 0) return [];
const sorted = Array.from(results.entries());
sorted.sort((a, b) => b[1].score - a[1].score);
return sorted.map((item) => item[1].item);
}

View File

@@ -41,3 +41,4 @@ export * from "./set.js";
export * from "./title-format.js";
export * from "./virtualized-grouping.js";
export * from "./crypto.js";
export * from "./fuzzy.js";

View File

@@ -33,23 +33,20 @@ type OperatorNode = {
type: "AND" | "OR" | "NOT";
};
const INVALID_CHARS = /[:<>./\\()$&=#!\-+~§@^?,;'"[\]{}|]/;
function escapeSQLString(str: string): string {
if (str.startsWith('"') && str.endsWith('"')) {
const innerStr = str.slice(1, -1).replace(/"/g, '""');
return `"${innerStr}"`;
}
const maybeColspec =
str.includes(":") ||
str.includes(">") ||
str.includes("<") ||
str.includes("-");
const isInvalidChar = INVALID_CHARS.test(str);
const isWildcard =
str.startsWith("*") ||
str.endsWith("*") ||
str.startsWith("%") ||
str.endsWith("%");
if (maybeColspec || isWildcard) {
if (isInvalidChar || isWildcard) {
return `"${str}"`;
}
@@ -159,6 +156,21 @@ function generateSQL(ast: QueryNode): string {
result.push(`(">${value}"`, "OR", value, "OR", `"${value}<")`);
result.push("AND");
continue;
} else if (
value.length === 3 &&
value[0] === '"' &&
value[2] === '"' &&
!["<", ">"].includes(value[1])
) {
result.push(
`(">${value[1]}"`,
"OR",
value,
"OR",
`"${value[1]}<")`
);
result.push("AND");
continue;
}
result.push(value);

View File

@@ -38,13 +38,6 @@ if (currentTheme) {
useThemeEngineStore.getState().setTheme(currentTheme);
}
window.addEventListener("error", (event) => {
post("editorError", {
message: event.error.message,
stack: event.error?.stack || ""
});
});
class ExceptionHandler extends React.Component<{
children: React.ReactNode;
component: string;

View File

@@ -556,7 +556,7 @@ const Tiptap = ({
</>
)}
{controller.loading || tab.session?.noteLocked ? (
{controller.loading || tab.session?.locked ? (
<div
style={{
width: "100%",

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