mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
73 Commits
web/publis
...
release/3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
109e46bd06 | ||
|
|
2738207c20 | ||
|
|
1aa73c1f23 | ||
|
|
9ec22724e9 | ||
|
|
4a7aa27bd9 | ||
|
|
6051614d96 | ||
|
|
295a461dda | ||
|
|
a4cbe83211 | ||
|
|
afd63db043 | ||
|
|
aadb6e3a01 | ||
|
|
7c3fdab6ee | ||
|
|
14f727d6e6 | ||
|
|
688a646181 | ||
|
|
7a7f0db941 | ||
|
|
96be35c105 | ||
|
|
36bacce3ac | ||
|
|
c89e333c11 | ||
|
|
d521eb3bd8 | ||
|
|
20bf090c7e | ||
|
|
6cc9b8d1e1 | ||
|
|
e931423eba | ||
|
|
743d47e9e9 | ||
|
|
37c96177a7 | ||
|
|
834479d095 | ||
|
|
643515c6c4 | ||
|
|
0b0a4f1b6f | ||
|
|
48d88069c0 | ||
|
|
9d7754f1e1 | ||
|
|
b2ed115011 | ||
|
|
3447e8133c | ||
|
|
4473d8ec67 | ||
|
|
08cf21b05a | ||
|
|
10dccba5b6 | ||
|
|
23c9a98013 | ||
|
|
473fae4a30 | ||
|
|
e9808c08f0 | ||
|
|
4bb46ea149 | ||
|
|
01811372fc | ||
|
|
6d2097259b | ||
|
|
4c3e065928 | ||
|
|
e45b72a8c4 | ||
|
|
aceb6db8b8 | ||
|
|
5a0ca0bed9 | ||
|
|
8dd5779308 | ||
|
|
dc958098ea | ||
|
|
bf536eb183 | ||
|
|
eb3eea031e | ||
|
|
d376e00a1e | ||
|
|
aca36b60b7 | ||
|
|
033666795c | ||
|
|
c0872c98a8 | ||
|
|
f2173a42d9 | ||
|
|
cbddea4518 | ||
|
|
c69304781a | ||
|
|
412631bf67 | ||
|
|
8c2be00920 | ||
|
|
553e2060be | ||
|
|
7603de25c6 | ||
|
|
146216949d | ||
|
|
c4d8abef16 | ||
|
|
baf823264b | ||
|
|
7f0a517ee1 | ||
|
|
da48cf0bd1 | ||
|
|
4c41e13c5d | ||
|
|
82e2953bc8 | ||
|
|
fc244b1196 | ||
|
|
98ecad3450 | ||
|
|
54dcbd9be6 | ||
|
|
d26c99c487 | ||
|
|
0922d9d833 | ||
|
|
0cdeee80fa | ||
|
|
3aa23d3215 | ||
|
|
e0b9932aba |
2
.github/workflows/desktop.preview.yml
vendored
2
.github/workflows/desktop.preview.yml
vendored
@@ -210,7 +210,7 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
outputs:
|
||||
windows-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
|
||||
steps:
|
||||
|
||||
15
.github/workflows/desktop.publish.yml
vendored
15
.github/workflows/desktop.publish.yml
vendored
@@ -107,7 +107,7 @@ jobs:
|
||||
target_commitish: ${{ github.ref }}
|
||||
|
||||
- name: Generate flatpak sources
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
if: inputs.build-linux
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
|
||||
@@ -127,6 +127,13 @@ jobs:
|
||||
files: ./generated-sources.json
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
- name: Upload generated-sources.json as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: inputs.build-linux
|
||||
with:
|
||||
name: generated-sources
|
||||
path: ./generated-sources.json
|
||||
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
@@ -182,7 +189,9 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
@@ -264,6 +273,7 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
@@ -331,6 +341,7 @@ jobs:
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
@@ -363,7 +374,7 @@ jobs:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
|
||||
107
.github/workflows/desktop.tests.yml
vendored
107
.github/workflows/desktop.tests.yml
vendored
@@ -18,11 +18,12 @@ on:
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
runs-on: macos-15-intel
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
@@ -33,44 +34,10 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Generate desktop build
|
||||
run: npm run tx @notesnook/web:build:desktop
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
needs: build
|
||||
runs-on: macos-15-intel
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --x64
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests x64
|
||||
@@ -87,7 +54,6 @@ jobs:
|
||||
|
||||
test-macos:
|
||||
name: Test macOS
|
||||
needs: build
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
@@ -97,23 +63,15 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --mac --dir --arm64
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests arm64
|
||||
@@ -130,7 +88,6 @@ jobs:
|
||||
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
@@ -140,29 +97,15 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --config=electron-builder.config.js --linux --dir --arm64 --x64
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
@@ -179,8 +122,7 @@ jobs:
|
||||
|
||||
test-windows:
|
||||
name: Test for Windows
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
@@ -189,34 +131,17 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
|
||||
npm i --cpu arm64 sqlite3-fts5-html
|
||||
npm i --cpu x64 sqlite3-fts5-html
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: node scripts/execute.mjs @notesnook/desktop:release
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --config=electron-builder.config.js --win --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
@@ -17,10 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test } from "@nn/test";
|
||||
import { test, expect } from "@nn/test";
|
||||
import { gt, lt } from "semver";
|
||||
import { AppModel } from "../../web/__e2e__/models/app.model.js";
|
||||
import { expect } from "playwright/test";
|
||||
|
||||
test.extend({ options: { version: "3.0.0" } })(
|
||||
"update starts downloading if version is outdated",
|
||||
|
||||
@@ -17,8 +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 { mergeTests } from "@playwright/test";
|
||||
import { test } from "@playwright/test";
|
||||
import { mergeTests, test } from "@playwright/test";
|
||||
import type { CommonFixtures, CommonWorkerFixtures } from "./common-fixtures";
|
||||
import { commonFixtures } from "./common-fixtures";
|
||||
import { platformTest } from "./platform-fixtures";
|
||||
|
||||
@@ -21,7 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { AppContext, buildAndLaunchApp, TestOptions } from "./utils";
|
||||
import path from "path";
|
||||
import { version } from "../../package.json";
|
||||
import type { Browser, ElectronApplication, Page } from "@playwright/test";
|
||||
import type { ElectronApplication, Page } from "@playwright/test";
|
||||
import type { TraceViewerFixtures } from "./trace-viewer-fixtures";
|
||||
import { traceViewerFixtures } from "./trace-viewer-fixtures";
|
||||
import { PageTestFixtures, PageWorkerFixtures } from "./page-test-api";
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
/* eslint-disable header/header */
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/// @ts-ignore
|
||||
import { oop, client } from "playwright-core/lib/coreBundle";
|
||||
|
||||
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.
|
||||
export type TestModeName = "default" | "driver";
|
||||
|
||||
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/>.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
// @ts-ignore
|
||||
import { start } from "playwright-core/lib/outofprocess";
|
||||
// @ts-ignore
|
||||
import type { Playwright } from "playwright-core/lib/client/playwright";
|
||||
|
||||
export type TestModeName =
|
||||
| "default"
|
||||
| "driver"
|
||||
| "service"
|
||||
| "service2"
|
||||
| "wsl";
|
||||
const { start } = oop;
|
||||
|
||||
interface TestMode {
|
||||
setup(): Promise<Playwright>;
|
||||
setup(): Promise<client.Playwright>;
|
||||
teardown(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DriverTestMode implements TestMode {
|
||||
private _impl: { playwright: Playwright; stop: () => Promise<void> };
|
||||
private _impl: { playwright: client.Playwright; stop: () => Promise<void> };
|
||||
|
||||
async setup() {
|
||||
this._impl = await start({
|
||||
|
||||
@@ -20,7 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { execSync } from "child_process";
|
||||
import { cp } from "fs/promises";
|
||||
import path, { join, resolve } from "path";
|
||||
import { _electron as electron, ElectronApplication, Page } from "playwright";
|
||||
import {
|
||||
_electron as electron,
|
||||
ElectronApplication,
|
||||
Page
|
||||
} from "@playwright/test";
|
||||
import { existsSync } from "fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
|
||||
@@ -30,7 +34,7 @@ const root = path.resolve(__dirname, "..", "..");
|
||||
const SOURCE_DIR = resolve(root, "output", productName);
|
||||
|
||||
export interface AppContext {
|
||||
app: import("playwright").ElectronApplication;
|
||||
app: ElectronApplication;
|
||||
userDataDir: string;
|
||||
outputDir: string;
|
||||
relaunch: () => Promise<void>;
|
||||
|
||||
10297
apps/desktop/package-lock.json
generated
10297
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.4.0-beta.1",
|
||||
"version": "3.4.4",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
@@ -54,18 +54,18 @@
|
||||
"zod": "3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.15.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^37.0.0",
|
||||
"electron": "^37.10.3",
|
||||
"electron-builder": "^26.8.1",
|
||||
"esbuild": "0.21.5",
|
||||
"fkill": "^10.0.3",
|
||||
"node-abi": "^4.5.0",
|
||||
"node-gyp-build": "^4.8.4",
|
||||
"playwright": "1.58.2",
|
||||
"playwright-core": "1.61.1",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -86,7 +86,7 @@
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package",
|
||||
"test": "playwright test --tsconfig tsconfig.json"
|
||||
"test": "playwright test --tsconfig __tests__/tsconfig.json --project notesnook-desktop"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
diff --git a/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js b/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
index 7d8468f..ab252ef 100644
|
||||
--- a/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
+++ b/node_modules/@playwright/test/node_modules/playwright-core/lib/coreBundle.js
|
||||
@@ -16010,6 +16010,7 @@ var init_validator = __esm({
|
||||
args: tOptional(tArray(tString)),
|
||||
chromiumSandbox: tOptional(tBoolean),
|
||||
cwd: tOptional(tString),
|
||||
+ baseURL: tOptional(tString),
|
||||
env: tOptional(tArray(tType("NameValue"))),
|
||||
timeout: tFloat,
|
||||
acceptDownloads: tOptional(tEnum(["accept", "deny", "internal-browser-default"])),
|
||||
diff --git a/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts b/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
index 0bff9d3..d195697 100644
|
||||
--- a/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
+++ b/node_modules/@playwright/test/node_modules/playwright-core/types/types.d.ts
|
||||
@@ -21813,6 +21813,7 @@ export interface Electron {
|
||||
* Additional arguments to pass to the application when launching. You typically pass the main script name here.
|
||||
*/
|
||||
args?: Array<string>;
|
||||
+ baseURL?: string;
|
||||
|
||||
/**
|
||||
* If specified, artifacts (traces, videos, downloads, HAR files, etc.) are saved into this directory. The directory
|
||||
@@ -1,24 +0,0 @@
|
||||
diff --git a/node_modules/playwright-core/lib/protocol/validator.js b/node_modules/playwright-core/lib/protocol/validator.js
|
||||
index b36c979..9e8ca15 100644
|
||||
--- a/node_modules/playwright-core/lib/protocol/validator.js
|
||||
+++ b/node_modules/playwright-core/lib/protocol/validator.js
|
||||
@@ -2536,6 +2536,7 @@ import_validatorPrimitives.scheme.ElectronLaunchParams = (0, import_validatorPri
|
||||
executablePath: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
args: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tArray)(import_validatorPrimitives.tString)),
|
||||
cwd: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
+ baseURL: (0, import_validatorPrimitives.tOptional)(import_validatorPrimitives.tString),
|
||||
env: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tArray)((0, import_validatorPrimitives.tType)("NameValue"))),
|
||||
timeout: import_validatorPrimitives.tFloat,
|
||||
acceptDownloads: (0, import_validatorPrimitives.tOptional)((0, import_validatorPrimitives.tEnum)(["accept", "deny", "internal-browser-default"])),
|
||||
diff --git a/node_modules/playwright-core/types/types.d.ts b/node_modules/playwright-core/types/types.d.ts
|
||||
index f6e6165..4efd4d4 100644
|
||||
--- a/node_modules/playwright-core/types/types.d.ts
|
||||
+++ b/node_modules/playwright-core/types/types.d.ts
|
||||
@@ -19233,6 +19233,7 @@ export interface Electron {
|
||||
* Additional arguments to pass to the application when launching. You typically pass the main script name here.
|
||||
*/
|
||||
args?: Array<string>;
|
||||
+ baseURL?: string;
|
||||
|
||||
/**
|
||||
* Toggles bypassing page's Content-Security-Policy. Defaults to `false`.
|
||||
@@ -46,7 +46,7 @@ const config: Config<PlaywrightWorkerOptions & PlaywrightTestOptions> = {
|
||||
globalTimeout: 5400000,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: process.env.CI
|
||||
? [["dot"], ["json", { outputFile: path.join(outputDir, "report.json") }]]
|
||||
: "line",
|
||||
|
||||
@@ -33,17 +33,38 @@ function generateId() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
function getStreamOrThrow(id: string, operation: "write" | "close") {
|
||||
const stream = activeStreams.get(id);
|
||||
if (!stream) {
|
||||
throw new Error(
|
||||
`Backup stream not found during ${operation}. The stream may have already been closed or was never opened. Stream id: ${id}`
|
||||
);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
export const backupsRouter = t.router({
|
||||
open: t.procedure
|
||||
.input(z.object({ filename: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const { filename } = input;
|
||||
if (!filename.trim()) {
|
||||
throw new Error("Invalid backup filename: filename cannot be empty.");
|
||||
}
|
||||
if (filename.includes(path.sep) || filename.includes("\\"))
|
||||
throw new Error("Invalid filename");
|
||||
throw new Error(
|
||||
`Invalid backup filename: expected a plain file name without path separators, received "${filename}".`
|
||||
);
|
||||
const resolvedBackupDir = resolvePath(config.backupDirectory);
|
||||
const backupPath = path.resolve(resolvedBackupDir, filename);
|
||||
if (!backupPath.startsWith(resolvedBackupDir))
|
||||
throw new Error("Invalid filename");
|
||||
const relativeBackupPath = path.relative(resolvedBackupDir, backupPath);
|
||||
if (
|
||||
relativeBackupPath.startsWith("..") ||
|
||||
path.isAbsolute(relativeBackupPath)
|
||||
)
|
||||
throw new Error(
|
||||
`Invalid backup filename: resolved path "${backupPath}" is outside the configured backup directory "${resolvedBackupDir}". The configured backup directory may be invalid.`
|
||||
);
|
||||
|
||||
mkdirSync(resolvedBackupDir, { recursive: true });
|
||||
const stream = createWriteStream(backupPath, { encoding: "utf-8" });
|
||||
@@ -55,8 +76,7 @@ export const backupsRouter = t.router({
|
||||
write: t.procedure
|
||||
.input(z.object({ id: z.string(), chunk: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const stream = activeStreams.get(input.id);
|
||||
if (!stream) throw new Error("Stream not found");
|
||||
const stream = getStreamOrThrow(input.id, "write");
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
stream.write(Buffer.from(input.chunk, "base64"), (err) =>
|
||||
err ? reject(err) : resolve()
|
||||
@@ -67,8 +87,7 @@ export const backupsRouter = t.router({
|
||||
close: t.procedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(({ input }) => {
|
||||
const stream = activeStreams.get(input.id);
|
||||
if (!stream) throw new Error("Stream not found");
|
||||
const stream = getStreamOrThrow(input.id, "close");
|
||||
return new Promise<void>((resolve) => {
|
||||
stream.end(() => {
|
||||
activeStreams.delete(input.id);
|
||||
|
||||
@@ -33,7 +33,7 @@ import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
import { getTheme, setTheme, Theme } from "../utils/theme";
|
||||
import { existsSync } from "fs";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { normalizePathString, resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak, isPortable, isSnap } from "../utils";
|
||||
@@ -60,7 +60,13 @@ export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
isSnap: t.procedure.query(() => isSnap()),
|
||||
isPortable: t.procedure.query(() => isPortable()),
|
||||
backupDirectory: t.procedure.query(() => config.backupDirectory),
|
||||
backupDirectory: t.procedure.query(() => {
|
||||
const backupDirectory = normalizePathString(config.backupDirectory);
|
||||
if (backupDirectory !== config.backupDirectory) {
|
||||
config.backupDirectory = backupDirectory;
|
||||
}
|
||||
return backupDirectory;
|
||||
}),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
||||
import { Messages, setI18nGlobal } from "@notesnook/intl";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { PATHS } from "./constants";
|
||||
import { normalizePathString } from "./utils/resolve-path";
|
||||
|
||||
const locale =
|
||||
process.env.NODE_ENV === "development"
|
||||
@@ -294,7 +295,7 @@ async function migrateBackupDirectory() {
|
||||
`localStorage.getItem("backupStorageLocation")`
|
||||
);
|
||||
if (!oldPath || oldPath === PATHS.backupsDirectory) return;
|
||||
config.backupDirectory = oldPath;
|
||||
config.backupDirectory = normalizePathString(oldPath);
|
||||
} catch (e) {
|
||||
console.error("Failed to migrate backup directory", e);
|
||||
const pressedButton = dialog.showMessageBoxSync(globalThis.window, {
|
||||
|
||||
@@ -20,11 +20,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { app } from "electron";
|
||||
import { isAbsolute, join } from "path";
|
||||
|
||||
export function normalizePathString(_path: string) {
|
||||
let normalizedPath = _path.trim();
|
||||
|
||||
try {
|
||||
const parsedPath = JSON.parse(normalizedPath);
|
||||
if (typeof parsedPath === "string") normalizedPath = parsedPath;
|
||||
} catch {
|
||||
// ignore invalid JSON and keep the original path string
|
||||
}
|
||||
|
||||
if (normalizedPath === "~") return app.getPath("home");
|
||||
if (/^~[\\/]/.test(normalizedPath)) {
|
||||
return join(app.getPath("home"), normalizedPath.slice(2));
|
||||
}
|
||||
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
export function resolvePath(_path: string) {
|
||||
if (isAbsolute(_path)) return _path;
|
||||
const normalizedPath = normalizePathString(_path);
|
||||
if (isAbsolute(normalizedPath)) return normalizedPath;
|
||||
|
||||
return join(
|
||||
..._path.split("/").map((segment) => {
|
||||
...normalizedPath.split("/").map((segment) => {
|
||||
let resolved = segment;
|
||||
try {
|
||||
resolved = app.getPath(resolved as any);
|
||||
|
||||
@@ -140,7 +140,7 @@ android {
|
||||
if (project.hasProperty("prBuildNumber")) {
|
||||
versionCode Integer.parseInt(prBuildNumber())
|
||||
} else {
|
||||
versionCode 3108
|
||||
versionCode 3111
|
||||
}
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
- Inbox API is now in beta. Learn more about it at https://help.notesnook.com/inbox-api/getting-started
|
||||
- Added close all tabs button in editor
|
||||
- Deeplinking support for nn:// urls
|
||||
- Set default image compression mode in Settings -> Behavior
|
||||
- Minor bug fixes and performance improvements
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
|
||||
@@ -729,6 +729,13 @@ export const VaultDialog: React.FC = () => {
|
||||
>
|
||||
{(isChangePassword ||
|
||||
isClearVault ||
|
||||
requestType === VaultRequestType.LockNote ||
|
||||
requestType === VaultRequestType.UnlockNote ||
|
||||
requestType === VaultRequestType.PermanentUnlock ||
|
||||
isGoToEditor ||
|
||||
isShareNote ||
|
||||
isDeleteNote ||
|
||||
isEnableFingerprint ||
|
||||
(isDeleteVault && isUserLoggedIn) ||
|
||||
isCustomAction) &&
|
||||
!isRevokeFingerprint ? (
|
||||
|
||||
@@ -39,6 +39,7 @@ import Share from "react-native-share";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
|
||||
const ImagePreview = () => {
|
||||
const { colors } = useThemeColors("dialog");
|
||||
@@ -87,9 +88,8 @@ const ImagePreview = () => {
|
||||
return;
|
||||
}
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
const path = `${cacheDir}/${
|
||||
"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
|
||||
}`;
|
||||
const path = `${cacheDir}/${"NN_" + (await getFileNameWithExtension(hash, attachment?.mimeType))
|
||||
}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, path).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
@@ -136,17 +136,23 @@ const ImagePreview = () => {
|
||||
borderWidth: 0
|
||||
}}
|
||||
onPress={async () => {
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(true);
|
||||
await Share.open({
|
||||
url: image
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setAppDidEnterBackgroundForAction(false);
|
||||
if (!image) return;
|
||||
|
||||
try {
|
||||
if (Platform.OS === "android") {
|
||||
await FileViewer.open(image.replace("file://", ""), {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
} as any);
|
||||
} else {
|
||||
await Share.open({
|
||||
url: image
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
|
||||
@@ -72,6 +72,7 @@ const MergeConflicts = () => {
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const content = useRef<UnencryptedContentItem>(null);
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const keepScreenOn = useSettingStore((state) => state.settings.keepScreenOn);
|
||||
|
||||
const applyChanges = async () => {
|
||||
const contentToSave = selectedContent;
|
||||
@@ -155,9 +156,9 @@ const MergeConflicts = () => {
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
@@ -182,9 +183,9 @@ const MergeConflicts = () => {
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
@@ -385,7 +386,7 @@ const MergeConflicts = () => {
|
||||
paddingTop: insets.top
|
||||
}}
|
||||
>
|
||||
<KeepAwake />
|
||||
{keepScreenOn && <KeepAwake />}
|
||||
{dialogVisible && (
|
||||
<BaseDialog visible={true}>
|
||||
<DialogContainer>
|
||||
|
||||
@@ -28,7 +28,7 @@ import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ActivityIndicator, TextInput, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { useDBItem } from "../../../hooks/use-db-item";
|
||||
@@ -40,6 +40,8 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
|
||||
const ListNoteItem = ({
|
||||
id,
|
||||
@@ -118,8 +120,8 @@ const ListBlockItem = ({
|
||||
{item?.content.length > 200
|
||||
? item?.content.slice(0, 200) + "..."
|
||||
: !item.content || item.content.trim() === ""
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
? strings.linkNoteEmptyBlock()
|
||||
: item.content}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
@@ -158,6 +160,7 @@ export default function LinkNote(props: {
|
||||
|
||||
const [selectedNote, setSelectedNote] = useState<Note>();
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string>();
|
||||
const [blocksLoading, setBlocksLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
db.notes.all.sorted(db.settings.getGroupOptions("notes")).then((notes) => {
|
||||
@@ -206,10 +209,12 @@ export default function LinkNote(props: {
|
||||
|
||||
const onSelectNote = async (note: Note) => {
|
||||
setSelectedNote(note);
|
||||
setBlocksLoading(true);
|
||||
inputRef.current?.clear();
|
||||
setTimeout(async () => {
|
||||
nodesRef.current = await db.notes.contentBlocks(note.id);
|
||||
setNodes(nodesRef.current);
|
||||
setBlocksLoading(false);
|
||||
});
|
||||
// Fetch and set note's nodes.
|
||||
};
|
||||
@@ -224,6 +229,7 @@ export default function LinkNote(props: {
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingBottom: DefaultAppStyles.GAP,
|
||||
minHeight: "100%",
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
@@ -330,28 +336,42 @@ export default function LinkNote(props: {
|
||||
windowSize={3}
|
||||
keyExtractor={(item) => item.id}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{blockLinking?.error}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
!blockLinking || blocksLoading ? (
|
||||
<ActivityIndicator size={25} color={colors.primary.accent} />
|
||||
) : !blockLinking.isAllowed ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
backgroundColor: colors.secondary.background,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
alignItems: "center"
|
||||
}}
|
||||
type="accent"
|
||||
/>
|
||||
</View>
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{blockLinking?.error}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
onPress={() => {
|
||||
Navigation.navigate("PayWall", {
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out",
|
||||
canGoBack: true
|
||||
});
|
||||
|
||||
props.close?.();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
data={blockLinking?.isAllowed ? nodes : []}
|
||||
/>
|
||||
|
||||
@@ -198,7 +198,7 @@ const PublishNoteSheet = ({
|
||||
};
|
||||
|
||||
const monographMetadata =
|
||||
monographData.result ?? lastMonographDataResult.current;
|
||||
monographData.result?.metadata ?? lastMonographDataResult.current;
|
||||
|
||||
useEffect(() => {
|
||||
const prevState = previousAppState.current;
|
||||
@@ -432,7 +432,7 @@ const PublishNoteSheet = ({
|
||||
{isFeatureAvailable?.isAllowed &&
|
||||
!selfDestruct &&
|
||||
monographMetadata &&
|
||||
monographMetadata?.totalViews > 0 ? (
|
||||
monographMetadata?.analytics?.totalViews > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -456,7 +456,9 @@ const PublishNoteSheet = ({
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
|
||||
<Paragraph>{monographMetadata?.totalViews || 0}</Paragraph>
|
||||
<Paragraph>
|
||||
{monographMetadata?.analytics?.totalViews || 0}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -156,7 +156,8 @@ export function MenuItem({
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{renderIcon ? (
|
||||
@@ -185,6 +186,7 @@ export function MenuItem({
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{ flexShrink: 1 }}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
@@ -195,6 +197,9 @@ export function MenuItem({
|
||||
color={
|
||||
isFocused ? colors.primary.paragraph : colors.secondary.paragraph
|
||||
}
|
||||
style={{
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{menuItemCount}
|
||||
</Paragraph>
|
||||
|
||||
@@ -72,7 +72,7 @@ export const NotebookItem = ({
|
||||
const notebook = item.notebook;
|
||||
const isFocused = focused;
|
||||
const { totalNotes, getTotalNotes } = useTotalNotes("notebook");
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
const getTotalNotesRef = React.useRef(getTotalNotes);
|
||||
getTotalNotesRef.current = getTotalNotes;
|
||||
const { colors } = useThemeColors();
|
||||
@@ -101,8 +101,8 @@ export const NotebookItem = ({
|
||||
item.depth === 0
|
||||
? undefined
|
||||
: item.depth < 6
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
? 15 * item.depth
|
||||
: 15 * 5,
|
||||
width: "100%",
|
||||
marginTop: 2,
|
||||
opacity: item.disabled ? 0.5 : 1
|
||||
@@ -162,7 +162,8 @@ export const NotebookItem = ({
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
@@ -191,8 +192,8 @@ export const NotebookItem = ({
|
||||
!item.hasChildren || disableExpand
|
||||
? "book-outline"
|
||||
: expanded
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
? "chevron-down"
|
||||
: "chevron-right"
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -201,6 +202,7 @@ export const NotebookItem = ({
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{ flexShrink: 1 }}
|
||||
>
|
||||
{notebook?.title}
|
||||
</Paragraph>
|
||||
@@ -211,7 +213,8 @@ export const NotebookItem = ({
|
||||
gap: DefaultAppStyles.GAP_SMALL,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{selectionEnabled ? (
|
||||
|
||||
@@ -56,7 +56,7 @@ const TagItem = (props: {
|
||||
const totalNotes = useTotalNotes("tag");
|
||||
const totalNotesRef = React.useRef(totalNotes);
|
||||
totalNotesRef.current = totalNotes;
|
||||
const updater = useRelationStore(state => state.updater);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.id) {
|
||||
@@ -67,6 +67,8 @@ const TagItem = (props: {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 35,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop:
|
||||
(props.id as number) === 0 ? DefaultAppStyles.GAP_VERTICAL : 2
|
||||
@@ -123,7 +125,8 @@ const TagItem = (props: {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -146,6 +149,9 @@ const TagItem = (props: {
|
||||
isFocused ? colors.selected.paragraph : colors.primary.paragraph
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{item?.title}
|
||||
</Paragraph>
|
||||
@@ -173,6 +179,9 @@ const TagItem = (props: {
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
marginLeft: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{totalNotes.totalNotes(item?.id)}
|
||||
</Paragraph>
|
||||
|
||||
@@ -1333,7 +1333,7 @@ export const useActions = ({
|
||||
icon: "link",
|
||||
onPress: () => {
|
||||
const link = createInternalLink(item.type, item.id);
|
||||
Clipboard.setString(link);
|
||||
Clipboard.setHTML(`<a href="${link}" >${item.title}</a>`, link);
|
||||
ToastManager.show({
|
||||
heading: strings.linkCopied(),
|
||||
message: link,
|
||||
|
||||
@@ -766,6 +766,26 @@ export const useAppEvents = () => {
|
||||
db.eventManager.subscribe(EVENTS.uploadCanceled, (data) => {
|
||||
useAttachmentStore.getState().setUploading(data);
|
||||
}),
|
||||
db.eventManager.subscribeMulti(
|
||||
[EVENTS.vaultAutoLocked, EVENTS.vaultLocked],
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
message: strings.vaultLocked(),
|
||||
type: "info"
|
||||
});
|
||||
}, 300);
|
||||
},
|
||||
undefined
|
||||
),
|
||||
db.eventManager.subscribe(EVENTS.vaultUnlocked, () => {
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
message: strings.vaultUnlocked(),
|
||||
type: "info"
|
||||
});
|
||||
}, 300);
|
||||
}),
|
||||
EV.subscribe(EVENTS.migrationStarted, (name) => {
|
||||
if (
|
||||
name !== "notesnook" ||
|
||||
@@ -820,7 +840,13 @@ export const useAppEvents = () => {
|
||||
|
||||
return () => {
|
||||
emitterSubscriptions.forEach((sub) => sub?.remove?.());
|
||||
subscriptions.forEach((sub) => sub?.unsubscribe?.());
|
||||
subscriptions.forEach((sub) => {
|
||||
if (Array.isArray(sub)) {
|
||||
sub.forEach((s) => s.unsubscribe());
|
||||
} else {
|
||||
sub?.unsubscribe?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [onSyncComplete, onUserUpdated]);
|
||||
|
||||
|
||||
@@ -447,7 +447,9 @@ const onChangeTab = async (event: { i: number; from: number }) => {
|
||||
if (event.i === 2) {
|
||||
editorState().movedAway = false;
|
||||
editorState().isFocused = true;
|
||||
activateKeepAwake();
|
||||
if (useSettingStore.getState().settings.keepScreenOn) {
|
||||
activateKeepAwake();
|
||||
}
|
||||
eSendEvent(eOnEnterEditor);
|
||||
|
||||
if (
|
||||
|
||||
@@ -163,7 +163,20 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
|
||||
}, [noteIds]);
|
||||
|
||||
const onSave = async () => {
|
||||
const changedNotebooks = useNotebookSelectionStore.getState().selection;
|
||||
const selectionState = useNotebookSelectionStore.getState();
|
||||
const changedNotebooks = { ...selectionState.selection };
|
||||
|
||||
if (!selectionState.multiSelect) {
|
||||
for (const id in selectionState.initialState) {
|
||||
if (
|
||||
selectionState.initialState[id] === "selected" &&
|
||||
changedNotebooks[id] !== "selected"
|
||||
) {
|
||||
changedNotebooks[id] = "deselected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in changedNotebooks) {
|
||||
const item = await db.notebooks.notebook(id);
|
||||
if (!item) continue;
|
||||
@@ -207,11 +220,11 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
|
||||
rightButton={
|
||||
hasSelection
|
||||
? {
|
||||
name: "restore",
|
||||
onPress: () => {
|
||||
updateInitialSelectionState(noteIds);
|
||||
}
|
||||
name: "restore",
|
||||
onPress: () => {
|
||||
updateInitialSelectionState(noteIds);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -398,7 +398,8 @@ const TagItem = ({
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "flex-start",
|
||||
height: 40
|
||||
minHeight: 40,
|
||||
alignItems: "center"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!tag) return;
|
||||
@@ -438,7 +439,9 @@ const TagItem = ({
|
||||
/>
|
||||
)}
|
||||
{tag ? (
|
||||
<Paragraph size={AppFontSize.sm}>{"#" + tag?.title}</Paragraph>
|
||||
<Paragraph style={{ flexShrink: 1 }} size={AppFontSize.sm}>
|
||||
{"#" + tag?.title}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -168,7 +168,7 @@ export const FailedInboxItems = () => {
|
||||
const result = usePromise(() => db.inboxItemsHistory.failed.items());
|
||||
|
||||
async function deleteItem(id: string) {
|
||||
await db.inboxItemsHistory.delete(id);
|
||||
await db.inboxItemsHistory.delete([id]);
|
||||
ToastManager.show({
|
||||
message: strings.itemDeleted(),
|
||||
type: "success"
|
||||
|
||||
@@ -210,7 +210,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
: status === SubscriptionStatus.ACTIVE
|
||||
? strings.subRenewOn(expiryDate)
|
||||
: status === SubscriptionStatus.CANCELED ||
|
||||
status === SubscriptionStatus.PAUSED
|
||||
status === SubscriptionStatus.PAUSED
|
||||
? strings.subEndsOn(expiryDate)
|
||||
: status === SubscriptionStatus.EXPIRED
|
||||
? subscriptionDaysLeft.time < -3
|
||||
@@ -965,6 +965,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
property: "checkForUpdates",
|
||||
icon: "update"
|
||||
},
|
||||
{
|
||||
id: "keep-screen-on",
|
||||
type: "switch",
|
||||
name: strings.keepScreenOn(),
|
||||
description: strings.keepScreenOnDesc(),
|
||||
property: "keepScreenOn",
|
||||
icon: "cellphone-screenshot",
|
||||
},
|
||||
{
|
||||
id: "image-compression",
|
||||
type: "component",
|
||||
@@ -1839,7 +1847,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
description: Platform.OS === "ios" ? "3.4.0-beta.1" : getVersion()
|
||||
description: getVersion()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,17 @@ import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { scale, updateSize } from "../utils/size";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
ScreenGuardModule.initSettings();
|
||||
|
||||
let isScreenGuardModuleReady = false;
|
||||
async function callScreenGuard(callback: () => void) {
|
||||
try {
|
||||
if (!isScreenGuardModuleReady) {
|
||||
await ScreenGuardModule.initSettings();
|
||||
isScreenGuardModuleReady = true;
|
||||
}
|
||||
callback();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
const settings = get();
|
||||
@@ -129,15 +139,21 @@ function setPrivacyScreen(enabled?: boolean) {
|
||||
if (enabled) {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
ScreenGuardModule.registerWithoutEffect();
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.registerWithoutEffect();
|
||||
});
|
||||
} else {
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(false);
|
||||
}
|
||||
ScreenGuardModule.unregister();
|
||||
callScreenGuard(() => {
|
||||
ScreenGuardModule.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +272,8 @@ export const SettingsService = {
|
||||
canLockAppInBackground,
|
||||
appEnteredBackground,
|
||||
setPrivacyScreen,
|
||||
getBackgroundEnterTime
|
||||
getBackgroundEnterTime,
|
||||
callScreenGuard
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
@@ -104,6 +104,7 @@ export type Settings = {
|
||||
checkForUpdates?: boolean;
|
||||
defaultLineHeight: number;
|
||||
imageCompression: "ask-every-time" | "enabled" | "disabled";
|
||||
keepScreenOn?: boolean;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -213,7 +214,8 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
lastFullBackupDate: 0,
|
||||
checkForUpdates: true,
|
||||
defaultLineHeight: EDITOR_LINE_HEIGHT.DEFAULT,
|
||||
imageCompression: "ask-every-time"
|
||||
imageCompression: "ask-every-time",
|
||||
keepScreenOn: true
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
// Production iOS build identifiers
|
||||
IOS_CURRENT_PROJECT_VERSION = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
IOS_CURRENT_PROJECT_VERSION = 2189
|
||||
IOS_MARKETING_VERSION = 3.4.5
|
||||
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
|
||||
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
|
||||
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Production iOS build identifiers
|
||||
IOS_CURRENT_PROJECT_VERSION = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
IOS_CURRENT_PROJECT_VERSION = 2189
|
||||
IOS_MARKETING_VERSION = 3.4.5
|
||||
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
|
||||
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
|
||||
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Staging iOS build identifiers
|
||||
IOS_CURRENT_PROJECT_VERSION = 2186
|
||||
IOS_MARKETING_VERSION = 3.4.3
|
||||
IOS_CURRENT_PROJECT_VERSION = 2189
|
||||
IOS_MARKETING_VERSION = 3.4.5
|
||||
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
|
||||
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
|
||||
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.4.0-beta.1",
|
||||
"version": "3.4.5",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -18,7 +18,7 @@
|
||||
"e2e-android": "detox test --configuration android.emu.release --detectOpenHandles",
|
||||
"e2e-android-debug": "detox test --configuration android.emu.debug --detectOpenHandles",
|
||||
"e2e-ios": "detox test -c ios.sim.release --detectOpenHandles",
|
||||
"bump": "npx react-native bump-version --skip-semver-for android",
|
||||
"bump": "node ./scripts/bump-version.mjs",
|
||||
"release-android": "cd android && GITHUB_RELEASE=true ENVFILE=.env.public ./gradlew assembleRelease --no-daemon",
|
||||
"release-android-bundle": "cd android && ./gradlew bundleRelease --no-daemon",
|
||||
"lint": "eslint --ext .js,.jsx,.tsx,.ts ./app"
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/android/.settings/org.eclipse.buildship.core.prefs b/node_modules/@react-native-clipboard/clipboard/android/.settings/org.eclipse.buildship.core.prefs
|
||||
deleted file mode 100644
|
||||
index 1c6d50d..0000000
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/android/.settings/org.eclipse.buildship.core.prefs
|
||||
+++ /dev/null
|
||||
@@ -1,13 +0,0 @@
|
||||
-arguments=
|
||||
-auto.sync=false
|
||||
-build.scans.enabled=false
|
||||
-connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(7.4.2))
|
||||
-connection.project.dir=../example/android
|
||||
-eclipse.preferences.version=1
|
||||
-gradle.user.home=
|
||||
-java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home
|
||||
-jvm.arguments=
|
||||
-offline.mode=false
|
||||
-override.workspace.settings=true
|
||||
-show.console.view=true
|
||||
-show.executions.view=true
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/android/gradle/.settings/org.eclipse.buildship.core.prefs b/node_modules/@react-native-clipboard/clipboard/android/gradle/.settings/org.eclipse.buildship.core.prefs
|
||||
deleted file mode 100644
|
||||
index e889521..0000000
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/android/gradle/.settings/org.eclipse.buildship.core.prefs
|
||||
+++ /dev/null
|
||||
@@ -1,2 +0,0 @@
|
||||
-connection.project.dir=
|
||||
-eclipse.preferences.version=1
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/android/src/main/java/com/reactnativecommunity/clipboard/ClipboardModule.java b/node_modules/@react-native-clipboard/clipboard/android/src/main/java/com/reactnativecommunity/clipboard/ClipboardModule.java
|
||||
index b68b858..324b293 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/android/src/main/java/com/reactnativecommunity/clipboard/ClipboardModule.java
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/android/src/main/java/com/reactnativecommunity/clipboard/ClipboardModule.java
|
||||
@@ -202,6 +202,37 @@ public class ClipboardModule extends NativeClipboardModuleSpec {
|
||||
}
|
||||
}
|
||||
|
||||
+ @ReactMethod
|
||||
+ public void setHTML(String html, String text) {
|
||||
+ try {
|
||||
+ ClipData clipdata = ClipData.newHtmlText(null, text, html);
|
||||
+ ClipboardManager clipboard = getClipboardService();
|
||||
+ clipboard.setPrimaryClip(clipdata);
|
||||
+ } catch (Exception e) {
|
||||
+ e.printStackTrace();
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @ReactMethod
|
||||
+ public void getHTML(Promise promise) {
|
||||
+ try {
|
||||
+ ClipboardManager clipboard = getClipboardService();
|
||||
+ ClipData clipData = clipboard.getPrimaryClip();
|
||||
+
|
||||
+ if (clipData == null
|
||||
+ || clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)
|
||||
+ || clipData.getItemCount() < 1) {
|
||||
+ promise.resolve("");
|
||||
+ return;
|
||||
+ }
|
||||
+ ClipData.Item item = clipData.getItemAt(0);
|
||||
+ promise.resolve(item.getHtmlText());
|
||||
+
|
||||
+ } catch (Exception e) {
|
||||
+ promise.reject(e);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@ReactMethod
|
||||
public void setListener() {
|
||||
try {
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.d.ts b/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.d.ts
|
||||
index 6c85195..afe3a5d 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.d.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.d.ts
|
||||
@@ -22,6 +22,9 @@ export declare const Clipboard: {
|
||||
* ```
|
||||
*/
|
||||
getStrings(): Promise<string[]>;
|
||||
+
|
||||
+ getHTML(): Promise<string>;
|
||||
+ setHTML(html: string, content: string): void;
|
||||
/**
|
||||
* Get clipboard image as PNG in base64, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
* ```javascript
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.js b/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.js
|
||||
index ccdf309..03e7a42 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.js
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/dist/Clipboard.js
|
||||
@@ -30,6 +30,12 @@ const NativeClipboardModule_1 = __importStar(require("./NativeClipboardModule"))
|
||||
* `Clipboard` gives you an interface for setting and getting content from Clipboard on both iOS and Android
|
||||
*/
|
||||
exports.Clipboard = {
|
||||
+ getHTML() {
|
||||
+ return NativeClipboardModule_1.default.getHTML();
|
||||
+ },
|
||||
+ setHTML(html, text) {
|
||||
+ return NativeClipboardModule_1.default.setHTML(html, text);
|
||||
+ },
|
||||
/**
|
||||
* Get content of string type, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
* ```javascript
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboard.d.ts b/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboard.d.ts
|
||||
index 5440157..75f346c 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboard.d.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboard.d.ts
|
||||
@@ -2,6 +2,8 @@ import type { EmitterSubscription } from 'react-native';
|
||||
import type { TurboModule } from 'react-native';
|
||||
export interface Spec extends TurboModule {
|
||||
getConstants: () => {};
|
||||
+ setHTML(html: string, content: string): void;
|
||||
+ getHTML(): Promise<string>;
|
||||
getString: () => Promise<string>;
|
||||
getStrings: () => Promise<string[]>;
|
||||
setString: (content: string) => void;
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboardModule.d.ts b/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboardModule.d.ts
|
||||
index 0b2111e..b395af2 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboardModule.d.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/dist/NativeClipboardModule.d.ts
|
||||
@@ -20,6 +20,8 @@ export interface Spec extends TurboModule {
|
||||
* ```
|
||||
*/
|
||||
getStrings(): Promise<string[]>;
|
||||
+ setHTML(html: string, content: string): void;
|
||||
+ getHTML(): Promise<string>;
|
||||
/**
|
||||
* Get clipboard image as PNG in base64, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
* ```javascript
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/ios/RNCClipboard.mm b/node_modules/@react-native-clipboard/clipboard/ios/RNCClipboard.mm
|
||||
index 4e38fd2..5dea6a7 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/ios/RNCClipboard.mm
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/ios/RNCClipboard.mm
|
||||
@@ -87,6 +87,29 @@ - (void) listener:(NSNotification *) notification
|
||||
resolve((clipboard.strings ? : @[]));
|
||||
}
|
||||
|
||||
+RCT_EXPORT_METHOD(setHTML:(nonnull NSString *)html
|
||||
+ content:(nonnull NSString*)content
|
||||
+) {
|
||||
+ UIPasteboard *clipboard = [UIPasteboard generalPasteboard];
|
||||
+ NSDictionary *htmlDict = [NSDictionary dictionaryWithObjectsAndKeys:html, (NSString*)kUTTypeHTML, nil];
|
||||
+ NSDictionary *textDict = [NSDictionary dictionaryWithObjectsAndKeys:content, (NSString*)kUTTypePlainText, nil];
|
||||
+ clipboard.items = [NSArray arrayWithObjects:htmlDict, textDict, nil];
|
||||
+}
|
||||
+
|
||||
+RCT_EXPORT_METHOD(getHTML:(RCTPromiseResolveBlock)resolve
|
||||
+ reject:(__unused RCTPromiseRejectBlock)reject)
|
||||
+{
|
||||
+ UIPasteboard *clipboard = [UIPasteboard generalPasteboard];
|
||||
+ id value = [clipboard valueForPasteboardType:(NSString*)kUTTypeHTML];
|
||||
+ NSString *html = nil;
|
||||
+ if ([value isKindOfClass:[NSString class]]) {
|
||||
+ html = (NSString *)value;
|
||||
+ } else if ([value isKindOfClass:[NSData class]]) {
|
||||
+ html = [[NSString alloc] initWithData:(NSData *)value encoding:NSUTF8StringEncoding];
|
||||
+ }
|
||||
+ resolve((html ? : @""));
|
||||
+}
|
||||
+
|
||||
RCT_EXPORT_METHOD(setImage:(NSString *)content
|
||||
resolve:(RCTPromiseResolveBlock) resolve
|
||||
reject:(RCTPromiseRejectBlock) reject
|
||||
@@ -238,10 +261,11 @@ - (void) listener:(NSNotification *) notification
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
-RCT_EXPORT_METHOD(removeListeners : (double)count) {
|
||||
+RCT_EXPORT_METHOD(removeListeners : (long)count) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
+
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
||||
(const facebook::react::ObjCTurboModule::InitParams &)params
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/src/Clipboard.ts b/node_modules/@react-native-clipboard/clipboard/src/Clipboard.ts
|
||||
index d5e61fc..b010a22 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/src/Clipboard.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/src/Clipboard.ts
|
||||
@@ -31,6 +31,18 @@ export const Clipboard = {
|
||||
getStrings(): Promise<string[]> {
|
||||
return NativeClipboard.getStrings();
|
||||
},
|
||||
+
|
||||
+ /**
|
||||
+ * Get content of HTML type, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
+ * ```javascript
|
||||
+ * async _getContent() {
|
||||
+ * var content = await Clipboard.getHTML();
|
||||
+ * }
|
||||
+ * ```
|
||||
+ */
|
||||
+ getHTML(): Promise<string> {
|
||||
+ return NativeClipboard.getHTML();
|
||||
+ },
|
||||
/**
|
||||
* Get clipboard image as PNG in base64, this method returns a `Promise`, so you can use following code to get clipboard content
|
||||
* ```javascript
|
||||
@@ -106,6 +118,19 @@ export const Clipboard = {
|
||||
setStrings(content: string[]) {
|
||||
NativeClipboard.setStrings(content);
|
||||
},
|
||||
+ /**
|
||||
+ * Set content of HTML type. You can use following code to set clipboard content
|
||||
+ * ```javascript
|
||||
+ * _setContent() {
|
||||
+ * Clipboard.setHTML('<b>hello world</b>', 'hello world');
|
||||
+ * }
|
||||
+ * ```
|
||||
+ * @param {string} content the HTML to be stored in the clipboard.
|
||||
+ * @param {string} text text to be used inplace of HTML be stored in the clipboard.
|
||||
+ */
|
||||
+ setHTML(content: string, text: string) {
|
||||
+ NativeClipboard.setHTML(content, text);
|
||||
+ },
|
||||
/**
|
||||
* Returns whether the clipboard has content or is empty.
|
||||
* This method returns a `Promise`, so you can use following code to get clipboard content
|
||||
diff --git a/node_modules/@react-native-clipboard/clipboard/src/NativeClipboardModule.ts b/node_modules/@react-native-clipboard/clipboard/src/NativeClipboardModule.ts
|
||||
index 2373747..a094285 100644
|
||||
--- a/node_modules/@react-native-clipboard/clipboard/src/NativeClipboardModule.ts
|
||||
+++ b/node_modules/@react-native-clipboard/clipboard/src/NativeClipboardModule.ts
|
||||
@@ -78,6 +78,9 @@ export interface Spec extends TurboModule {
|
||||
* @param the content to be stored in the clipboard.
|
||||
*/
|
||||
setString(content: string): void;
|
||||
+
|
||||
+ setHTML(html: string, content: string): void;
|
||||
+ getHTML(): Promise<string>;
|
||||
/**
|
||||
* Set content of string array type. You can use following code to set clipboard content
|
||||
* ```javascript
|
||||
194
apps/mobile/scripts/bump-version.mjs
Normal file
194
apps/mobile/scripts/bump-version.mjs
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
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 { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const PROJECT_ROOT = path.resolve(
|
||||
new URL(import.meta.url).pathname,
|
||||
"..",
|
||||
".."
|
||||
); // scripts/.. -> mobile
|
||||
|
||||
const REPO_ROOT = path.resolve(PROJECT_ROOT, "..", ".."); // mobile/../.. -> repo root
|
||||
const IOS_BUILD_CONFIGS_DIR = path.resolve(PROJECT_ROOT, "ios/build-configs");
|
||||
const ANDROID_GRADLE = path.resolve(PROJECT_ROOT, "android/app/build.gradle");
|
||||
const PACKAGE_JSON = path.resolve(PROJECT_ROOT, "package.json");
|
||||
const FDROID_CHANGELOGS_DIR = path.resolve(
|
||||
REPO_ROOT,
|
||||
"fastlane/metadata/android/en-US/changelogs"
|
||||
);
|
||||
|
||||
async function resolveVersion() {
|
||||
const provided = process.argv[2];
|
||||
if (provided) {
|
||||
if (!/^\d+\.\d+\.\d+$/.test(provided)) {
|
||||
console.error(`Invalid version "${provided}". Expected format: x.y.z`);
|
||||
process.exit(1);
|
||||
}
|
||||
return provided;
|
||||
}
|
||||
// No version provided: bump the current package.json version as a patch.
|
||||
const pkg = JSON.parse(await fs.readFile(PACKAGE_JSON, "utf8"));
|
||||
const match = String(pkg.version).match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!match) {
|
||||
console.error(
|
||||
`Cannot auto-bump: package.json version "${pkg.version}" is not x.y.z`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const [, major, minor, patch] = match;
|
||||
return `${major}.${minor}.${Number(patch) + 1}`;
|
||||
}
|
||||
|
||||
const version = await resolveVersion();
|
||||
|
||||
async function updatePackageJson() {
|
||||
const raw = await fs.readFile(PACKAGE_JSON, "utf8");
|
||||
const pkg = JSON.parse(raw);
|
||||
const from = pkg.version;
|
||||
pkg.version = version;
|
||||
// Preserve trailing newline if present.
|
||||
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
||||
await fs.writeFile(
|
||||
PACKAGE_JSON,
|
||||
JSON.stringify(pkg, null, 2) + trailingNewline
|
||||
);
|
||||
console.log(`package.json: version ${from} -> ${version}`);
|
||||
}
|
||||
|
||||
async function updateAndroid() {
|
||||
let content = await fs.readFile(ANDROID_GRADLE, "utf8");
|
||||
const match = content.match(/versionCode\s+(\d+)/);
|
||||
if (!match) {
|
||||
throw new Error("Could not find `versionCode <number>` in build.gradle");
|
||||
}
|
||||
const current = parseInt(match[1], 10);
|
||||
const next = current + 1;
|
||||
content = content.replace(match[0], `versionCode ${next}`);
|
||||
await fs.writeFile(ANDROID_GRADLE, content);
|
||||
console.log(`android/app/build.gradle: versionCode ${current} -> ${next}`);
|
||||
return next;
|
||||
}
|
||||
|
||||
// Derives the F-Droid release version code from build.gradle. F-Droid builds a
|
||||
// separate APK per ABI (see `applicationVariants.all` in build.gradle) using:
|
||||
// output.versionCodeOverride = defaultConfig.versionCode * <mult> + versionCodes[abi]
|
||||
// and publishes the highest of those. So the changelog file is named after
|
||||
// `versionCode * <mult> + max(versionCodes)`. Both are parsed here rather than
|
||||
// hardcoded so this stays in sync with the formula in build.gradle.
|
||||
async function computeFdroidVersionCode(versionCode) {
|
||||
const gradle = await fs.readFile(ANDROID_GRADLE, "utf8");
|
||||
|
||||
const multMatch = gradle.match(/defaultConfig\.versionCode\s*\*\s*(\d+)/);
|
||||
if (!multMatch) {
|
||||
throw new Error(
|
||||
"Could not find the F-Droid versionCode multiplier in build.gradle"
|
||||
);
|
||||
}
|
||||
const multiplier = parseInt(multMatch[1], 10);
|
||||
|
||||
const mapMatch = gradle.match(/versionCodes\s*=\s*\[([^\]]*)\]/);
|
||||
if (!mapMatch) {
|
||||
throw new Error("Could not find the ABI versionCodes map in build.gradle");
|
||||
}
|
||||
const abiCodes = [...mapMatch[1].matchAll(/:\s*(\d+)/g)].map((m) =>
|
||||
parseInt(m[1], 10)
|
||||
);
|
||||
if (abiCodes.length === 0) {
|
||||
throw new Error("ABI versionCodes map in build.gradle is empty");
|
||||
}
|
||||
const maxAbiCode = Math.max(...abiCodes);
|
||||
|
||||
return versionCode * multiplier + maxAbiCode;
|
||||
}
|
||||
|
||||
async function createFdroidChangelog(versionCode) {
|
||||
const fdroidVersionCode = await computeFdroidVersionCode(versionCode);
|
||||
const file = path.resolve(FDROID_CHANGELOGS_DIR, `${fdroidVersionCode}.txt`);
|
||||
const rel = path.relative(REPO_ROOT, file);
|
||||
|
||||
try {
|
||||
await fs.access(file);
|
||||
console.log(`${rel}: already exists, skipped`);
|
||||
return;
|
||||
} catch {
|
||||
// File does not exist yet — create it.
|
||||
}
|
||||
|
||||
const placeholder = "- Minor bug fixes and improvements\n\nThank you for using Notesnook!\n";
|
||||
await fs.mkdir(FDROID_CHANGELOGS_DIR, { recursive: true });
|
||||
await fs.writeFile(file, placeholder);
|
||||
console.log(`${rel}: created (edit with this release's changelog)`);
|
||||
}
|
||||
|
||||
async function updateIosConfig(file) {
|
||||
let content = await fs.readFile(file, "utf8");
|
||||
const rel = path.relative(PROJECT_ROOT, file);
|
||||
|
||||
const buildMatch = content.match(/IOS_CURRENT_PROJECT_VERSION\s*=\s*(\d+)/);
|
||||
if (!buildMatch) {
|
||||
throw new Error(`Could not find IOS_CURRENT_PROJECT_VERSION in ${rel}`);
|
||||
}
|
||||
const currentBuild = parseInt(buildMatch[1], 10);
|
||||
const nextBuild = currentBuild + 1;
|
||||
content = content.replace(
|
||||
buildMatch[0],
|
||||
`IOS_CURRENT_PROJECT_VERSION = ${nextBuild}`
|
||||
);
|
||||
|
||||
const marketingMatch = content.match(/IOS_MARKETING_VERSION\s*=\s*(\S+)/);
|
||||
if (!marketingMatch) {
|
||||
throw new Error(`Could not find IOS_MARKETING_VERSION in ${rel}`);
|
||||
}
|
||||
const currentMarketing = marketingMatch[1];
|
||||
content = content.replace(
|
||||
marketingMatch[0],
|
||||
`IOS_MARKETING_VERSION = ${version}`
|
||||
);
|
||||
|
||||
await fs.writeFile(file, content);
|
||||
console.log(
|
||||
`${rel}: version ${currentMarketing} -> ${version}, build ${currentBuild} -> ${nextBuild}`
|
||||
);
|
||||
}
|
||||
|
||||
async function updateIos() {
|
||||
const entries = await fs.readdir(IOS_BUILD_CONFIGS_DIR);
|
||||
const configs = entries.filter((e) => e.endsWith(".xcconfig"));
|
||||
if (configs.length === 0) {
|
||||
throw new Error("No .xcconfig files found in ios/build-configs");
|
||||
}
|
||||
for (const config of configs) {
|
||||
await updateIosConfig(path.resolve(IOS_BUILD_CONFIGS_DIR, config));
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log(`Bumping to version ${version}...\n`);
|
||||
await updatePackageJson();
|
||||
const androidVersionCode = await updateAndroid();
|
||||
await updateIos();
|
||||
await createFdroidChangelog(androidVersionCode);
|
||||
console.log("\nDone.");
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error("Failed:", err?.message || err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/vericrypt",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/platform": "^1.3.6",
|
||||
@@ -43,4 +43,4 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import { PasteEncryptedData, SyncRequestBody } from "./components/step-4";
|
||||
import { StepSeperator } from "./components/step-seperator";
|
||||
import { Footer } from "./components/footer";
|
||||
import { useState } from "react";
|
||||
import { NNCrypto } from "@notesnook/crypto";
|
||||
import { NNCrypto, Cipher } from "@notesnook/crypto";
|
||||
import { Code } from "./components/code";
|
||||
import { Accordion } from "./components/accordion";
|
||||
import { DecryptedResult } from "./components/decrypted-result";
|
||||
@@ -41,7 +41,7 @@ const instructions = [
|
||||
"Go to Notesnook",
|
||||
"Open Settings",
|
||||
<>
|
||||
Click on <Code text="Backup data recovery key" />
|
||||
Click on <Code text="Save data recovery key" />
|
||||
</>,
|
||||
"Enter your account password for verification",
|
||||
"Confirm that your generated encryption key matches"
|
||||
@@ -52,6 +52,7 @@ function App() {
|
||||
const [salt, setSalt] = useState<string>();
|
||||
const [key, setKey] = useState<string>();
|
||||
const [data, setData] = useState<SyncRequestBody | undefined>();
|
||||
const [accountDataKey, setAccountDataKey] = useState<Cipher<"base64"> | null | undefined>();
|
||||
const theme = useTheme({ accent: getDefaultAccentColor(), theme: "light" });
|
||||
|
||||
return (
|
||||
@@ -67,7 +68,7 @@ function App() {
|
||||
>
|
||||
<LoginToNotesnook />
|
||||
<StepSeperator />
|
||||
<GetAccountSalt onSaltSubmitted={setSalt} />
|
||||
<GetAccountSalt onSaltSubmitted={setSalt} setAccountDataKey={setAccountDataKey} />
|
||||
|
||||
{salt && (
|
||||
<>
|
||||
@@ -91,7 +92,7 @@ function App() {
|
||||
return true;
|
||||
}}
|
||||
popup={{
|
||||
title: "Your data encryption key",
|
||||
title: "Your master encryption key",
|
||||
body: key ? (
|
||||
<>
|
||||
<Text
|
||||
@@ -146,11 +147,13 @@ function App() {
|
||||
password={password}
|
||||
salt={salt}
|
||||
data={data}
|
||||
accountKey={accountDataKey}
|
||||
onRestartProcess={() => {
|
||||
setSalt(undefined);
|
||||
setPassword(undefined);
|
||||
setKey(undefined);
|
||||
setData(undefined);
|
||||
setAccountDataKey(undefined);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
BIN
apps/vericrypt/src/assets/screenshots/devtools_copy_users.png
Normal file
BIN
apps/vericrypt/src/assets/screenshots/devtools_copy_users.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Flex, Button, Text, Link } from "@theme-ui/components";
|
||||
import { StepContainer } from "./step-container";
|
||||
import { SyncRequestBody } from "./step-4";
|
||||
import { NNCrypto } from "@notesnook/crypto";
|
||||
import { NNCrypto, Cipher } from "@notesnook/crypto";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FcDataEncryption } from "react-icons/fc";
|
||||
import { Code } from "./code";
|
||||
@@ -32,6 +32,7 @@ type DecryptedResultProps = {
|
||||
password: string;
|
||||
salt: string;
|
||||
data: SyncRequestBody;
|
||||
accountKey: Cipher<"base64"> | null | undefined;
|
||||
onRestartProcess: () => void;
|
||||
};
|
||||
|
||||
@@ -51,10 +52,19 @@ export function DecryptedResult(props: DecryptedResultProps) {
|
||||
};
|
||||
const crypto = new NNCrypto();
|
||||
const key = await crypto.exportKey(props.password, props.salt);
|
||||
let encryptionKey = undefined;
|
||||
if (props.accountKey != null){
|
||||
// Need to decrypt the account key
|
||||
props.accountKey.format = "base64"; // account keys don't have the format set, but they're base64.
|
||||
const dataEncryptionKey = JSON.parse(await crypto.decrypt(key, props.accountKey, "text"))
|
||||
encryptionKey = dataEncryptionKey;
|
||||
} else {
|
||||
encryptionKey = key;
|
||||
}
|
||||
for (const arrayKey in data) {
|
||||
const array = data[arrayKey];
|
||||
for (const encryptedItem of (props.data as any)[arrayKey]) {
|
||||
const data = await crypto.decrypt(key, encryptedItem, "text");
|
||||
const data = await crypto.decrypt(encryptionKey, encryptedItem, "text");
|
||||
array.push(JSON.parse(data));
|
||||
}
|
||||
}
|
||||
@@ -150,19 +160,9 @@ export function DecryptedResult(props: DecryptedResultProps) {
|
||||
support@streetwriters.co
|
||||
</Link>{" "}
|
||||
or{" "}
|
||||
<Link href="https://discord.gg/">joining our Discord community</Link>.
|
||||
<Link href="https://go.notesnook.com/discord">join our Discord community</Link>.
|
||||
We'll do our best to alleviate all your worries.
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mx: 2, mt: 2, fontWeight: "bold" }}>
|
||||
What about open sourcing Notesnook?
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mx: 2, my: 2 }}>
|
||||
Open sourcing is another part of garnering our users' trust. We
|
||||
have <Link href="https://notesnook.com/roadmap">plans</Link> to begin
|
||||
open sourcing in May but open sourcing will not make this tool
|
||||
obsolete. Verifying the integrity of encrypted data at any point in
|
||||
time is very important even if the software is open source.
|
||||
</Text>
|
||||
</Accordion>
|
||||
<Flex sx={{ alignSelf: "center", mt: 4 }}>
|
||||
<Button
|
||||
|
||||
@@ -38,11 +38,6 @@ export function LoginToNotesnook() {
|
||||
>
|
||||
<Text variant="title">Welcome to Vericrypt</Text>
|
||||
<Text as="p" variant="body" sx={{ mt: 1 }}>
|
||||
<del>
|
||||
Trust is a huge problem in closed source end-to-end encrypted
|
||||
applications. How can you be sure that the app is actually encrypting
|
||||
your data?
|
||||
</del>
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mt: 1 }}>
|
||||
The only way to earn a user's trust is by allowing them to see how
|
||||
@@ -56,9 +51,10 @@ export function LoginToNotesnook() {
|
||||
</Link>
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mt: 1 }}>
|
||||
Yes, that's right. Notesnook is now 100% open source under the
|
||||
GPLv3 license. That includes the app, the encryption library, the
|
||||
backend server, and everything else.
|
||||
Yes, that's right. Notesnook is 100% open source under the
|
||||
GPLv3 license. That includes the app, the encryption library, and
|
||||
everything else. We've even open sourced the server, available under the
|
||||
AGPLv3 license.
|
||||
</Text>
|
||||
<Text as="p" variant="body" sx={{ mt: 1 }}>
|
||||
However, even with an open source app, how can you be sure that the app
|
||||
@@ -86,7 +82,7 @@ export function LoginToNotesnook() {
|
||||
sx={{ alignSelf: "center", mt: 2 }}
|
||||
onClick={() => window.open("https://app.notesnook.com/login", "_blank")}
|
||||
>
|
||||
Login to Notesnook
|
||||
Get Started by Logging in
|
||||
</Button>
|
||||
</StepContainer>
|
||||
);
|
||||
|
||||
@@ -16,11 +16,11 @@ 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 { Flex, Text, Image, Input } from "@theme-ui/components";
|
||||
import { Flex, Text, Image, Textarea } from "@theme-ui/components";
|
||||
import { StepContainer } from "./step-container";
|
||||
import DevtoolsCopySaltChrome from "../assets/screenshots/devtools_copy_salt.png";
|
||||
import DevtoolsCopyUsersChrome from "../assets/screenshots/devtools_copy_users.png";
|
||||
|
||||
import DevtoolsCopySaltFirefox from "../assets/screenshots/firefox/firefox_copy_salt.png";
|
||||
import DevtoolsCopyUsersFirefox from "../assets/screenshots/firefox/firefox_copy_users.png";
|
||||
|
||||
import { Accordion } from "./accordion";
|
||||
import { getCombo } from "../utils/keycombos";
|
||||
@@ -29,9 +29,11 @@ import { KeyCombo } from "./key-combo";
|
||||
import { Code } from "./code";
|
||||
import { useState } from "react";
|
||||
import { getSourceUrl } from "../utils/links";
|
||||
import { Cipher } from "@notesnook/crypto";
|
||||
|
||||
type GetAccountSaltProps = {
|
||||
onSaltSubmitted: (salt: string) => void;
|
||||
setAccountDataKey: (accountDataKey: Cipher<"base64"> | null) => void;
|
||||
};
|
||||
|
||||
const steps = {
|
||||
@@ -52,10 +54,10 @@ const steps = {
|
||||
</Text>,
|
||||
<Flex key="key-named-salt" sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Follow the steps as shown in the image:</Text>
|
||||
<Image src={DevtoolsCopySaltChrome} width={"100%"} sx={{ mt: 1 }} />
|
||||
<Image src={DevtoolsCopyUsersChrome} width={"100%"} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<Flex key="copy-salt" sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Copy the salt and paste it below.</Text>
|
||||
<Text as="p">Copy everything in the response and paste it below.</Text>
|
||||
</Flex>
|
||||
],
|
||||
firefox: [
|
||||
@@ -75,10 +77,10 @@ const steps = {
|
||||
</Text>,
|
||||
<Flex key="key-named-salt" sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Follow the steps as shown in the image:</Text>
|
||||
<Image src={DevtoolsCopySaltFirefox} width={"100%"} sx={{ mt: 1 }} />
|
||||
<Image src={DevtoolsCopyUsersFirefox} width={"100%"} sx={{ mt: 1 }} />
|
||||
</Flex>,
|
||||
<Flex key="copy-salt" sx={{ flexDirection: "column" }}>
|
||||
<Text as="p">Copy the salt and paste it below.</Text>
|
||||
<Text as="p">Copy everything in the response and paste it below.</Text>
|
||||
</Flex>
|
||||
]
|
||||
};
|
||||
@@ -114,14 +116,14 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
|
||||
sx={{ flexDirection: "column" }}
|
||||
>
|
||||
<Flex sx={{ justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text variant="title">Account salt</Text>
|
||||
<Text variant="title">Account Key Data</Text>
|
||||
<Code
|
||||
text="src/components/step-2.tsx"
|
||||
href={getSourceUrl("src/components/step-2.tsx")}
|
||||
/>
|
||||
</Flex>
|
||||
<Accordion
|
||||
title="How to get your account salt?"
|
||||
title="How to get your key data?"
|
||||
sx={{
|
||||
border: "1px solid var(--border)",
|
||||
mt: 2,
|
||||
@@ -129,9 +131,9 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
|
||||
}}
|
||||
>
|
||||
<Text variant="body" sx={{ mx: 2 }}>
|
||||
We'll be extracting your account's salt right from
|
||||
You'll be extracting your account's keys right from
|
||||
Notesnook's local database that lives in your web browser. So put
|
||||
on your seat belt and let's get some salt!
|
||||
on your seat belt and let's retrieve those keys.
|
||||
</Text>
|
||||
<Text as="ol" variant="body" sx={{ mb: 2 }}>
|
||||
{instructions?.map((item, index) => (
|
||||
@@ -141,12 +143,11 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
|
||||
))}
|
||||
</Text>
|
||||
</Accordion>
|
||||
<Input
|
||||
<Textarea
|
||||
variant="forms.clean"
|
||||
id="salt"
|
||||
name="salt"
|
||||
type="text"
|
||||
placeholder="Enter your account salt"
|
||||
id="account-data"
|
||||
name="account-data"
|
||||
placeholder="Paste the response here"
|
||||
sx={{
|
||||
mt: 2,
|
||||
fontSize: "subheading",
|
||||
@@ -162,13 +163,21 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
|
||||
spellCheck={false}
|
||||
onChange={(e) => {
|
||||
setIsSaltValid(undefined);
|
||||
/*
|
||||
If there is no dataEncryptionKey, try reading the legacyDataEncryptionKey.
|
||||
If there is a legacyDataEncryptionKey, use this one.
|
||||
If there is no legacyDataEncryptionKey, it is a legacy account (pre DEK update) that has not
|
||||
changed passwords. (Use master key instead.)
|
||||
*/
|
||||
try {
|
||||
const value = e.target.value;
|
||||
const isValid = Buffer.from(value, "base64").length === 16;
|
||||
const value = JSON.parse(e.target.value);
|
||||
props.setAccountDataKey(value.dataEncryptionKey ?? value.legacyDataEncryptionKey ?? null);
|
||||
const salt = value.salt;
|
||||
const isValid = Buffer.from(salt, "base64").length === 16;
|
||||
if (!isValid) setIsSaltValid(false);
|
||||
else {
|
||||
setIsSaltValid(true);
|
||||
props.onSaltSubmitted(value);
|
||||
props.onSaltSubmitted(salt);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
987
apps/web/package-lock.json
generated
987
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.4.0-beta.1",
|
||||
"version": "3.4.4",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@lingui/react": "5.1.2",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/react": "1.6.1",
|
||||
"@notesnook-importer/core": "^2.2.5",
|
||||
"@notesnook-importer/core": "^2.4.5",
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
@@ -61,6 +61,7 @@
|
||||
"katex": "0.16.11",
|
||||
"mac-scrollbar": "0.13.6",
|
||||
"mutative": "^1.1.0",
|
||||
"p-queue": "^9.3.0",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"phone": "^3.1.14",
|
||||
"platform": "^1.3.6",
|
||||
|
||||
@@ -64,6 +64,11 @@ function attachListeners() {
|
||||
attachListener(AppEvents.updateError)
|
||||
);
|
||||
|
||||
desktop.bridge.onOpenLink.subscribe(
|
||||
undefined,
|
||||
attachListener(AppEvents.onOpenLink)
|
||||
);
|
||||
|
||||
TaskScheduler.register("updateCheck", "0 0 */12 * * * *", () => {
|
||||
checkForUpdate(settingStore.get().autoUpdates);
|
||||
});
|
||||
|
||||
@@ -142,7 +142,13 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
|
||||
<Flex sx={{ flexDirection: "column" }} p={4} pb={0}>
|
||||
{props.title && (
|
||||
<Flex
|
||||
sx={{ alignItems: "center", justifyContent: "space-between" }}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent:
|
||||
props.textAlignment === "center"
|
||||
? "center"
|
||||
: "space-between"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="heading"
|
||||
|
||||
@@ -589,7 +589,7 @@ export function Editor(props: EditorProps) {
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment)
|
||||
throw new Error("No attachment found with hash: " + hash);
|
||||
if (attachment.mimeType.startsWith("image/")) {
|
||||
if (attachment.mimeType.startsWith("image/") || type === "image") {
|
||||
await previewImageAttachment(attachment);
|
||||
} else if (
|
||||
onPreviewDocument &&
|
||||
@@ -633,7 +633,7 @@ export function Editor(props: EditorProps) {
|
||||
await insertAttachments(mime, (attachments) => {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
if (!editor) return;
|
||||
attachments.forEach((a) => editor?.attachFile(a));
|
||||
editor?.attachFiles(...attachments);
|
||||
});
|
||||
}}
|
||||
onGetAttachmentData={async (attachment) => {
|
||||
@@ -662,7 +662,7 @@ export function Editor(props: EditorProps) {
|
||||
onDone: (attachments) => {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
if (!editor) return;
|
||||
attachments.forEach((a) => editor?.attachFile(a));
|
||||
editor?.attachFiles(...attachments);
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -825,7 +825,7 @@ function DropZone(props: DropZoneProps) {
|
||||
.getState()
|
||||
.getEditor(activeEditorId)?.editor;
|
||||
if (!editor) return;
|
||||
attachments.forEach((a) => editor?.attachFile(a));
|
||||
editor?.attachFiles(...attachments);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -27,10 +27,10 @@ import {
|
||||
hashStream,
|
||||
writeEncryptedFile
|
||||
} from "../../interfaces/fs";
|
||||
import { compressImage, FileWithURI } from "../../utils/image-compressor";
|
||||
import { checkFeature } from "../../common";
|
||||
import { AttachFilesDialog } from "../../dialogs/attach-files-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import Queue from "p-queue";
|
||||
|
||||
export async function insertAttachments(
|
||||
type: string,
|
||||
@@ -81,7 +81,6 @@ export async function reuploadAttachment(
|
||||
}
|
||||
|
||||
type AttachFilesMessage =
|
||||
| { type: "compressing"; index: number }
|
||||
| { type: "encrypting"; index: number }
|
||||
| {
|
||||
type: "done";
|
||||
@@ -90,60 +89,39 @@ type AttachFilesMessage =
|
||||
}
|
||||
| { type: "error"; index: number; error: string };
|
||||
|
||||
export async function* attachFiles(
|
||||
export async function attachFiles(
|
||||
files: File[],
|
||||
shouldCompress: boolean[],
|
||||
report: (progress: AttachFilesMessage) => void,
|
||||
skipSpecialImageHandling = false
|
||||
): AsyncGenerator<AttachFilesMessage> {
|
||||
) {
|
||||
const queue = new Queue({ concurrency: 8 });
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
const shouldCompressFile = shouldCompress[i];
|
||||
queue.add(async () => {
|
||||
const file = files[i];
|
||||
|
||||
report({ type: "encrypting", index: i });
|
||||
|
||||
if (shouldCompressFile) {
|
||||
yield { type: "compressing", index: i };
|
||||
try {
|
||||
const compressed = await compressImage(file, {
|
||||
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
|
||||
width: (naturalWidth) => naturalWidth,
|
||||
height: (_, naturalHeight) => naturalHeight,
|
||||
resize: "contain",
|
||||
quality: 0.7
|
||||
});
|
||||
file = new FileWithURI([compressed], file.name, {
|
||||
lastModified: file.lastModified,
|
||||
type: file.type
|
||||
const allowed = await checkFeature("fileSize", {
|
||||
value: file.size,
|
||||
type: "toast"
|
||||
});
|
||||
if (!allowed) {
|
||||
throw new Error(strings.fileSizeLimitExceededPleaseUpgrade());
|
||||
}
|
||||
|
||||
const attachment =
|
||||
!skipSpecialImageHandling && file.type.startsWith("image/")
|
||||
? await pickImage(file)
|
||||
: await pickFile(file);
|
||||
|
||||
report({ type: "done", index: i, attachment: attachment || undefined });
|
||||
} catch (e) {
|
||||
yield {
|
||||
type: "error",
|
||||
index: i,
|
||||
error: (e as Error).message || strings.compressionFailed()
|
||||
};
|
||||
continue;
|
||||
report({ type: "error", index: i, error: (e as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
yield { type: "encrypting", index: i };
|
||||
|
||||
try {
|
||||
const allowed = await checkFeature("fileSize", {
|
||||
value: file.size,
|
||||
type: "toast"
|
||||
});
|
||||
if (!allowed) {
|
||||
throw new Error(strings.fileSizeLimitExceededPleaseUpgrade());
|
||||
}
|
||||
|
||||
const attachment =
|
||||
!skipSpecialImageHandling && file.type.startsWith("image/")
|
||||
? await pickImage(file)
|
||||
: await pickFile(file);
|
||||
|
||||
yield { type: "done", index: i, attachment: attachment || undefined };
|
||||
} catch (e) {
|
||||
yield { type: "error", index: i, error: (e as Error).message };
|
||||
}
|
||||
});
|
||||
}
|
||||
await queue.onIdle();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,6 +183,7 @@ export type AttachmentProgress = {
|
||||
type: "encrypt" | "download" | "upload";
|
||||
total: number;
|
||||
loaded: number;
|
||||
file?: File;
|
||||
};
|
||||
|
||||
type AddAttachmentOptions = {
|
||||
@@ -250,7 +229,7 @@ async function addAttachment(
|
||||
hash,
|
||||
hashType,
|
||||
filename: exists?.filename || file.name,
|
||||
mimeType: exists?.type || file.type,
|
||||
mimeType: exists?.mimeType || file.type,
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
@@ -684,6 +684,12 @@ function TiptapWrapper(
|
||||
}
|
||||
}, [props.spellcheck]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorContainerRef.current) {
|
||||
editorContainerRef.current.style.fontSize = `${editorConfig.fontSize}px`;
|
||||
}
|
||||
}, [editorConfig.fontSize]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={containerRef}
|
||||
@@ -786,10 +792,8 @@ function toIEditor(editor: Editor): IEditor {
|
||||
})
|
||||
.run();
|
||||
},
|
||||
attachFile: (file: Attachment) =>
|
||||
file.type === "image"
|
||||
? editor.commands.insertImage(file)
|
||||
: editor.commands.insertAttachment(file),
|
||||
attachFiles: async (...files: Attachment[]) =>
|
||||
editor.commands.insertAttachment(...files),
|
||||
sendAttachmentProgress: (hash, progress) =>
|
||||
editor.commands.updateAttachment(
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface IEditor {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
updateContent: (content: string) => void;
|
||||
attachFile: (file: Attachment) => void;
|
||||
attachFiles: (...files: Attachment[]) => void;
|
||||
sendAttachmentProgress: (hash: string, progress: number) => void;
|
||||
startSearch: () => void;
|
||||
getContent: () => string;
|
||||
|
||||
@@ -23,16 +23,18 @@ import {
|
||||
ProviderSettings,
|
||||
transform
|
||||
} from "@notesnook-importer/core";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { formatBytes, getFormattedDate } from "@notesnook/common";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { Button, Flex, Input, Text } from "@theme-ui/components";
|
||||
import { xxhash64 } from "hash-wasm";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { importNote } from "../../../utils/importer";
|
||||
import { PromptDialog } from "../../../dialogs/prompt";
|
||||
import Accordion from "../../accordion";
|
||||
import { TransformResult } from "../types";
|
||||
import { useStore as useAppStore } from "../../../stores/app-store";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type FileProviderHandlerProps = {
|
||||
provider: IFileProvider;
|
||||
@@ -44,6 +46,12 @@ type Progress = {
|
||||
done: number;
|
||||
};
|
||||
|
||||
type LogMessage = {
|
||||
type: "info" | "error";
|
||||
text: string;
|
||||
date: number;
|
||||
};
|
||||
|
||||
export function FileProviderHandler(props: FileProviderHandlerProps) {
|
||||
const { provider, onTransformFinished } = props;
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
@@ -53,7 +61,7 @@ export function FileProviderHandler(props: FileProviderHandlerProps) {
|
||||
});
|
||||
const [totalNoteCount, setTotalNoteCount] = useState(0);
|
||||
const [_, setCounter] = useState<number>(0);
|
||||
const logs = useRef<string[]>([]);
|
||||
const logs = useRef<LogMessage[]>([]);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
setFiles((files) => {
|
||||
@@ -83,26 +91,46 @@ export function FileProviderHandler(props: FileProviderHandlerProps) {
|
||||
clear: async () => undefined,
|
||||
get: async () => [],
|
||||
write: async (data) => {
|
||||
logs.current.push(
|
||||
`[${new Date().toLocaleString()}] Pushing ${
|
||||
data.title
|
||||
} into database`
|
||||
);
|
||||
logs.current.push({
|
||||
type: "info",
|
||||
date: Date.now(),
|
||||
text: `Pushing ${data.title} into database`
|
||||
});
|
||||
|
||||
await importNote(data);
|
||||
const errors = await importNote(data);
|
||||
errors.forEach((error) =>
|
||||
logs.current.push({
|
||||
type: "error",
|
||||
date: Date.now(),
|
||||
text: `Error importing ${data.title}: ${error}`
|
||||
})
|
||||
);
|
||||
},
|
||||
iterate: async function* () {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
log: (message) => {
|
||||
logs.current.push(
|
||||
`[${new Date(message.date).toLocaleString()}] ${message.text}`
|
||||
);
|
||||
logs.current.push({
|
||||
type: "info",
|
||||
date: message.date,
|
||||
text: message.text
|
||||
});
|
||||
setCounter((s) => ++s);
|
||||
},
|
||||
reporter: () => {
|
||||
setTotalNoteCount(++totalNotes);
|
||||
},
|
||||
options: {
|
||||
colornote: {
|
||||
getPassword: async (filename: string) => {
|
||||
const password = await PromptDialog.show({
|
||||
title: strings.colorNotePasswordFor(filename),
|
||||
description: strings.colorNotPasswordForDesc()
|
||||
});
|
||||
return password || undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -163,7 +191,17 @@ export function FileProviderHandler(props: FileProviderHandlerProps) {
|
||||
>
|
||||
{logs.current.map((c, index) => (
|
||||
<>
|
||||
<span key={index.toString()}>{c}</span>
|
||||
<span
|
||||
key={index.toString()}
|
||||
style={{
|
||||
color:
|
||||
c.type === "error"
|
||||
? "var(--paragraph-error)"
|
||||
: "inherit"
|
||||
}}
|
||||
>
|
||||
[{getFormattedDate(c.date, "date-time")}] {c.text}
|
||||
</span>
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
@@ -224,6 +262,7 @@ export function FileProviderHandler(props: FileProviderHandlerProps) {
|
||||
</Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{files.length > 0 ? (
|
||||
<Accordion
|
||||
isClosed
|
||||
|
||||
@@ -496,6 +496,7 @@ export const noteMenuItems: (
|
||||
"text/html": `<a href="${url}">${note.title}</a>`,
|
||||
"text/markdown": `[${note.title}](${url})`
|
||||
});
|
||||
showToast("success", strings.linkCopied());
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -594,6 +595,7 @@ export const noteMenuItems: (
|
||||
"text/html": `<a href="${link}">${note.title}</a>`,
|
||||
"text/markdown": `[${note.title}](${link})`
|
||||
});
|
||||
showToast("success", strings.linkCopied());
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -67,8 +67,7 @@ function Toggle(props: ToggleProps) {
|
||||
sx={{
|
||||
m: 0,
|
||||
bg: isOn ? "accent" : "icon-secondary",
|
||||
flexShrink: 0,
|
||||
scale: 0.75
|
||||
flexShrink: 0
|
||||
}}
|
||||
checked={isOn}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -82,6 +82,29 @@ function PublishView(props: PublishViewProps) {
|
||||
};
|
||||
}, [note.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const monographsUpdatedEvent = db.eventManager.subscribe(
|
||||
EVENTS.monographsUpdated,
|
||||
async (ids?: string[]) => {
|
||||
if (ids && ids.length > 0 && !ids.includes(note.id)) return;
|
||||
|
||||
const m = await resolveMonograph(note.id);
|
||||
setMonograph(m);
|
||||
|
||||
// if monograph has been unpublished, reset all the fields
|
||||
if (!m) {
|
||||
setSelfDestruct(false);
|
||||
if (titleInput.current) titleInput.current.value = note.title;
|
||||
if (passwordInput.current) passwordInput.current.value = "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
monographsUpdatedEvent.unsubscribe();
|
||||
};
|
||||
}, [note.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{monograph?.id ? (
|
||||
@@ -171,7 +194,7 @@ function PublishView(props: PublishViewProps) {
|
||||
</Text>
|
||||
</Flex>
|
||||
) : null}
|
||||
{monograph?.id ? (
|
||||
{monograph?.id && !selfDestruct ? (
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
@@ -242,8 +265,7 @@ function PublishView(props: PublishViewProps) {
|
||||
sx={{
|
||||
m: 0,
|
||||
bg: selfDestruct ? "accent" : "icon-secondary",
|
||||
flexShrink: 0,
|
||||
scale: 0.75
|
||||
flexShrink: 0
|
||||
}}
|
||||
checked={selfDestruct}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -115,7 +115,7 @@ export function UnlockView(props: UnlockViewProps) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isWrong && <ErrorText error="Wrong password" />}
|
||||
{isWrong && <ErrorText sx={{ mt: 1 }} error="Wrong password" />}
|
||||
<Button
|
||||
mt={3}
|
||||
variant="accent"
|
||||
|
||||
@@ -17,10 +17,9 @@ 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 { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Dialog from "../components/dialog";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { Box, Flex, Image, Switch, Text } from "@theme-ui/components";
|
||||
import { Flex, Image, Switch, Text } from "@theme-ui/components";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -32,10 +31,11 @@ import { ImageCompressionOptions } from "../stores/setting-store";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
import {
|
||||
CheckCircle,
|
||||
Loading,
|
||||
File as FileIcon,
|
||||
CloseCircle
|
||||
} from "../components/icons";
|
||||
import { compressImage } from "../utils/image-compressor";
|
||||
import Queue from "p-queue";
|
||||
|
||||
type FileStatus = "pending" | "compressing" | "encrypting" | "done" | "error";
|
||||
|
||||
@@ -45,6 +45,7 @@ type FileState = {
|
||||
progress: number;
|
||||
error?: string;
|
||||
compress: boolean;
|
||||
compressedFile?: File;
|
||||
};
|
||||
|
||||
type AttachFilesDialogProps = BaseDialogProps<false> & {
|
||||
@@ -66,40 +67,46 @@ export const AttachFilesDialog = DialogManager.register(
|
||||
ImageCompressionOptions.ASK_EVERY_TIME
|
||||
);
|
||||
const [fileStates, setFileStates] = useState<FileState[]>(() =>
|
||||
files.map((file) => ({
|
||||
file,
|
||||
status: "pending",
|
||||
progress: 0,
|
||||
compress: file.type.startsWith("image/")
|
||||
? imageCompressionConfig !== ImageCompressionOptions.DISABLE
|
||||
: false
|
||||
}))
|
||||
files
|
||||
.sort((a, b) => {
|
||||
const aIsImage = a.type.startsWith("image/");
|
||||
const bIsImage = b.type.startsWith("image/");
|
||||
if (aIsImage && !bIsImage) return -1;
|
||||
if (!aIsImage && bIsImage) return 1;
|
||||
return a.type.localeCompare(b.type);
|
||||
})
|
||||
.map((file) => ({
|
||||
file,
|
||||
status: "pending",
|
||||
progress: 0,
|
||||
compress: file.type.startsWith("image/")
|
||||
? imageCompressionConfig !== ImageCompressionOptions.DISABLE
|
||||
: false
|
||||
}))
|
||||
);
|
||||
const [showCompressionPrompt, setShowCompressionPrompt] = useState(
|
||||
const isCompressionOptional =
|
||||
hasImages &&
|
||||
imageCompressionConfig === ImageCompressionOptions.ASK_EVERY_TIME
|
||||
imageCompressionConfig === ImageCompressionOptions.ASK_EVERY_TIME;
|
||||
const isProcessing = fileStates.some(
|
||||
(f) => f.status === "compressing" || f.status === "encrypting"
|
||||
);
|
||||
const isDone = fileStates.every(
|
||||
(f) => f.status === "done" || f.status === "error"
|
||||
);
|
||||
const processingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const event = AppEventManager.subscribe(
|
||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||
({ type, total, loaded }: AttachmentProgress) => {
|
||||
({ type, total, loaded, file }: AttachmentProgress) => {
|
||||
if (type !== "encrypt") return;
|
||||
|
||||
setFileStates((prev) =>
|
||||
prev.map((s) => {
|
||||
/**
|
||||
* only one file is encrypted at a time, so we can just update progress of the state with "encrypting" status
|
||||
*/
|
||||
if (s.status !== "encrypting") return s;
|
||||
|
||||
return {
|
||||
...s,
|
||||
progress: Math.round((loaded / total) * 100)
|
||||
};
|
||||
})
|
||||
);
|
||||
setFileStates((prev) => {
|
||||
const index = prev.findIndex((s) => s.file === file);
|
||||
if (index === -1) return prev;
|
||||
return updateFileStates(prev, index, {
|
||||
progress: Math.round((loaded / total) * 100)
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
@@ -107,133 +114,166 @@ export const AttachFilesDialog = DialogManager.register(
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCompressionPrompt || processingRef.current) return;
|
||||
const compressFiles = useCallback(async () => {
|
||||
setFileStates((prev) =>
|
||||
prev.map((s) => ({
|
||||
...s,
|
||||
status: s.compressedFile || !s.compress ? s.status : "compressing"
|
||||
}))
|
||||
);
|
||||
|
||||
processingRef.current = true;
|
||||
const queue = new Queue({ concurrency: 8 });
|
||||
|
||||
const shouldCompress: boolean[] = fileStates.map((s) => !!s.compress);
|
||||
for (const [index, state] of fileStates.entries()) {
|
||||
if (state.compressedFile || !state.compress) continue;
|
||||
|
||||
(async () => {
|
||||
const attachments: Attachment[] = [];
|
||||
let hasError = false;
|
||||
await queue.add(async () => {
|
||||
try {
|
||||
const compressed = await compressImage(state.file, {
|
||||
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
|
||||
width: (naturalWidth) => naturalWidth,
|
||||
height: (_, naturalHeight) => naturalHeight,
|
||||
resize: "contain",
|
||||
quality: 0.7
|
||||
});
|
||||
const compressedFile = new File([compressed], state.file.name, {
|
||||
lastModified: state.file.lastModified,
|
||||
type: state.file.type
|
||||
});
|
||||
setFileStates((prev) =>
|
||||
updateFileStates(prev, index, {
|
||||
status: "pending",
|
||||
compressedFile
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
const error = (e as Error).message || strings.compressionFailed();
|
||||
setFileStates((prev) =>
|
||||
updateFileStates(prev, index, { status: "error", error })
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for await (const message of attachFiles(
|
||||
files,
|
||||
shouldCompress,
|
||||
skipSpecialImageHandling
|
||||
)) {
|
||||
const { index } = message;
|
||||
await queue.onIdle();
|
||||
}, [fileStates]);
|
||||
|
||||
const processFiles = useCallback(async () => {
|
||||
const attachments: Attachment[] = [];
|
||||
const validIndices = fileStates
|
||||
.map((s, i) => (s.status === "error" ? -1 : i))
|
||||
.filter((i) => i !== -1);
|
||||
const filesToAttach = validIndices.map((i) => {
|
||||
const state = fileStates[i];
|
||||
return state.compress && state.compressedFile
|
||||
? state.compressedFile
|
||||
: state.file;
|
||||
});
|
||||
|
||||
await attachFiles(
|
||||
filesToAttach,
|
||||
(message) => {
|
||||
const index = validIndices[message.index];
|
||||
switch (message.type) {
|
||||
case "compressing":
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, i) =>
|
||||
i === index
|
||||
? { ...s, status: "compressing" as FileStatus }
|
||||
: s
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "encrypting":
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, i) =>
|
||||
i === index
|
||||
? { ...s, status: "encrypting" as FileStatus, progress: 0 }
|
||||
: s
|
||||
)
|
||||
updateFileStates(prev, index, {
|
||||
status: "encrypting",
|
||||
progress: 0
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "done":
|
||||
if (message.attachment) attachments.push(message.attachment);
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, i) =>
|
||||
i === index
|
||||
? {
|
||||
...s,
|
||||
status: "done" as FileStatus
|
||||
}
|
||||
: s
|
||||
)
|
||||
updateFileStates(prev, index, { status: "done" })
|
||||
);
|
||||
break;
|
||||
case "error":
|
||||
hasError = true;
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, i) =>
|
||||
i === index
|
||||
? {
|
||||
...s,
|
||||
status: "error" as FileStatus,
|
||||
error: message.error
|
||||
}
|
||||
: s
|
||||
)
|
||||
updateFileStates(prev, index, {
|
||||
status: "error",
|
||||
error: message.error
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
skipSpecialImageHandling
|
||||
);
|
||||
|
||||
onDone(attachments);
|
||||
if (files.length === 1 && !hasError) onClose(false);
|
||||
})();
|
||||
}, [showCompressionPrompt]);
|
||||
onDone(attachments);
|
||||
|
||||
const hasError = fileStates.some((s) => s.status === "error");
|
||||
if (fileStates.length === 1 && !hasError) onClose(false);
|
||||
}, [fileStates, onClose, onDone, skipSpecialImageHandling]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={
|
||||
showCompressionPrompt
|
||||
? strings.imageCompression()
|
||||
: strings.attachingFiles()
|
||||
}
|
||||
description={
|
||||
showCompressionPrompt ? strings.imageCompressionDesc() : ""
|
||||
}
|
||||
onClose={() => onClose(false)}
|
||||
title={strings.attachingFiles()}
|
||||
onOpen={async () => {
|
||||
await compressFiles();
|
||||
if (!isCompressionOptional) await processFiles();
|
||||
}}
|
||||
onClose={() => {
|
||||
if (isProcessing) return;
|
||||
onClose(false);
|
||||
}}
|
||||
width={500}
|
||||
positiveButton={
|
||||
showCompressionPrompt
|
||||
isCompressionOptional && !isDone
|
||||
? {
|
||||
text: strings.done(),
|
||||
onClick: () => setShowCompressionPrompt(false)
|
||||
text: strings.insert(),
|
||||
disabled: isProcessing,
|
||||
onClick: async () => {
|
||||
await processFiles();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
negativeButton={{
|
||||
text: strings.close(),
|
||||
disabled: isProcessing,
|
||||
onClick: () => onClose(false)
|
||||
}}
|
||||
>
|
||||
<ScrollContainer
|
||||
style={{
|
||||
maxHeight: 350,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative"
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
px: 1,
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
{fileStates.map((state, index) => (
|
||||
<FileRow
|
||||
key={`${state.file.name}-${index}`}
|
||||
state={state}
|
||||
showDivider={fileStates.length > 1}
|
||||
showCompressionToggle={showCompressionPrompt}
|
||||
onToggleCompress={async () => {
|
||||
if (
|
||||
!(await checkFeature("fullQualityImages", { type: "toast" }))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
<Text variant="body" sx={{ color: "paragraph-secondary", flex: 1 }}>
|
||||
{strings.name()}
|
||||
</Text>
|
||||
<Text variant="body" sx={{ color: "paragraph-secondary" }}>
|
||||
{isCompressionOptional && !isProcessing && !isDone
|
||||
? strings.compress()
|
||||
: strings.status()}
|
||||
</Text>
|
||||
</Flex>
|
||||
{fileStates.map((state, index) => (
|
||||
<FileRow
|
||||
key={`${state.file.name}-${index}`}
|
||||
state={state}
|
||||
showDivider={fileStates.length > 1}
|
||||
showCompressionToggle={isCompressionOptional}
|
||||
onToggleCompress={async () => {
|
||||
if (
|
||||
!(await checkFeature("fullQualityImages", { type: "toast" }))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFileStates((prev) =>
|
||||
prev.map((s, idx) =>
|
||||
idx === index ? { ...s, compress: !s.compress } : s
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
setFileStates((prev) =>
|
||||
updateFileStates(prev, index, { compress: !state.compress })
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -250,11 +290,12 @@ function FileRow({
|
||||
showCompressionToggle?: boolean;
|
||||
onToggleCompress?: () => void;
|
||||
}) {
|
||||
const { file, status, progress, error, compress } = state;
|
||||
const { file, status, progress, error, compress, compressedFile } = state;
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const activeFile = compress && compressedFile ? compressedFile : file;
|
||||
const thumbnail = useMemo(
|
||||
() => (isImage ? URL.createObjectURL(file) : undefined),
|
||||
[file, isImage]
|
||||
() => (isImage ? URL.createObjectURL(activeFile) : undefined),
|
||||
[activeFile, isImage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -309,28 +350,33 @@ function FileRow({
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{file.name}
|
||||
{activeFile.name}
|
||||
</Text>
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
{formatBytes(file.size)}
|
||||
{status === "compressing"
|
||||
? ` — ${strings.compressing()}...`
|
||||
: status === "encrypting"
|
||||
? ` — ${strings.encrypting()} ${progress}%`
|
||||
: status === "error"
|
||||
? ` — ${error}`
|
||||
: ""}
|
||||
{formatBytes(activeFile.size)}
|
||||
{status === "error" ? ` — ${error}` : ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex sx={{ flexShrink: 0, alignItems: "center" }}>
|
||||
{showCompressionToggle && isImage ? (
|
||||
{status === "error" ? (
|
||||
<CloseCircle size={20} color="accent-error" />
|
||||
) : status === "done" ? (
|
||||
<CheckCircle size={20} color="accent" />
|
||||
) : status === "encrypting" || status === "compressing" ? (
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
{status === "compressing"
|
||||
? `${strings.compressing()}`
|
||||
: status === "encrypting"
|
||||
? `${strings.encrypting()} ${progress}%`
|
||||
: ""}
|
||||
</Text>
|
||||
) : showCompressionToggle && isImage ? (
|
||||
<Switch
|
||||
sx={{
|
||||
m: 0,
|
||||
bg: compress ? "accent" : "icon-secondary",
|
||||
flexShrink: 0,
|
||||
scale: 0.75
|
||||
flexShrink: 0
|
||||
}}
|
||||
checked={compress}
|
||||
onChange={onToggleCompress}
|
||||
@@ -339,35 +385,18 @@ function FileRow({
|
||||
<Text variant="subBody" sx={{ color: "paragraph-secondary" }}>
|
||||
N/A
|
||||
</Text>
|
||||
) : status === "done" ? (
|
||||
<CheckCircle size={20} color="accent" />
|
||||
) : status === "error" ? (
|
||||
<CloseCircle size={20} color="accent-error" />
|
||||
) : status === "encrypting" || status === "compressing" ? (
|
||||
<Flex sx={{ alignItems: "center", gap: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 60,
|
||||
height: 4,
|
||||
bg: "border",
|
||||
borderRadius: "full",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: `${status === "compressing" ? 50 : progress}%`,
|
||||
height: "100%",
|
||||
bg: "accent",
|
||||
transition: "width 0.2s ease"
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
) : (
|
||||
<Loading size={16} color="icon" />
|
||||
)}
|
||||
) : null}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function updateFileStates(
|
||||
prev: FileState[],
|
||||
index: number,
|
||||
update: Partial<FileState>
|
||||
) {
|
||||
const clone = prev.slice();
|
||||
clone[index] = { ...clone[index], ...update };
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Text, Flex } from "@theme-ui/components";
|
||||
import { Text, Flex, Link } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { getHomeRoute, hardNavigate } from "../navigation";
|
||||
import { appVersion } from "../utils/version";
|
||||
@@ -27,8 +27,9 @@ import {
|
||||
Checkmark,
|
||||
Icon,
|
||||
Warn,
|
||||
File,
|
||||
InternalLink
|
||||
Inbox,
|
||||
LinkedTo,
|
||||
Plus
|
||||
} from "../components/icons";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
@@ -31,6 +31,7 @@ import ScrollContainer, {
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { ConfirmDialog } from "./confirm";
|
||||
|
||||
type InboxHistoryDialogProps = BaseDialogProps<boolean>;
|
||||
|
||||
@@ -105,6 +106,7 @@ function DetailsCell({ value }: { value: string }) {
|
||||
<Box
|
||||
as="pre"
|
||||
sx={{
|
||||
color: "paragraph",
|
||||
m: 0,
|
||||
fontSize: "0.72em",
|
||||
whiteSpace: "pre-wrap",
|
||||
@@ -152,7 +154,7 @@ export const InboxHistoryDialog = DialogManager.register(
|
||||
const result = usePromise(() => db.inboxItemsHistory.failed.items());
|
||||
|
||||
async function deleteItem(id: string) {
|
||||
await db.inboxItemsHistory.delete(id);
|
||||
await db.inboxItemsHistory.delete([id]);
|
||||
showToast("success", strings.itemDeleted());
|
||||
if (result.status !== "pending") {
|
||||
result.refresh();
|
||||
@@ -160,6 +162,14 @@ export const InboxHistoryDialog = DialogManager.register(
|
||||
}
|
||||
|
||||
async function deleteAll() {
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: strings.deleteAll(),
|
||||
subtitle: strings.deleteAllFailedItemsDesc(),
|
||||
positiveButtonText: strings.yes(),
|
||||
negativeButtonText: strings.no()
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
await db.inboxItemsHistory.deleteFailed();
|
||||
showToast("success", strings.allItemsDeleted());
|
||||
if (result.status !== "pending") {
|
||||
@@ -265,7 +275,17 @@ export const InboxHistoryDialog = DialogManager.register(
|
||||
<Button
|
||||
variant="icon"
|
||||
title={strings.delete()}
|
||||
onClick={() => deleteItem(item.id)}
|
||||
onClick={async () => {
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: strings.deleteItem(),
|
||||
subtitle: strings.areYouSure(),
|
||||
positiveButtonText: strings.yes(),
|
||||
negativeButtonText: strings.no()
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
sx={{ color: "accent-error", p: "2px" }}
|
||||
>
|
||||
<Trash size={16} />
|
||||
|
||||
@@ -59,6 +59,9 @@ export const AuthenticationSettings: SettingsGroup[] = [
|
||||
}
|
||||
},
|
||||
validate: async ({ oldPassword, newPassword }) => {
|
||||
const verify = await db.user.verifyPassword(oldPassword);
|
||||
if (!verify) return false;
|
||||
|
||||
try {
|
||||
if (!(await createBackup({ noVerify: true }))) return false;
|
||||
return (
|
||||
|
||||
@@ -401,10 +401,14 @@ const AddApiKeyDialog = DialogManager.register(function AddApiKeyDialog(
|
||||
setSelectedExpiry(Number(value));
|
||||
}}
|
||||
sx={{
|
||||
color: "var(--paragraph)",
|
||||
bg: "background-secondary",
|
||||
border: "1px solid",
|
||||
borderColor: "border",
|
||||
padding: "5px"
|
||||
padding: "5px",
|
||||
"& + svg": {
|
||||
fill: "var(--paragraph)"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{EXPIRY_OPTIONS.map((option) => (
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function writeEncryptedFile(
|
||||
total: file.size,
|
||||
loaded: done ? file.size : totalRead
|
||||
},
|
||||
{ type: "encrypt", hash }
|
||||
{ type: "encrypt", hash, file }
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -470,11 +470,12 @@ export async function checkUpload(
|
||||
|
||||
function reportProgress(
|
||||
ev: { total?: number; loaded?: number } | undefined,
|
||||
{ type, hash }: { type: string; hash: string }
|
||||
{ type, hash, file }: { type: string; hash: string; file?: File }
|
||||
) {
|
||||
AppEventManager.publish(AppEvents.UPDATE_ATTACHMENT_PROGRESS, {
|
||||
type,
|
||||
hash,
|
||||
file,
|
||||
total: ev?.total || 1,
|
||||
loaded: ev?.loaded || 1
|
||||
});
|
||||
|
||||
@@ -17,18 +17,6 @@ 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 class FileWithURI extends File {
|
||||
uri: string;
|
||||
constructor(
|
||||
fileBits: BlobPart[],
|
||||
fileName: string,
|
||||
options?: FilePropertyBag
|
||||
) {
|
||||
super(fileBits, fileName, options);
|
||||
this.uri = URL.createObjectURL(this);
|
||||
}
|
||||
}
|
||||
|
||||
type DeriveDimension = (naturalWidth: number, naturalHeight: number) => number;
|
||||
|
||||
interface CompressorOptions {
|
||||
@@ -84,7 +72,7 @@ export function compressImage(file: File, options: Partial<CompressorOptions>) {
|
||||
naturalHeight: image.naturalHeight,
|
||||
naturalWidth: image.naturalWidth
|
||||
});
|
||||
resolve(compressed);
|
||||
resolve(compressed.size > file.size ? file : compressed);
|
||||
URL.revokeObjectURL(image.src);
|
||||
};
|
||||
image.onabort = () => {
|
||||
|
||||
@@ -48,11 +48,18 @@ const colorMap: Record<string, string | undefined> = {
|
||||
yellow: "#FFC107"
|
||||
};
|
||||
|
||||
export async function importNote(note: Note) {
|
||||
export async function importNote(note: Note): Promise<string[]> {
|
||||
const errors: string[] = [];
|
||||
const encryptedAttachmentFieldsMap = await processAttachments(
|
||||
note.attachments
|
||||
);
|
||||
await processNote(note, encryptedAttachmentFieldsMap);
|
||||
).catch((e) => {
|
||||
errors.push(e.message);
|
||||
return {};
|
||||
});
|
||||
await processNote(note, encryptedAttachmentFieldsMap).catch((e) => {
|
||||
errors.push(e.message);
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
async function processAttachments(attachments: Note["attachments"]) {
|
||||
@@ -114,6 +121,10 @@ async function processNote(
|
||||
|
||||
if (!noteId) return;
|
||||
|
||||
if (note.archived) {
|
||||
await db.notes.archive(true, noteId);
|
||||
}
|
||||
|
||||
for (const tag of note.tags || []) {
|
||||
const tagId =
|
||||
(await db.tags.find(tag))?.id ||
|
||||
@@ -162,6 +173,24 @@ async function processNote(
|
||||
for (const notebookId of notebookIds)
|
||||
await db.notes.addToNotebook(notebookId, noteId);
|
||||
}
|
||||
|
||||
if (note.reminder) {
|
||||
const reminderId = await db.reminders.add({
|
||||
recurringMode: note.reminder.recurringMode,
|
||||
mode: note.reminder.mode,
|
||||
selectedDays: note.reminder.selectedDays,
|
||||
date: note.reminder.date,
|
||||
title: note.reminder.title,
|
||||
description: note.reminder.description
|
||||
});
|
||||
|
||||
if (reminderId) {
|
||||
await db.relations.add(
|
||||
{ id: noteId, type: "note" },
|
||||
{ id: reminderId, type: "reminder" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function importNotebook(
|
||||
|
||||
@@ -89,7 +89,7 @@ export class WebExtensionServer implements Server {
|
||||
);
|
||||
|
||||
let attachment;
|
||||
for await (const message of attachFiles([clippedFile], [false])) {
|
||||
for await (const message of attachFiles([clippedFile])) {
|
||||
if (message.type === "done") attachment = message.attachment;
|
||||
else if (message.type === "error") return;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ On all three platforms we use the same exact library for all cryptographic funct
|
||||
|
||||
> info Fun story
|
||||
>
|
||||
> When we first added encryption, we used AES-GCM-256 across platforms but the cross-platform compatbility was abyssmal. That is when I found out about the great libsodium. Written in C, wrappers available for all platforms...what more could I want?
|
||||
> When we first added encryption, we used AES-GCM-256 across platforms but the cross-platform compatibility was abyssmal. That is when I found out about the great libsodium. Written in C, wrappers available for all platforms...what more could I want?
|
||||
|
||||
## Process
|
||||
|
||||
@@ -41,13 +41,7 @@ This process is repeated every time you sign in.
|
||||
|
||||
### 2. Key generation
|
||||
|
||||
After you are signed in, the app requests your user data which includes, among other things, your salt.
|
||||
|
||||
> info Salt generation
|
||||
>
|
||||
> When you create an account, the server generates a cryptographically secure random salt for you. This salt is used for key generation.
|
||||
|
||||
You password & salt is then used to derive a strong irreversible key using Argon2 as the password key derivation function (PKDF).
|
||||
When you first sign up for an account, your client generates two encryption keys. One is a unique data encryption key that encrypts all your notes and other data. The second is your master encryption key, this is derived by your password and predictable salt. This key protects all your encryption keys, like the aforementioned data encryption key. If you change your password, your client will re-encrypt your existing data encryption key with your new master key.
|
||||
|
||||
### 3. Encryption key storage
|
||||
|
||||
@@ -55,7 +49,7 @@ You password & salt is then used to derive a strong irreversible key using Argon
|
||||
|
||||
Instead of storing the key as plain text (and allowing anyone to copy/move it), we use browser's `IndexedDB` to store the key as a `CryptoKey`.
|
||||
|
||||
`CryptoKey` is stored securely by the browser and cannot be exported, viewed, copied except by the app & browser.
|
||||
`CryptoKey` is stored securely by the browser and cannot be exported, viewed, or copied except by the app & browser.
|
||||
|
||||
# [Mobile](#/tab/mobile)
|
||||
|
||||
@@ -65,12 +59,12 @@ On iOS and Android, the encryption key is stored in the phone's keychain.
|
||||
|
||||
### 4. Data encryption
|
||||
|
||||
Encryption only takes place when you sync. Each item in the database is encrypted seperately using XChaCha-Poly1305-IETF.
|
||||
Encryption only takes place when you sync. Each item in the database is encrypted separately using XChaCha-Poly1305-IETF.
|
||||
|
||||
#### How it works
|
||||
|
||||
1. The item is read from the database as JSON object and stringified (i.e. converted to a string).
|
||||
2. The string is encrypted using the encryption key generated earlier.
|
||||
2. The string is encrypted using the data encryption key generated earlier.
|
||||
3. The result is a JSON object which contains:
|
||||
1. A base64 encoded `cipher`
|
||||
2. A 192-bit nonce (`iv`)
|
||||
@@ -83,3 +77,9 @@ Encryption only takes place when you sync. Each item in the database is encrypte
|
||||
> See the whole process in action [here.](https://vericrypt.notesnook.com/)
|
||||
|
||||
This object is then sent to the server for storage. The server performs no further operation on this data (because it can't).
|
||||
|
||||
## Faqs
|
||||
|
||||
### I am an old user of Notesnook, I don't have a data encryption key.
|
||||
|
||||
Your data encryption key will be created when you change your password.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: ColorNote
|
||||
---
|
||||
|
||||
# How to import notes from ColorNote notes app?
|
||||
|
||||
The following steps will help you import your notes from ColorNote easily.
|
||||
|
||||
1. Download the ColorNote mobile app.
|
||||
2. Go to `Settings > Backup` and create a backup. Make sure to remember the password you set while creating the backup as it will be needed during import.
|
||||
3. Open the Notesnook app (web or desktop).
|
||||
4. Go to `Settings > Notesnook Importer` and select `ColorNote` from list of apps.
|
||||
5. Drop the backup file you exported earlier in the box or click anywhere to open system file picker to select the backup.
|
||||
7. Click on "Start importing" and enter the password you used while creating the backup in the popup.
|
||||
8. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, [report it on github](https://github.com/streetwriters/notesnook).
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: UpNote
|
||||
---
|
||||
|
||||
# How to import notes from UpNote notes app?
|
||||
|
||||
The following steps will help you import your notes from UpNote easily.
|
||||
|
||||
1. Download the UpNote desktop app.
|
||||
2. Click on settings icon on the header.
|
||||
3. Go to `General` tab in Settings
|
||||
4. Click on `Export All Notes`.
|
||||
5. Export from the `Export to HTML` option to download your notes.
|
||||
6. Create a `.zip` file of the exported folder.
|
||||
7. Open the Notesnook app (web or desktop).
|
||||
8. Go to `Settings > Notesnook Importer` and select `UpNote` from list of apps.
|
||||
9. Drop the `.zip` file you exported earlier in the box or click anywhere to open system file picker to select the backup and click "Start processing".
|
||||
10. Once importing completes you should see all your notes in Notesnook. If you face any issues during importing, [report it on github](https://github.com/streetwriters/notesnook).
|
||||
|
||||
## Supported formats
|
||||
|
||||
UpNote's export is a folder with HTML files for each note and a folder for attachments. Make sure to create a `.zip` of the exported folder before importing to Notesnook. The Notesnook Importer preserves all formatting, images, and attachments in the imported notes.
|
||||
@@ -3,10 +3,6 @@ title: Getting Started
|
||||
description: Learn about Notesnook's Inbox API.
|
||||
---
|
||||
|
||||
> warn Beta feature
|
||||
>
|
||||
> Inbox API is still in beta so expect things to change and/or break. If you find any issues, kindly open an issue on our [GitHub repo](https://github.com/streetwriters/notesnook).
|
||||
|
||||
# Getting started with the Inbox API
|
||||
|
||||
Think of the Inbox API as a private mailbox for your Notesnook account. You give other apps or services a key to drop notes into it, and those notes show up in your Notesnook account the next time you sync — fully encrypted, readable only by you.
|
||||
|
||||
@@ -3,10 +3,6 @@ title: Self-Hosting Inbox API
|
||||
description: Learn about self-hosting Notesnook's Inbox API.
|
||||
---
|
||||
|
||||
> warn Beta feature
|
||||
>
|
||||
> Inbox API is still in beta so expect things to change and/or break. If you find any issues, kindly open an issue on our [GitHub repo](https://github.com/streetwriters/notesnook).
|
||||
|
||||
# Self-Hosting Inbox API
|
||||
|
||||
If you prefer not to send data through Notesnook's hosted inbox instance, you have two options:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> error This feature is Android only.
|
||||
|
||||
A simple, quick and convinient way to take notes on your phone from notifications. This works the same way as a messaging/chat app allows you to reply to messages from notifications.
|
||||
A simple, quick and convenient way to take notes on your phone from notifications. This works the same way as a messaging/chat app allows you to reply to messages from notifications.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Basic web clipping support is available on both Android & iOS apps via share extensions.
|
||||
|
||||
We are going to use browser app as an example to demonstarte how you can clip information from webpages and other apps on your iPhone.
|
||||
We are going to use browser app as an example to demonstrate how you can clip information from webpages and other apps on your iPhone.
|
||||
|
||||
1. Select some text on a webpage then Tap on "Share" on the context menu.
|
||||
|
||||
@@ -40,7 +40,7 @@ We are going to use browser app as an example to demonstarte how you can clip in
|
||||
|
||||
> info
|
||||
>
|
||||
> Share exenstion will save the note you selected so in future web clips, it will be selected by default until you reset it.
|
||||
> Share extension will save the note you selected so in future web clips, it will be selected by default until you reset it.
|
||||
|
||||
## Clipping full webpage content
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ The first step to recovering your account consists of requesting an account reco
|
||||
5. On the next page, your email should be prefilled. If it isn't, fill it out.
|
||||

|
||||
6. Click on `Send recovery email`
|
||||
7. If everything goes well, you should recieve an email from Notesnook in your inbox:
|
||||
7. If everything goes well, you should receive an email from Notesnook in your inbox:
|
||||

|
||||
> info What if I didn't receive an email?
|
||||
>
|
||||
|
||||
@@ -42,6 +42,8 @@ navigation:
|
||||
- path: importing-notes/import-notes-from-html-files.md
|
||||
- path: importing-notes/import-notes-from-markdown-files.md
|
||||
- path: importing-notes/import-notes-from-plaintext-files.md
|
||||
- path: importing-notes/import-notes-from-colornote.md
|
||||
- path: importing-notes/import-notes-from-upnote.md
|
||||
- path: export-notes-from-notesnook.md
|
||||
- path: backup-and-restore-notes-in-notesnook.md
|
||||
|
||||
|
||||
7
fastlane/metadata/android/en-US/changelogs/15549.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/15549.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
- Inbox API is now in beta. Learn more about it at https://help.notesnook.com/inbox-api/getting-started
|
||||
- Added close all tabs button in editor
|
||||
- Deeplinking support for nn:// urls
|
||||
- Set default image compression mode in Settings -> Behavior
|
||||
- Minor bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
7
fastlane/metadata/android/en-US/changelogs/15554.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/15554.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
- Inbox API is now in beta. Learn more about it at https://help.notesnook.com/inbox-api/getting-started
|
||||
- Added close all tabs button in editor
|
||||
- Deeplinking support for nn:// urls
|
||||
- Set default image compression mode in Settings -> Behavior
|
||||
- Minor bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15559.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15559.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
133
packages/core/__tests__/sql-migrations.test.ts
Normal file
133
packages/core/__tests__/sql-migrations.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
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 {
|
||||
Kysely,
|
||||
Migration,
|
||||
MigrationProvider,
|
||||
SqliteDialect,
|
||||
sql
|
||||
} from "@streetwriters/kysely";
|
||||
import BetterSQLite3 from "better-sqlite3-multiple-ciphers";
|
||||
import * as betterTrigram from "sqlite-better-trigram";
|
||||
import * as fts5Html from "sqlite3-fts5-html";
|
||||
import { getLoadablePath } from "sqlite-regex";
|
||||
import { NNMigrationProvider } from "../src/database/migrations.js";
|
||||
import {
|
||||
initializeDatabase,
|
||||
SqliteBooleanPlugin
|
||||
} from "../src/database/index.js";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
function createSchemaTestDatabase() {
|
||||
const sqlite = BetterSQLite3(":memory:").unsafeMode(true);
|
||||
betterTrigram.load(sqlite);
|
||||
fts5Html.load(sqlite);
|
||||
sqlite.loadExtension(getLoadablePath());
|
||||
|
||||
return new Kysely({
|
||||
dialect: new SqliteDialect({
|
||||
database: sqlite
|
||||
}),
|
||||
plugins: [new SqliteBooleanPlugin()]
|
||||
});
|
||||
}
|
||||
|
||||
async function createMigrationProviderBefore(migrationName: string) {
|
||||
const migrations = await new NNMigrationProvider().getMigrations();
|
||||
const entries = Object.entries(migrations);
|
||||
const index = entries.findIndex(([name]) => name === migrationName);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error(`Migration not found: ${migrationName}`);
|
||||
}
|
||||
|
||||
return {
|
||||
migrations,
|
||||
provider: {
|
||||
async getMigrations() {
|
||||
return Object.fromEntries(entries.slice(0, index));
|
||||
}
|
||||
} satisfies MigrationProvider
|
||||
};
|
||||
}
|
||||
|
||||
describe("Schema migration recovery", () => {
|
||||
test("replays a partially applied spellcheck migration", async () => {
|
||||
const db = createSchemaTestDatabase();
|
||||
|
||||
try {
|
||||
const { migrations, provider } = await createMigrationProviderBefore(
|
||||
"a-2026-04-06"
|
||||
);
|
||||
|
||||
await initializeDatabase(db, provider, "notesnook-test");
|
||||
await migrations["a-2026-04-06"]?.up(db);
|
||||
|
||||
await initializeDatabase(db, new NNMigrationProvider(), "notesnook-test");
|
||||
|
||||
const migrationRow = await sql<{ name: string }>`
|
||||
SELECT name
|
||||
FROM kysely_migration
|
||||
WHERE name = ${"a-2026-04-06"}
|
||||
`.execute(db);
|
||||
const column = await sql<{ name: string }>`
|
||||
SELECT name
|
||||
FROM pragma_table_info(${"notes"})
|
||||
WHERE name = ${"spellcheck"}
|
||||
`.execute(db);
|
||||
|
||||
expect(migrationRow.rows).toHaveLength(1);
|
||||
expect(column.rows).toHaveLength(1);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test("replays a partially applied inbox items history migration", async () => {
|
||||
const db = createSchemaTestDatabase();
|
||||
|
||||
try {
|
||||
const { migrations, provider } = await createMigrationProviderBefore(
|
||||
"a-2026-05-07"
|
||||
);
|
||||
|
||||
await initializeDatabase(db, provider, "notesnook-test");
|
||||
await migrations["a-2026-05-07"]?.up(db);
|
||||
|
||||
await initializeDatabase(db, new NNMigrationProvider(), "notesnook-test");
|
||||
|
||||
const migrationRow = await sql<{ name: string }>`
|
||||
SELECT name
|
||||
FROM kysely_migration
|
||||
WHERE name = ${"a-2026-05-07"}
|
||||
`.execute(db);
|
||||
const table = await sql<{ name: string }>`
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name = ${"inboxitemshistory"}
|
||||
`.execute(db);
|
||||
|
||||
expect(migrationRow.rows).toHaveLength(1);
|
||||
expect(table.rows).toHaveLength(1);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
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 { Debug } from "../debug.ts";
|
||||
import createFetchMock from "vitest-fetch-mock";
|
||||
import { vi, test, expect } from "vitest";
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
|
||||
const SUCCESS_REPORT_RESPONSE = {
|
||||
url: "https://reported/"
|
||||
};
|
||||
|
||||
test("reporting issue should return issue url", async () => {
|
||||
fetchMocker.enableMocks();
|
||||
|
||||
fetch.mockResponseOnce(JSON.stringify(SUCCESS_REPORT_RESPONSE), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
|
||||
expect(
|
||||
await Debug.report({
|
||||
title: "I am title",
|
||||
body: "I am body",
|
||||
userId: "anything"
|
||||
})
|
||||
).toBe(SUCCESS_REPORT_RESPONSE.url);
|
||||
|
||||
fetchMocker.disableMocks();
|
||||
});
|
||||
|
||||
test("reporting invalid issue should return undefined", async () => {
|
||||
fetchMocker.enableMocks();
|
||||
|
||||
fetch.mockResponseOnce(
|
||||
JSON.stringify({
|
||||
error_description: "Invalid issue."
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
expect(await Debug.report({})).toBeUndefined();
|
||||
|
||||
fetchMocker.disableMocks();
|
||||
});
|
||||
@@ -300,6 +300,7 @@ export class Sync {
|
||||
throw new Error(
|
||||
`Failed to send all items. Sent ${done} out of ${total}.`
|
||||
);
|
||||
if (total === 0) return false;
|
||||
await this.connection?.send("PushCompletedV2", deviceId);
|
||||
return true;
|
||||
}
|
||||
@@ -560,15 +561,18 @@ export class Sync {
|
||||
return true;
|
||||
});
|
||||
|
||||
this.connection.on("SendMonographs", async (monographs) => {
|
||||
this.connection.on("SendMonographs", async (monographs: Monograph[]) => {
|
||||
if (this.connection?.state !== HubConnectionState.Connected) return false;
|
||||
|
||||
this.db.monographsCollection.collection.put(
|
||||
monographs.map((m: Monograph) => ({
|
||||
const ids = monographs.map((m) => m.id);
|
||||
await this.db.monographsCollection.collection.put(
|
||||
monographs.map((m) => ({
|
||||
...m,
|
||||
type: "monograph"
|
||||
}))
|
||||
);
|
||||
await this.db.monographs.refresh().catch(this.logger.error);
|
||||
this.db.eventManager.publish(EVENTS.monographsUpdated, ids);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -632,7 +632,10 @@ class UserManager {
|
||||
throw new Error("Incorrect old password.");
|
||||
|
||||
const oldPassword = old_password
|
||||
? await this.db.storage().hash(old_password, email, {
|
||||
? // we don't lowercase email here to allow user accounts with
|
||||
// mixed cased emails to change their passwords. Once that is done,
|
||||
// we will lowercase the email in the backend.
|
||||
await this.db.storage().hash(old_password, email, {
|
||||
usesFallback: await this.usesFallbackPWHash(old_password)
|
||||
})
|
||||
: null;
|
||||
@@ -696,7 +699,9 @@ class UserManager {
|
||||
`${constants.API_HOST}/users/password/${type}`,
|
||||
{
|
||||
oldPassword: oldPassword,
|
||||
newPassword: await this.db.storage().hash(new_password, email),
|
||||
newPassword: await this.db
|
||||
.storage()
|
||||
.hash(new_password, email.toLowerCase()),
|
||||
userKeys: updateUserPayload
|
||||
},
|
||||
token
|
||||
|
||||
@@ -40,14 +40,17 @@ export default class Vault {
|
||||
}
|
||||
|
||||
private set password(value) {
|
||||
this.vaultPassword = value;
|
||||
if (this.vaultPassword !== value) {
|
||||
this.vaultPassword = value;
|
||||
if (value) this.db.eventManager.publish(EVENTS.vaultUnlocked);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
this.startEraser();
|
||||
}
|
||||
}
|
||||
|
||||
private startEraser() {
|
||||
this.db.eventManager.publish(EVENTS.vaultUnlocked);
|
||||
clearTimeout(this.erasureTimeout);
|
||||
|
||||
const lockAfter = this.db.settings.getVaultLockAfter();
|
||||
|
||||
@@ -67,8 +67,8 @@ export class InboxItemsHistory implements ICollection {
|
||||
);
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
await this.collection.softDelete([id]);
|
||||
async delete(ids: string[]) {
|
||||
await this.collection.softDelete(ids);
|
||||
}
|
||||
|
||||
async deleteFailed() {
|
||||
|
||||
@@ -437,6 +437,7 @@ export class Notes implements ICollection {
|
||||
const duplicateId = await this.db.notes.add({
|
||||
...clone(note),
|
||||
id: undefined,
|
||||
pinned: false,
|
||||
isGeneratedTitle: false,
|
||||
contentId: undefined,
|
||||
title: note.title + " (Copy)",
|
||||
@@ -489,6 +490,7 @@ export class Notes implements ICollection {
|
||||
await this.db.relations.unlinkOfType("note", ids);
|
||||
await this.collection.softDelete(ids);
|
||||
await this.db.content.removeByNoteId(...ids);
|
||||
await this.db.inboxItemsHistory.delete(ids);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ export const EVENTS = {
|
||||
fileUploaded: "file:uploaded",
|
||||
attachmentDeleted: "attachment:deleted",
|
||||
mediaAttachmentDownloaded: "attachments:mediaDownloaded",
|
||||
monographsUpdated: "monographs:updated",
|
||||
vaultLocked: "vault:locked",
|
||||
vaultUnlocked: "vault:unlocked",
|
||||
systemTimeInvalid: "system:invalidTime",
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
sql
|
||||
} from "@streetwriters/kysely";
|
||||
import { rebuildSearchIndex } from "./fts.js";
|
||||
import type { RawDatabaseSchema } from "./index.js";
|
||||
|
||||
const COLLATE_NOCASE: ColumnBuilderCallback = (col) =>
|
||||
col.modifyEnd(sql`collate nocase`);
|
||||
@@ -389,18 +390,22 @@ export class NNMigrationProvider implements MigrationProvider {
|
||||
},
|
||||
"8": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("isGeneratedTitle", "boolean")
|
||||
.execute();
|
||||
await ensureColumn(db, "notes", "isGeneratedTitle", async () => {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("isGeneratedTitle", "boolean")
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("archived", "boolean")
|
||||
.execute();
|
||||
await ensureColumn(db, "notes", "archived", async () => {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("archived", "boolean")
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
},
|
||||
// changing the migrations name scheme from here because
|
||||
@@ -434,10 +439,12 @@ export class NNMigrationProvider implements MigrationProvider {
|
||||
},
|
||||
"a-2026-01-07": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("expiryDate", "text")
|
||||
.execute();
|
||||
await ensureColumn(db, "notes", "expiryDate", async () => {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("expiryDate", "text")
|
||||
.execute();
|
||||
});
|
||||
await db.schema
|
||||
.createIndex("note_expiry_date")
|
||||
.ifNotExists()
|
||||
@@ -448,32 +455,39 @@ export class NNMigrationProvider implements MigrationProvider {
|
||||
},
|
||||
"a-2026-01-09": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.alterTable("sessioncontent")
|
||||
.addColumn("title", "text")
|
||||
.execute();
|
||||
await ensureColumn(db, "sessioncontent", "title", async () => {
|
||||
await db.schema
|
||||
.alterTable("sessioncontent")
|
||||
.addColumn("title", "text")
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
},
|
||||
"a-2026-02-11": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.alterTable("monographs")
|
||||
.addColumn("publishUrl", "text", COLLATE_NOCASE)
|
||||
.execute();
|
||||
await ensureColumn(db, "monographs", "publishUrl", async () => {
|
||||
await db.schema
|
||||
.alterTable("monographs")
|
||||
.addColumn("publishUrl", "text", COLLATE_NOCASE)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
},
|
||||
"a-2026-04-06": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("spellcheck", "boolean", (c) => c.defaultTo(true))
|
||||
.execute();
|
||||
await ensureColumn(db, "notes", "spellcheck", async () => {
|
||||
await db.schema
|
||||
.alterTable("notes")
|
||||
.addColumn("spellcheck", "boolean", (c) => c.defaultTo(true))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
},
|
||||
"a-2026-05-07": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.createTable("inboxitemshistory")
|
||||
.ifNotExists()
|
||||
.modifyEnd(sql`without rowid`)
|
||||
.$call(addBaseColumns)
|
||||
.addColumn("dateSynced", "integer")
|
||||
@@ -568,3 +582,27 @@ async function runFTSTablesMigrations(db: Kysely<any>) {
|
||||
});
|
||||
await rebuildSearchIndex(db);
|
||||
}
|
||||
|
||||
async function hasColumn(
|
||||
db: Kysely<RawDatabaseSchema>,
|
||||
tableName: string,
|
||||
columnName: string
|
||||
) {
|
||||
const result = await sql<{ name: string }>`
|
||||
SELECT name
|
||||
FROM pragma_table_info(${tableName})
|
||||
WHERE name = ${columnName}
|
||||
`.execute(db);
|
||||
|
||||
return result.rows.length > 0;
|
||||
}
|
||||
|
||||
async function ensureColumn(
|
||||
db: Kysely<RawDatabaseSchema>,
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
callback: () => Promise<void>
|
||||
) {
|
||||
if (await hasColumn(db, tableName, columnName)) return;
|
||||
await callback();
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ setTimeout(() => {
|
||||
globalThis.logger("info", ...arguments);
|
||||
};
|
||||
}
|
||||
}, 100);
|
||||
}, 1000);
|
||||
let appLoaded = false;
|
||||
function loadApp() {
|
||||
if (appLoaded) return;
|
||||
|
||||
26
packages/editor/package-lock.json
generated
26
packages/editor/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook-importer/core": "^2.2.5",
|
||||
"@notesnook-importer/core": "^2.4.5",
|
||||
"@notesnook/common": "file:../common",
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
@@ -980,14 +980,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@notesnook-importer/core": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/core/-/core-2.2.5.tgz",
|
||||
"integrity": "sha512-S2ie0kgNG/PGdmCF81auvVX0+cvUekjBRzqyT7AYwYi6/z1JN+yGY9S8b1UVscOpm33T7/q6w9FphepEsDXNAA==",
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/core/-/core-2.4.5.tgz",
|
||||
"integrity": "sha512-7Oqkw07CykwC/xzGrC6St4C/87QmXqK8UtmSxwE05fpz8DWoKEviJSWFa5fKY52umTphGvmtY23252dZ9J9EJg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook-importer/enex": "file:../enex",
|
||||
"@notesnook-importer/storage": "file:../storage",
|
||||
"@notesnook-importer/znel": "file:../znel",
|
||||
"@notesnook-importer/enex": "^2.3.5",
|
||||
"@notesnook-importer/storage": "^2.3.5",
|
||||
"@notesnook-importer/znel": "^2.3.5",
|
||||
"@stablelib/chacha20poly1305": "^1.0.1",
|
||||
"@streamparser/json": "^0.0.10",
|
||||
"@zip.js/zip.js": "^2.7.32",
|
||||
@@ -1028,7 +1028,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@notesnook-importer/enex": {
|
||||
"version": "2.2.3",
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/enex/-/enex-2.3.5.tgz",
|
||||
"integrity": "sha512-0+08XVhsQQFoEUrar3k07DuTluNho/FHxJ25n/DT+ug8kVraHHVqI40a2P9h8bW8iWxYs02Zq1n+N2uTv5qb+g==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -1041,11 +1043,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@notesnook-importer/storage": {
|
||||
"version": "2.2.3",
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/storage/-/storage-2.3.5.tgz",
|
||||
"integrity": "sha512-orvSK0XIcqlmiWmjQ3RzS3qZp7viBdCpAloX557Ml200LMSMGDsOtofYNv5IYllBETnqApERLPQaUzBSuviuvA==",
|
||||
"license": "GPL-3.0-or-later"
|
||||
},
|
||||
"node_modules/@notesnook-importer/znel": {
|
||||
"version": "2.2.3",
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook-importer/znel/-/znel-2.3.5.tgz",
|
||||
"integrity": "sha512-s1kTA3EQ5OswBc0RvWnWgIm6/mIWvnh3iWmcOJcApIqhDwQziEkYiS+UMfuY9nOUyIWtepMe/WJ0Odk15uah9w==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook-importer/core": "^2.2.5",
|
||||
"@notesnook-importer/core": "^2.4.5",
|
||||
"@notesnook/common": "file:../common",
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
|
||||
@@ -25,6 +25,8 @@ import { Attachment } from "./types.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { hasPermission } from "../../types.js";
|
||||
import { AudioNode } from "../audio/audio.js";
|
||||
import { ImageNode } from "../image/image.js";
|
||||
import { WebClipNode } from "../web-clip/web-clip.js";
|
||||
|
||||
export type AttachmentType = "image" | "file" | "camera";
|
||||
export interface AttachmentOptions {
|
||||
@@ -35,7 +37,7 @@ export interface AttachmentOptions {
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
attachment: {
|
||||
insertAttachment: (attachment: Attachment) => ReturnType;
|
||||
insertAttachment: (...attachment: Attachment[]) => ReturnType;
|
||||
removeAttachment: () => ReturnType;
|
||||
updateAttachment: (
|
||||
attachment: Partial<Attachment>,
|
||||
@@ -110,27 +112,41 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
addCommands() {
|
||||
return {
|
||||
insertAttachment:
|
||||
(attachment) =>
|
||||
(...attachments) =>
|
||||
({ commands, state }) => {
|
||||
if (!hasPermission("insertAttachment")) {
|
||||
if (!hasPermission("insertAttachment") || attachments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { $from } = state.selection;
|
||||
const maybeAttachmentNode = state.doc.nodeAt($from.pos);
|
||||
if (maybeAttachmentNode?.type === this.type) {
|
||||
const selectedNode = state.doc.nodeAt($from.pos);
|
||||
if (
|
||||
selectedNode &&
|
||||
[
|
||||
ImageNode.name,
|
||||
WebClipNode.name,
|
||||
AudioNode.name,
|
||||
this.name
|
||||
].includes(selectedNode.type.name)
|
||||
) {
|
||||
console.log(
|
||||
"Inserting attachment after the selected node",
|
||||
selectedNode
|
||||
);
|
||||
return commands.insertContentAt(
|
||||
$from.pos + maybeAttachmentNode.nodeSize,
|
||||
{
|
||||
type: mimeToExtension(attachment.mime),
|
||||
attrs: attachment
|
||||
}
|
||||
$from.pos + selectedNode.nodeSize,
|
||||
attachments.map((a) => ({
|
||||
type: mimeToExtension(a.mime),
|
||||
attrs: a
|
||||
}))
|
||||
);
|
||||
}
|
||||
return commands.insertContent({
|
||||
type: mimeToExtension(attachment.mime),
|
||||
attrs: attachment
|
||||
});
|
||||
return commands.insertContent(
|
||||
attachments.map((a) => ({
|
||||
type: mimeToExtension(a.mime),
|
||||
attrs: a
|
||||
}))
|
||||
);
|
||||
},
|
||||
removeAttachment:
|
||||
() =>
|
||||
@@ -189,7 +205,8 @@ export const AttachmentNode = Node.create<AttachmentOptions>({
|
||||
});
|
||||
|
||||
function mimeToExtension(mime: string): string {
|
||||
if (mime.startsWith("audio/")) return AudioNode.name;
|
||||
if (mime.startsWith("image/")) return ImageNode.name;
|
||||
else if (mime.startsWith("audio/")) return AudioNode.name;
|
||||
return AttachmentNode.name;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ export class ClipboardDOMParser extends ProsemirrorDOMParser {
|
||||
convertBrToSingleSpacedParagraphs(dom);
|
||||
removeRestrictedFeatures(dom);
|
||||
removeBlockId(dom);
|
||||
cleanupPastedNNLinkHtml(dom);
|
||||
}
|
||||
return super.parseSlice(dom, options);
|
||||
}
|
||||
@@ -55,6 +56,7 @@ export class ClipboardDOMParser extends ProsemirrorDOMParser {
|
||||
convertBrToSingleSpacedParagraphs(dom);
|
||||
removeRestrictedFeatures(dom);
|
||||
removeBlockId(dom);
|
||||
cleanupPastedNNLinkHtml(dom);
|
||||
}
|
||||
return super.parse(dom, options);
|
||||
}
|
||||
@@ -66,6 +68,15 @@ export function removeBlockId(dom: HTMLElement | Document) {
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupPastedNNLinkHtml(dom: HTMLElement | Document) {
|
||||
const isNNLink = dom.querySelectorAll(`[href^="nn://"]`).length === 1;
|
||||
if (isNNLink) {
|
||||
for (const element of dom.querySelectorAll("[style]")) {
|
||||
element.removeAttribute("style");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCodeblocks(dom: HTMLElement | Document) {
|
||||
for (const pre of dom.querySelectorAll("pre")) {
|
||||
pre.innerHTML = pre.innerHTML?.replaceAll(/<br.*?>/g, "\n");
|
||||
|
||||
@@ -31,7 +31,6 @@ import { createNodeView } from "../react/index.js";
|
||||
import { TextDirections } from "../text-direction/index.js";
|
||||
import { ImageComponent } from "./component.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { hasPermission } from "../../types.js";
|
||||
import { toBlob } from "../../utils/downloader.js";
|
||||
|
||||
export interface ImageOptions {
|
||||
@@ -55,7 +54,7 @@ declare module "@tiptap/core" {
|
||||
/**
|
||||
* Add an image
|
||||
*/
|
||||
insertImage: (options: Partial<ImageAttributes>) => ReturnType;
|
||||
insertImage: (...options: Partial<ImageAttributes>[]) => ReturnType;
|
||||
setImageAlignment: (options: ImageAlignmentOptions) => ReturnType;
|
||||
setImageSize: (size: ImageSize) => ReturnType;
|
||||
};
|
||||
@@ -161,25 +160,9 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
addCommands() {
|
||||
return {
|
||||
insertImage:
|
||||
(options) =>
|
||||
({ commands, state }) => {
|
||||
if (!hasPermission("insertAttachment")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { $from } = state.selection;
|
||||
const maybeImageNode = state.doc.nodeAt($from.pos);
|
||||
if (maybeImageNode?.type === this.type) {
|
||||
return commands.insertContentAt($from.pos + 1, {
|
||||
type: this.name,
|
||||
attrs: options
|
||||
});
|
||||
}
|
||||
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: options
|
||||
});
|
||||
(...images) =>
|
||||
({ commands }) => {
|
||||
return commands.insertAttachment(...(images as ImageAttachment[]));
|
||||
},
|
||||
setImageAlignment:
|
||||
(options) =>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
PasteRuleMatch
|
||||
} from "@tiptap/core";
|
||||
import { Plugin, TextSelection } from "@tiptap/pm/state";
|
||||
import { find, registerCustomProtocol, reset } from "linkifyjs";
|
||||
import { find, registerCustomProtocol } from "linkifyjs";
|
||||
import { autolink } from "./helpers/autolink.js";
|
||||
import { clickHandler } from "./helpers/clickHandler.js";
|
||||
import { pasteHandler } from "./helpers/pasteHandler.js";
|
||||
@@ -114,10 +114,6 @@ export const Link = Mark.create<LinkOptions>({
|
||||
});
|
||||
},
|
||||
|
||||
onDestroy() {
|
||||
reset();
|
||||
},
|
||||
|
||||
inclusive() {
|
||||
return this.options.autolink;
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user