mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 11:08:47 +02:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92bafbafa7 | ||
|
|
2705caf6bd | ||
|
|
8380c7d994 | ||
|
|
550c0f3854 | ||
|
|
04cd135295 | ||
|
|
e212a080c2 | ||
|
|
533abb58db | ||
|
|
941a542cc6 | ||
|
|
5bcda2cd3a | ||
|
|
869d8ba8f9 | ||
|
|
984e8f8780 | ||
|
|
750b61fb75 | ||
|
|
4a2defa300 | ||
|
|
5c9f7a8bcb | ||
|
|
b66caa9848 | ||
|
|
81cac1e54f | ||
|
|
36a99cd64f | ||
|
|
8c494642e4 | ||
|
|
7ab4088331 | ||
|
|
580af33caf | ||
|
|
bbeeacc480 | ||
|
|
51efef5005 | ||
|
|
ee030c8f61 | ||
|
|
20835e1fb3 | ||
|
|
b542680e7a | ||
|
|
fb32aedd69 | ||
|
|
56f6d34995 | ||
|
|
c68e380a0f | ||
|
|
fc5507bdd3 | ||
|
|
3f0a656a36 | ||
|
|
60667149d4 | ||
|
|
cdb3ede207 | ||
|
|
445494ce95 | ||
|
|
972f52fd01 | ||
|
|
f5e86d668f | ||
|
|
0e817236a1 | ||
|
|
0ea3b3ac2a | ||
|
|
7bfaef82d8 | ||
|
|
f51220c9a8 | ||
|
|
88e9ca721a | ||
|
|
c1ab444646 | ||
|
|
b497b53ef2 | ||
|
|
80decaae8a | ||
|
|
41197cb52c | ||
|
|
d14abcbba7 | ||
|
|
cd36fc8d4a | ||
|
|
e872487724 | ||
|
|
5e7e91caf0 | ||
|
|
a971a29ff2 | ||
|
|
a967482c4f | ||
|
|
f91ada3c48 | ||
|
|
44782d552d | ||
|
|
a51dfb4033 | ||
|
|
ccb18e9335 |
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"javascript.format.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "modificationsIfAvailable",
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"eslint.packageManager": "npm",
|
||||
"eslint.run": "onSave",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"eslint.execArgv": ["--cache"]
|
||||
"eslint.execArgv": ["--cache"],
|
||||
"git.rebaseWhenSync": true
|
||||
}
|
||||
|
||||
21
apps/desktop/package-lock.json
generated
21
apps/desktop/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.5.6",
|
||||
"version": "2.5.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -9,10 +9,10 @@
|
||||
"version": "2.5.6",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@trpc/client": "^10.29.1",
|
||||
"@trpc/server": "^10.29.1",
|
||||
"@trpc/client": "10.31.0",
|
||||
"@trpc/server": "10.31.0",
|
||||
"diary": "^0.3.1",
|
||||
"electron-trpc": "^0.5.2",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "^5.3.0",
|
||||
"icojs": "^0.17.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
@@ -22,12 +22,14 @@
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.2.13",
|
||||
"@types/node": "^18.15.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^24.4.0",
|
||||
"electron-builder": "^23.6.0",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"electron-reloader": "^1.2.3",
|
||||
"esbuild": "^0.17.19",
|
||||
"node-fetch": "^3.3.1"
|
||||
"node-fetch": "^3.3.1",
|
||||
"tree-kill": "^1.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -4982,6 +4984,15 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.5.6",
|
||||
"version": "2.5.7",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -12,10 +12,10 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "*",
|
||||
"@trpc/client": "^10.29.1",
|
||||
"@trpc/server": "^10.29.1",
|
||||
"@trpc/client": "10.31.0",
|
||||
"@trpc/server": "10.31.0",
|
||||
"diary": "^0.3.1",
|
||||
"electron-trpc": "^0.5.2",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "^5.3.0",
|
||||
"icojs": "^0.17.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
@@ -23,6 +23,8 @@
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar": "^3.5.3",
|
||||
"tree-kill": "^1.2.2",
|
||||
"@electron/rebuild": "^3.2.13",
|
||||
"@types/node": "^18.15.0",
|
||||
"electron": "^24.4.0",
|
||||
@@ -36,9 +38,9 @@
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "turbowatch scripts/dev.ts",
|
||||
"staging": "zx scripts/build.mjs --run",
|
||||
"release": "zx scripts/build.mjs",
|
||||
"start": "node scripts/dev.mjs",
|
||||
"staging": "node scripts/build.mjs --run",
|
||||
"release": "node scripts/build.mjs",
|
||||
"build": "tsc",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --minify --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",
|
||||
@@ -55,11 +57,15 @@
|
||||
"copyright": "Copyright © 2023 Streetwriters (Private) Limited",
|
||||
"artifactName": "notesnook_${os}_${arch}.${ext}",
|
||||
"npmRebuild": false,
|
||||
"asar": false,
|
||||
"files": [
|
||||
"!*.chunk.js.map",
|
||||
"!*.chunk.js.LICENSE.txt",
|
||||
"build/",
|
||||
"!build/screenshots${/*}",
|
||||
"!build/banner.jpg",
|
||||
"!build/*.ico",
|
||||
"!build/*.png",
|
||||
"!node_modules${/*}"
|
||||
],
|
||||
"afterSign": "electron-builder-notarize",
|
||||
|
||||
@@ -20,9 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
import { argv, os } from "zx";
|
||||
import yargs from "yargs-parser";
|
||||
import os from "os";
|
||||
import * as childProcess from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const args = yargs(process.argv);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const sodiumNativePrebuildPath = (arch) =>
|
||||
path.join(
|
||||
`node_modules`,
|
||||
@@ -37,7 +42,7 @@ const sodiumNativePrebuildPath = (arch) =>
|
||||
`${os.platform()}-${arch}`
|
||||
);
|
||||
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
|
||||
if (argv.rebuild) {
|
||||
if (args.rebuild) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec(`cd ${webAppPath} && npm run build:desktop`);
|
||||
@@ -48,7 +53,7 @@ if (argv.rebuild) {
|
||||
});
|
||||
}
|
||||
|
||||
if (argv.variant === "mas") {
|
||||
if (args.variant === "mas") {
|
||||
await exec(`npm run bundle:mas`);
|
||||
} else {
|
||||
await exec(`npm run bundle`);
|
||||
@@ -92,13 +97,14 @@ if (existsSync(sodiumNativePrebuildPath("arm64"))) {
|
||||
);
|
||||
}
|
||||
|
||||
if (argv.run) {
|
||||
if (args.run) {
|
||||
await exec(`npx electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`npx electron-builder --win --x64`);
|
||||
// await exec(`.\\output\\notesnook_win_x64_portable.exe`);
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
await exec(`./output/darwin-unpacked/Notesnook`);
|
||||
} else {
|
||||
await exec(`npx electron-builder --linux AppImage:x64`);
|
||||
await exec(`./output/notesnook_linux_x86_64.AppImage`);
|
||||
await exec(`./output/linux-unpacked/Notesnook`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
197
apps/desktop/scripts/dev.mjs
Normal file
197
apps/desktop/scripts/dev.mjs
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
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 path from "path";
|
||||
import fs from "fs/promises";
|
||||
import chokidar from "chokidar";
|
||||
import { execSync, spawn } from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import treekill from "tree-kill";
|
||||
import crypto from "crypto";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const sodiumNativePrebuildPath = path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`
|
||||
);
|
||||
const RUNNING_PROCESSES = [];
|
||||
const RESTARTABLE_PROCESSES = [];
|
||||
let lastBundleHash = null;
|
||||
|
||||
await onChange(true);
|
||||
|
||||
console.log("Watching...");
|
||||
|
||||
const watcher = chokidar.watch("src/", { ignoreInitial: true });
|
||||
watcher.on("all", () => {
|
||||
onChange(false);
|
||||
});
|
||||
|
||||
process.on("SIGINT", async (s) => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
}
|
||||
|
||||
await exec(`npm run bundle`);
|
||||
execAsync(`npx`, [`tsc`]);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
console.log("Restarting...", RESTARTABLE_PROCESSES.length);
|
||||
await killProcesses(RESTARTABLE_PROCESSES);
|
||||
}
|
||||
|
||||
execAsync(
|
||||
"npx",
|
||||
["electron", path.join("build", "electron.js")],
|
||||
true,
|
||||
cleanup
|
||||
);
|
||||
}
|
||||
|
||||
function spawnAndWaitUntil(cmd, cwd, predicate) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(">", ...cmd);
|
||||
|
||||
const s = spawn(cmd[0], cmd.slice(1), {
|
||||
cwd,
|
||||
env: { ...process.env, NO_COLOR: "true" },
|
||||
shell: false
|
||||
});
|
||||
|
||||
RUNNING_PROCESSES.push(s);
|
||||
|
||||
s.stderr.pipe(process.stderr);
|
||||
s.stdout.on("data", (data) => {
|
||||
process.stdout.write(data);
|
||||
if (predicate(data)) resolve(undefined); //
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function exec(cmd) {
|
||||
try {
|
||||
console.log(">", cmd);
|
||||
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: false
|
||||
});
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
function execAsync(cmd, args, restartable, onExit) {
|
||||
try {
|
||||
console.log(">", cmd, ...args);
|
||||
|
||||
const proc = spawn(cmd, args, {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: false
|
||||
});
|
||||
|
||||
const array = restartable ? RESTARTABLE_PROCESSES : RUNNING_PROCESSES;
|
||||
array.push(proc);
|
||||
proc.on("exit", (code, signal) => {
|
||||
console.log(cmd, ...args, "closed with code", code);
|
||||
if (code === 0 && !signal) {
|
||||
array.splice(array.indexOf(proc), 1);
|
||||
onExit && onExit();
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function killProcesses(processes) {
|
||||
for (const process of processes.slice()) {
|
||||
processes.splice(processes.indexOf(process), 1);
|
||||
await new Promise((resolve, reject) =>
|
||||
treekill(process.pid, (err) => (err ? reject(err) : resolve()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
console.log("Cleaning up");
|
||||
|
||||
await killProcesses(RESTARTABLE_PROCESSES);
|
||||
await killProcesses(RUNNING_PROCESSES);
|
||||
await sleep(1000);
|
||||
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
process.exit();
|
||||
}
|
||||
|
||||
async function isBundleSame() {
|
||||
const bundle = await fs.readFile(
|
||||
path.join(__dirname, "..", "build", "electron.js")
|
||||
);
|
||||
|
||||
const hashSum = crypto.createHash("sha256");
|
||||
hashSum.update(bundle);
|
||||
const hex = hashSum.digest("hex");
|
||||
|
||||
if (!lastBundleHash) {
|
||||
lastBundleHash = hex;
|
||||
return false;
|
||||
}
|
||||
if (hex === lastBundleHash) return true;
|
||||
lastBundleHash = hex;
|
||||
return false;
|
||||
}
|
||||
@@ -1,118 +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 path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { watch } from "turbowatch";
|
||||
import { execSync, spawn } from "child_process";
|
||||
|
||||
const sodiumNativePrebuildPath = path.join(
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`crypto`,
|
||||
`node_modules`,
|
||||
`@notesnook`,
|
||||
`sodium`,
|
||||
`node_modules`,
|
||||
`sodium-native`,
|
||||
`prebuilds`
|
||||
);
|
||||
let isServerRunning = false;
|
||||
|
||||
async function main() {
|
||||
const { shutdown } = await watch({
|
||||
project: path.join(__dirname, ".."),
|
||||
cwd: path.join(__dirname, ".."),
|
||||
triggers: [
|
||||
{
|
||||
expression: [
|
||||
"allof",
|
||||
["not", ["dirname", "node_modules"]],
|
||||
["match", "*.ts", "basename"]
|
||||
],
|
||||
name: "dev",
|
||||
// retry: { retries: 0 },
|
||||
onTeardown: async () => {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
},
|
||||
onChange: async ({ first }) => {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
}
|
||||
|
||||
await exec(`npm run bundle`);
|
||||
await exec(`npx tsc`);
|
||||
|
||||
if (first) {
|
||||
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!isServerRunning) {
|
||||
await spawnAndWaitUntil(
|
||||
["npm", "run", "start:desktop"],
|
||||
path.join(__dirname, "..", "..", "web"),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
isServerRunning = true;
|
||||
}
|
||||
|
||||
await exec(`npx electron ${path.join("build", "electron.js")}`);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// SIGINT is the signal sent when we press Ctrl+C
|
||||
process.once("SIGINT", () => {
|
||||
void shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
function spawnAndWaitUntil(
|
||||
cmd: string[],
|
||||
cwd: string,
|
||||
predicate: (data: string) => boolean
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
const s = spawn(cmd[0], cmd.slice(1), {
|
||||
cwd,
|
||||
env: { ...process.env, NO_COLOR: "true" }
|
||||
});
|
||||
|
||||
process.once("beforeExit", () => {
|
||||
s.kill(-1);
|
||||
});
|
||||
|
||||
s.stdout.on("data", (data) => {
|
||||
if (predicate(data)) resolve(undefined); //
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function exec(cmd) {
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit"
|
||||
});
|
||||
}
|
||||
@@ -25,10 +25,10 @@ import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
import { getTheme, setTheme, Theme } from "../utils/theme";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { platform } from "os";
|
||||
import { dirname } from "path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -122,10 +122,10 @@ export const osIntegrationRouter = t.router({
|
||||
.query(({ input }) => {
|
||||
const notification = new Notification({
|
||||
...input,
|
||||
icon: join(
|
||||
__dirname,
|
||||
platform() === "win32" ? "app.ico" : "favicon-72x72.png"
|
||||
)
|
||||
icon: AssetManager.appIcon({
|
||||
size: 64,
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
})
|
||||
});
|
||||
notification.show();
|
||||
if (input.urgency === "critical") {
|
||||
|
||||
@@ -16,9 +16,8 @@ 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/>.
|
||||
*/
|
||||
/* global MAC_APP_STORE, RELEASE */
|
||||
|
||||
import { app, BrowserWindow, nativeTheme, shell } from "electron";
|
||||
import { app, BrowserWindow, nativeTheme, session, shell } from "electron";
|
||||
import { isDevelopment } from "./utils";
|
||||
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
|
||||
import { configureAutoUpdater } from "./utils/autoupdater";
|
||||
@@ -46,8 +45,8 @@ if (process.platform == "win32" && process.env.PORTABLE_EXECUTABLE_DIR) {
|
||||
app.setPath("documents", path.join(root, "Documents"));
|
||||
app.setPath("userData", path.join(root, "UserData"));
|
||||
}
|
||||
// only run a single instance
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
app.exit();
|
||||
}
|
||||
@@ -56,22 +55,22 @@ if (process.platform === "win32") {
|
||||
app.setAppUserModelId(app.name);
|
||||
}
|
||||
|
||||
async function createWindow() {
|
||||
await AssetManager.loadIcons();
|
||||
const cliOptions = await parseArguments();
|
||||
app.commandLine.appendSwitch("lang", "en-US");
|
||||
|
||||
const mainWindowState = new WindowState({});
|
||||
async function createWindow() {
|
||||
const cliOptions = await parseArguments();
|
||||
setTheme(getTheme());
|
||||
|
||||
const mainWindowState = new WindowState({});
|
||||
const mainWindow = new BrowserWindow({
|
||||
show: false,
|
||||
show: !cliOptions.hidden,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
darkTheme: getTheme() === "dark",
|
||||
backgroundColor: getBackgroundColor(),
|
||||
|
||||
opacity: 0,
|
||||
autoHideMenuBar: true,
|
||||
icon: AssetManager.appIcon({
|
||||
size: 512,
|
||||
@@ -79,44 +78,42 @@ async function createWindow() {
|
||||
}),
|
||||
webPreferences: {
|
||||
zoomFactor: config.zoomFactor,
|
||||
devTools: true, // isDev,
|
||||
nodeIntegration: true, //true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: config.isSpellCheckerEnabled,
|
||||
preload: __dirname + "/preload.js"
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on("show", async () => {
|
||||
if (config.privacyMode) {
|
||||
await api.integration.setPrivacyMode(config.privacyMode);
|
||||
}
|
||||
});
|
||||
|
||||
mainWindowState.manage(mainWindow);
|
||||
globalThis.window = mainWindow;
|
||||
setupMenu();
|
||||
setupJumplist();
|
||||
setupDesktopIntegration(cliOptions.hidden);
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
|
||||
mainWindow.minimize();
|
||||
|
||||
setTimeout(() => {
|
||||
mainWindow.setOpacity(1);
|
||||
}, 70);
|
||||
|
||||
setupDesktopIntegration();
|
||||
createIPCHandler({ router, windows: [mainWindow] });
|
||||
|
||||
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);
|
||||
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
|
||||
if (isDevelopment())
|
||||
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
|
||||
|
||||
try {
|
||||
await mainWindow.loadURL(`${createURL(cliOptions)}`);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.error(e);
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow.once("closed", () => {
|
||||
globalThis.window = null;
|
||||
});
|
||||
|
||||
setupMenu();
|
||||
setupJumplist();
|
||||
|
||||
if (isDevelopment())
|
||||
mainWindow.webContents.openDevTools({ mode: "right", activate: true });
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
@@ -126,10 +123,14 @@ async function createWindow() {
|
||||
setupTray();
|
||||
setupJumplist();
|
||||
});
|
||||
|
||||
await AssetManager.loadIcons();
|
||||
if (config.privacyMode) {
|
||||
await api.integration.setPrivacyMode(config.privacyMode);
|
||||
}
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch("lang", "en-US");
|
||||
app.on("ready", async () => {
|
||||
app.once("ready", async () => {
|
||||
console.info("App ready. Opening window.");
|
||||
|
||||
if (!isDevelopment()) registerProtocol();
|
||||
@@ -137,7 +138,7 @@ app.on("ready", async () => {
|
||||
configureAutoUpdater();
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
app.once("window-all-closed", () => {
|
||||
if (process.platform !== "darwin" || MAC_APP_STORE) {
|
||||
app.quit();
|
||||
}
|
||||
@@ -149,9 +150,10 @@ app.on("activate", () => {
|
||||
}
|
||||
});
|
||||
|
||||
function createURL(options: CLIOptions) {
|
||||
function createURL(options: CLIOptions, path = "/") {
|
||||
const url = new URL(isDevelopment() ? "http://localhost:3000" : PROTOCOL_URL);
|
||||
|
||||
url.pathname = path;
|
||||
if (options.note === true) url.hash = "/notes/create/1";
|
||||
else if (options.notebook === true) url.hash = "/notebooks/create";
|
||||
else if (options.reminder === true) url.hash = "/reminders/create";
|
||||
@@ -163,7 +165,7 @@ function createURL(options: CLIOptions) {
|
||||
return url;
|
||||
}
|
||||
|
||||
function setupDesktopIntegration(hidden: boolean) {
|
||||
function setupDesktopIntegration() {
|
||||
const desktopIntegration = config.desktopSettings;
|
||||
|
||||
if (
|
||||
@@ -173,24 +175,13 @@ function setupDesktopIntegration(hidden: boolean) {
|
||||
setupTray();
|
||||
}
|
||||
|
||||
globalThis.window?.on("ready-to-show", () => {
|
||||
if (!hidden) {
|
||||
globalThis.window?.show();
|
||||
}
|
||||
|
||||
if (hidden && !desktopIntegration.minimizeToSystemTray) {
|
||||
globalThis.window?.show();
|
||||
globalThis.window?.minimize();
|
||||
}
|
||||
});
|
||||
|
||||
// when close to system tray is enabled, it becomes nigh impossible
|
||||
// to "quit" the app. This is necessary in order to fix that.
|
||||
if (desktopIntegration.closeToSystemTray) {
|
||||
app.on("before-quit", () => app.exit(0));
|
||||
}
|
||||
|
||||
globalThis.window?.on("close", (e) => {
|
||||
globalThis.window?.once("close", (e) => {
|
||||
if (config.desktopSettings.closeToSystemTray) {
|
||||
e.preventDefault();
|
||||
if (process.platform == "darwin") {
|
||||
|
||||
@@ -21,8 +21,8 @@ import { NativeImage, nativeImage } from "electron";
|
||||
import path from "path";
|
||||
import { isDevelopment } from "./index";
|
||||
import { parse, ParsedImage } from "icojs";
|
||||
import { readFileSync } from "fs";
|
||||
import { getSystemTheme } from "./theme";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
type Formats = "ico" | "png" | "icns";
|
||||
type IconOptions<TFormat extends Formats> = {
|
||||
@@ -70,7 +70,7 @@ export class AssetManager {
|
||||
"icons",
|
||||
`${icon}${prefix}.ico`
|
||||
);
|
||||
const icoBuffer = readFileSync(icoPath);
|
||||
const icoBuffer = await readFile(icoPath);
|
||||
const images = await parse(icoBuffer, "image/png");
|
||||
ALL_ICONS.push({ id: icon, images, prefix });
|
||||
}
|
||||
@@ -80,10 +80,10 @@ export class AssetManager {
|
||||
static appIcon(options: IconOptions<Formats>) {
|
||||
const { size = 32, format = "png" } = options;
|
||||
|
||||
if (format === "ico") return path.join("assets", "icons", "app.ico");
|
||||
if (format === "icns") return path.join("assets", "icons", "app.icns");
|
||||
if (format === "ico") return "assets\\icons\\app.ico";
|
||||
if (format === "icns") return "assets/icons/app.icns";
|
||||
|
||||
return path.join("assets", "icons", `${size}x${size}.png`);
|
||||
return `assets/icons/${size}x${size}.png`;
|
||||
}
|
||||
|
||||
static icon<TFormat extends Formats>(
|
||||
|
||||
@@ -18,14 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { join } from "path";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
export const APP_ICON_PATH = join(
|
||||
__dirname,
|
||||
process.platform === "win32" ? "app.ico" : "favicon-72x72.png"
|
||||
);
|
||||
|
||||
export function isDevelopment() {
|
||||
return process.env.ELECTRON_IS_DEV
|
||||
? Number.parseInt(process.env.ELECTRON_IS_DEV, 10) === 1
|
||||
|
||||
@@ -19,8 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { protocol, ProtocolRequest } from "electron";
|
||||
import { isDevelopment } from "./index";
|
||||
import { createReadStream, statSync } from "fs";
|
||||
import { extname, join, normalize } from "path";
|
||||
import { createReadStream } from "fs";
|
||||
import { extname, normalize } from "path";
|
||||
import { URL } from "url";
|
||||
import fetch, { Response } from "node-fetch";
|
||||
|
||||
@@ -46,14 +46,13 @@ function registerProtocol() {
|
||||
console.info("Intercepting request:", request.url);
|
||||
|
||||
const loadIndex = !extname(url.pathname);
|
||||
const absoluteFilePath = normalize(
|
||||
const filePath = normalize(
|
||||
`${__dirname}${
|
||||
loadIndex
|
||||
? `${BASE_PATH}/index.html`
|
||||
: `${BASE_PATH}/${url.pathname}`
|
||||
}`
|
||||
);
|
||||
const filePath = getPath(absoluteFilePath);
|
||||
if (!filePath) {
|
||||
console.error("Local asset file not found at", filePath);
|
||||
callback({ error: FILE_NOT_FOUND });
|
||||
@@ -159,18 +158,3 @@ async function getBody(request: ProtocolRequest) {
|
||||
return blob;
|
||||
}
|
||||
|
||||
function getPath(filePath: string): string | undefined {
|
||||
try {
|
||||
const result = statSync(filePath);
|
||||
|
||||
if (result.isFile()) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
if (result.isDirectory()) {
|
||||
return getPath(join(filePath, "index.html"));
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,38 +30,41 @@ const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.8.101:5264",
|
||||
// AUTH_HOST: "http://192.168.8.101:8264",
|
||||
// SSE_HOST: "http://192.168.8.101:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.8.101:9264",
|
||||
// ISSUES_HOST: "http://192.168.8.101:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.8.101:5264",
|
||||
// AUTH_HOST: "http://192.168.8.101:8264",
|
||||
// SSE_HOST: "http://192.168.8.101:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.8.101:9264",
|
||||
// ISSUES_HOST: "http://192.168.8.101:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
|
||||
initalize(new KV(LoggerStorage), true);
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
}
|
||||
DatabaseLogger.info(`File uploaded: ${filename}`);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}, ${text}`);
|
||||
return result;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
@@ -90,11 +90,7 @@ const AuthModal = () => {
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
height: Platform.OS === "android" ? "100%" : "100%"
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
bounces={false}
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
@@ -71,7 +71,7 @@ export const Login = ({ changeMode }) => {
|
||||
}, 5000);
|
||||
});
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
|
||||
const { width, height } = useWindowDimensions();
|
||||
useEffect(() => {
|
||||
async () => {
|
||||
setStep(LoginSteps.emailAuth);
|
||||
@@ -95,14 +95,11 @@ export const Login = ({ changeMode }) => {
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
minHeight: "100%"
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 0.8,
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.nav,
|
||||
@@ -114,7 +111,8 @@ export const Login = ({ changeMode }) => {
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%"
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -164,8 +162,7 @@ export const Login = ({ changeMode }) => {
|
||||
: "99.9%",
|
||||
backgroundColor: colors.bg,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20,
|
||||
flexGrow: 1
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
@@ -49,7 +49,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
|
||||
const { width, height } = useWindowDimensions();
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastEvent.show({
|
||||
@@ -101,13 +101,11 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
backgroundColor: colors.bg,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.nav,
|
||||
@@ -119,7 +117,8 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%"
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -163,8 +162,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.bg,
|
||||
alignSelf: "center",
|
||||
flexGrow: 0.5
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import {
|
||||
Linking,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View
|
||||
@@ -34,15 +35,16 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
|
||||
const Intro = ({ navigation }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const isTelemetryEnabled = useSettingStore(
|
||||
(state) => state.settings.telemetry
|
||||
);
|
||||
const { width } = useWindowDimensions();
|
||||
const { width, height } = useWindowDimensions();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const renderItem = React.useCallback(
|
||||
({ item }) => (
|
||||
<View
|
||||
@@ -80,7 +82,6 @@ const Intro = ({ navigation }) => {
|
||||
<View
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginBottom: 20,
|
||||
maxWidth: "90%",
|
||||
width: "100%"
|
||||
}}
|
||||
@@ -120,16 +121,14 @@ const Intro = ({ navigation }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
<ScrollView
|
||||
testID="notesnook.splashscreen"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 0.65,
|
||||
width: deviceMode !== "mobile" ? width / 2 : "100%",
|
||||
backgroundColor: colors.nav,
|
||||
marginBottom: 20,
|
||||
@@ -139,7 +138,10 @@ const Intro = ({ navigation }) => {
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
minHeight: height * 0.7
|
||||
}}
|
||||
>
|
||||
<SwiperFlatList
|
||||
@@ -184,8 +186,8 @@ const Intro = ({ navigation }) => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flex: 0.3,
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
minHeight: height * 0.3
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -242,7 +244,7 @@ const Intro = ({ navigation }) => {
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -562,20 +562,6 @@ export const useAppEvents = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadNotes = useCallback(async () => {
|
||||
if (appLocked) return;
|
||||
|
||||
setImmediate(() => {
|
||||
db.notes.init().then(() => {
|
||||
Walkthrough.init();
|
||||
initialize();
|
||||
setImmediate(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [setLoading, appLocked]);
|
||||
|
||||
const IsDatabaseMigrationRequired = useCallback(() => {
|
||||
if (!db.migrations.required() || appLocked) return false;
|
||||
|
||||
@@ -585,12 +571,13 @@ export const useAppEvents = () => {
|
||||
if (!db.isInitialized) {
|
||||
await db.init();
|
||||
}
|
||||
loadNotes();
|
||||
initialize();
|
||||
setLoading(false);
|
||||
},
|
||||
disableClosing: true
|
||||
});
|
||||
return true;
|
||||
}, [appLocked, loadNotes]);
|
||||
}, [appLocked]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
@@ -608,8 +595,10 @@ export const useAppEvents = () => {
|
||||
await db.init();
|
||||
}
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
loadNotes();
|
||||
}, [IsDatabaseMigrationRequired, loadNotes]);
|
||||
initialize();
|
||||
setLoading(false);
|
||||
Walkthrough.init();
|
||||
}, [IsDatabaseMigrationRequired]);
|
||||
|
||||
useEffect(() => {
|
||||
let sub;
|
||||
|
||||
@@ -188,7 +188,7 @@ const Tabs = React.memo(_Tabs, () => true);
|
||||
|
||||
const _NavigationStack = () => {
|
||||
const clearSelection = useSelectionStore((state) => state.clearSelection);
|
||||
|
||||
const loading = useNoteStore((state) => state.loading);
|
||||
const onStateChange = React.useCallback(() => {
|
||||
if (useSelectionStore.getState().selectionMode) {
|
||||
clearSelection(true);
|
||||
@@ -202,7 +202,7 @@ const _NavigationStack = () => {
|
||||
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
|
||||
<Tabs />
|
||||
</NavigationContainer>
|
||||
<TopicsSheet />
|
||||
{loading ? null : <TopicsSheet />}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"html-to-text": "9.0.5",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.18",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.21",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
|
||||
@@ -62,6 +62,7 @@ import { useDragState } from "../../settings/editor/state";
|
||||
import { EventTypes } from "./editor-events";
|
||||
import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { EditorEvents, editorState } from "./utils";
|
||||
import { useNoteStore } from "../../../stores/use-notes-store";
|
||||
|
||||
const publishNote = async (editor: useEditorType) => {
|
||||
const user = useUserStore.getState().user;
|
||||
@@ -130,6 +131,7 @@ export const useEditorEvents = (
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const fullscreen = useSettingStore((state) => state.fullscreen);
|
||||
const corsProxy = useSettingStore((state) => state.settings.corsProxy);
|
||||
const loading = useNoteStore((state) => state.loading);
|
||||
const [dateFormat, timeFormat] = useSettingStore((state) => [
|
||||
state.dateFormat,
|
||||
state.timeFormat
|
||||
@@ -168,6 +170,7 @@ export const useEditorEvents = (
|
||||
}, [editor.commands, editor.postMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
editor.commands.setSettings({
|
||||
deviceMode: deviceMode || "mobile",
|
||||
fullscreen: fullscreen || false,
|
||||
@@ -200,7 +203,8 @@ export const useEditorEvents = (
|
||||
defaultFontSize,
|
||||
defaultFontFamily,
|
||||
dateFormat,
|
||||
timeFormat
|
||||
timeFormat,
|
||||
loading
|
||||
]);
|
||||
|
||||
const onBackPress = useCallback(async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { Platform, ScrollView, View } from "react-native";
|
||||
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { AuthMode } from "../../components/auth";
|
||||
@@ -141,7 +141,8 @@ const AppLock = ({ route }) => {
|
||||
borderColor: deviceMode !== "mobile" ? colors.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%"
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: 180
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -188,7 +189,7 @@ const AppLock = ({ route }) => {
|
||||
)}
|
||||
|
||||
<Seperator />
|
||||
<View
|
||||
<ScrollView
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
width: DDS.isTab && welcome ? "50%" : "100%",
|
||||
@@ -203,7 +204,8 @@ const AppLock = ({ route }) => {
|
||||
onPress={async () => {
|
||||
if (
|
||||
!(await BiometicService.isBiometryAvailable()) &&
|
||||
!useUserStore.getState().user
|
||||
!useUserStore.getState().user &&
|
||||
item.value !== modes[0].value
|
||||
) {
|
||||
ToastEvent.show({
|
||||
heading: "Biometrics not enrolled",
|
||||
@@ -276,13 +278,14 @@ const AppLock = ({ route }) => {
|
||||
alignSelf: "center",
|
||||
...getElevationStyle(5),
|
||||
marginTop: 30,
|
||||
borderRadius: 100
|
||||
borderRadius: 100,
|
||||
marginBottom: 30
|
||||
}}
|
||||
type="accent"
|
||||
title="Next"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</>
|
||||
</Animated.View>
|
||||
</>
|
||||
|
||||
@@ -30,8 +30,9 @@ import dayjs from "dayjs";
|
||||
export const FontPicker = createSettingsPicker({
|
||||
getValue: () => useSettingStore.getState().settings.defaultFontFamily,
|
||||
updateValue: (item) => {
|
||||
console.log(item.id);
|
||||
SettingsService.set({
|
||||
defaultFontFamily: item
|
||||
defaultFontFamily: item.id
|
||||
});
|
||||
},
|
||||
formatValue: (item) => {
|
||||
|
||||
@@ -161,7 +161,7 @@ export const useSettingStore = create<SettingStore>((set) => ({
|
||||
settings: { ...defaultSettings },
|
||||
sheetKeyboardHandler: true,
|
||||
fullscreen: false,
|
||||
deviceMode: "mobile",
|
||||
deviceMode: null,
|
||||
dimensions: { width, height },
|
||||
appLoading: true,
|
||||
setSettings: (settings) => set({ settings }),
|
||||
|
||||
@@ -22,7 +22,6 @@ import { isImage } from "@notesnook/core/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { db } from "../common/database";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import { IOS_APPGROUPID } from "./constants";
|
||||
|
||||
export async function attachFile(uri, hash, type, filename, options) {
|
||||
@@ -57,63 +56,53 @@ export async function attachFile(uri, hash, type, filename, options) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createNotes() {
|
||||
const bundles = MMKV.getArray("shared:noteBundles") || [];
|
||||
console.log("creating notes from note bundles", bundles);
|
||||
async function createNotes(bundle) {
|
||||
const sessionId = bundle.note.sessionId;
|
||||
const id = await db.notes.add(bundle.note);
|
||||
|
||||
for (let i = 0; i < bundles.length; i++) {
|
||||
const bundle = bundles[i];
|
||||
const id = await db.notes.add(bundle.note);
|
||||
for (const item of bundle.notebooks) {
|
||||
if (item.type === "notebook") {
|
||||
db.relations.add(item, { id, type: "note" });
|
||||
} else {
|
||||
db.notes.addToNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id
|
||||
},
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of bundle.files) {
|
||||
const uri =
|
||||
Platform.OS === "ios"
|
||||
? `${file.value.replace("file://", "")}`
|
||||
: `${file.value}`;
|
||||
const hash = await Sodium.hashFile({
|
||||
uri: uri,
|
||||
type: "cache"
|
||||
});
|
||||
await attachFile(uri, hash, file.type, file.name, {
|
||||
type: "cache",
|
||||
id: id,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
});
|
||||
let content = ``;
|
||||
if (isImage(file.type)) {
|
||||
content = `<img data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" />`;
|
||||
} else {
|
||||
content = `<p><span data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" data-size="${file.size}" /></p>`;
|
||||
}
|
||||
const rawContent = await db.content.raw(
|
||||
db.notes.note(id).data?.contentId
|
||||
);
|
||||
await db.notes.add({
|
||||
id: id,
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: rawContent?.data ? rawContent?.data + content : content
|
||||
for (const item of bundle.notebooks) {
|
||||
if (item.type === "notebook") {
|
||||
db.relations.add(item, { id, type: "note" });
|
||||
} else {
|
||||
db.notes.addToNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id
|
||||
},
|
||||
sessionId: Date.now()
|
||||
});
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _bundles = [...bundles];
|
||||
_bundles.splice(i, 1);
|
||||
MMKV.setArray("shared:noteBundles", _bundles);
|
||||
for (const file of bundle.files) {
|
||||
const uri =
|
||||
Platform.OS === "ios"
|
||||
? `${file.value.replace("file://", "")}`
|
||||
: `${file.value}`;
|
||||
const hash = await Sodium.hashFile({
|
||||
uri: uri,
|
||||
type: "cache"
|
||||
});
|
||||
await attachFile(uri, hash, file.type, file.name, {
|
||||
type: "cache",
|
||||
id: id,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
});
|
||||
let content = ``;
|
||||
if (isImage(file.type)) {
|
||||
content = `<img data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" />`;
|
||||
} else {
|
||||
content = `<p><span data-hash="${hash}" data-mime="${file.type}" data-filename="${file.name}" data-size="${file.size}" /></p>`;
|
||||
}
|
||||
const rawContent = await db.content.raw(db.notes.note(id).data?.contentId);
|
||||
await db.notes.add({
|
||||
id: id,
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: rawContent?.data ? rawContent?.data + content : content
|
||||
},
|
||||
sessionId: sessionId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ const getDpi = (pd) => {
|
||||
};
|
||||
const correction = (size, multiplier) => {
|
||||
let dSize = getDeviceSize();
|
||||
if (dSize >= 4 && dSize <= 5.3 && pixelDensity <= 3) {
|
||||
if (dSize <= 4.5 && pixelDensity <= 3) {
|
||||
return size * 0.85;
|
||||
} else if (dSize <= 5.3 && pixelDensity <= 3) {
|
||||
return size * 0.93;
|
||||
} else if (dSize > 5.3 && dSize < 7 && pixelDensity < 3 && !DDS.isTab) {
|
||||
if (Platform.OS === "ios") {
|
||||
@@ -54,9 +56,9 @@ const correction = (size, multiplier) => {
|
||||
} else if (dSize >= 6.5 && dSize <= 7.2 && DDS.isTab) {
|
||||
return size * multiplier;
|
||||
} else if (dSize > 7.2 && dSize <= 8.5 && DDS.isTab) {
|
||||
return size * 0.85;
|
||||
return size * 0.92;
|
||||
} else if (dSize > 8.5 && dSize <= 9.2 && DDS.isTab) {
|
||||
return size * 0.9;
|
||||
return size * 0.92;
|
||||
} else if (dSize > 9.2 && dSize <= 10.5 && DDS.isTab) {
|
||||
return size * 0.95;
|
||||
} else if (dSize > 10.5) {
|
||||
|
||||
20
apps/mobile/package-lock.json
generated
20
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.5.3",
|
||||
"version": "2.5.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.5.3",
|
||||
"version": "2.5.4",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
"native/",
|
||||
@@ -39,7 +39,7 @@
|
||||
"html-to-text": "9.0.5",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.18",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.21",
|
||||
"react-native-blob-util": "0.17.3",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
@@ -19655,9 +19655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-actions-sheet": {
|
||||
"version": "0.9.0-alpha.18",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.9.0-alpha.18.tgz",
|
||||
"integrity": "sha512-X3PBVXyI7DQMfa1p3TbEFY2UjAUGqwl9ej6HxZ6ohm4KacqmGoDUZKLNz9lKCzw0vYeK8lwxtCC7qDSuEEgGQg==",
|
||||
"version": "0.9.0-alpha.21",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.9.0-alpha.21.tgz",
|
||||
"integrity": "sha512-ZYaVjQfNUtdUZBh0uVAesjk00WyxJm9siTpADVCb07Qavimiz4npHn3Sm4jR0fyvFEokVqY3FKT4ETzaUDiVSA==",
|
||||
"peerDependencies": {
|
||||
"react-native": "*",
|
||||
"react-native-gesture-handler": "*"
|
||||
@@ -28688,7 +28688,7 @@
|
||||
"html-to-text": "9.0.5",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.18",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.21",
|
||||
"react-native-blob-util": "0.17.3",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
@@ -38720,9 +38720,9 @@
|
||||
}
|
||||
},
|
||||
"react-native-actions-sheet": {
|
||||
"version": "0.9.0-alpha.18",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.9.0-alpha.18.tgz",
|
||||
"integrity": "sha512-X3PBVXyI7DQMfa1p3TbEFY2UjAUGqwl9ej6HxZ6ohm4KacqmGoDUZKLNz9lKCzw0vYeK8lwxtCC7qDSuEEgGQg=="
|
||||
"version": "0.9.0-alpha.21",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.9.0-alpha.21.tgz",
|
||||
"integrity": "sha512-ZYaVjQfNUtdUZBh0uVAesjk00WyxJm9siTpADVCb07Qavimiz4npHn3Sm4jR0fyvFEokVqY3FKT4ETzaUDiVSA=="
|
||||
},
|
||||
"react-native-actions-shortcuts": {
|
||||
"version": "1.0.1",
|
||||
|
||||
@@ -41,7 +41,7 @@ import { EventTypes } from "../app/screens/editor/tiptap/editor-events";
|
||||
|
||||
const useEditor = () => {
|
||||
const ref = useRef();
|
||||
const [sessionId] = useState("share-editor-session");
|
||||
const [sessionId] = useState("share-editor-session" + Date.now());
|
||||
const colors = useShareStore((state) => state.colors);
|
||||
const accent = useShareStore((state) => state.accent);
|
||||
const commands = useMemo(() => new Commands(ref), [ref]);
|
||||
|
||||
@@ -17,7 +17,10 @@ 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 ShareExtension from "@ammarahmed/react-native-share-extension";
|
||||
import { getPreviewData } from "@flyerhq/react-native-link-preview";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { isImage } from "@notesnook/core/utils/filename";
|
||||
import { parseHTML } from "@notesnook/core/utils/html-parser";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
@@ -34,27 +37,23 @@ import {
|
||||
View,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
SafeAreaProvider,
|
||||
useSafeAreaInsets
|
||||
} from "react-native-safe-area-context";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import ShareExtension from "@ammarahmed/react-native-share-extension";
|
||||
import isURL from "validator/lib/isURL";
|
||||
import { db } from "../app/common/database";
|
||||
import { MMKV } from "../app/common/database/mmkv";
|
||||
import Storage from "../app/common/database/storage";
|
||||
import { eSendEvent } from "../app/services/event-manager";
|
||||
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../app/utils/constants";
|
||||
import { getElevationStyle } from "../app/utils/elevation";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { eOnLoadNote } from "../app/utils/events";
|
||||
import { NoteBundle } from "../app/utils/note-bundle";
|
||||
import { Editor } from "./editor";
|
||||
import { Search } from "./search";
|
||||
import { initDatabase, useShareStore } from "./store";
|
||||
import { isImage } from "@notesnook/core/utils/filename";
|
||||
import { NoteBundle } from "../app/utils/note-bundle";
|
||||
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../app/utils/constants";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
const getLinkPreview = (url) => {
|
||||
return getPreviewData(url, 5000);
|
||||
@@ -369,15 +368,11 @@ const ShareView = ({ quicknote = false }) => {
|
||||
_note.sessionId = Date.now();
|
||||
}
|
||||
|
||||
const noteBundle = {
|
||||
await NoteBundle.createNotes({
|
||||
files: rawFiles,
|
||||
note: _note,
|
||||
notebooks: useShareStore.getState().selectedNotebooks
|
||||
};
|
||||
const bundles = MMKV.getArray("shared:noteBundles") || [];
|
||||
bundles.push(noteBundle);
|
||||
MMKV.setArray("shared:noteBundles", bundles);
|
||||
await NoteBundle.createNotes();
|
||||
});
|
||||
|
||||
try {
|
||||
await db.sync(false, false);
|
||||
|
||||
@@ -30,6 +30,8 @@ import { db } from "../app/common/database";
|
||||
export async function initDatabase() {
|
||||
if (!db.isInitialized) {
|
||||
await db.init();
|
||||
} else {
|
||||
await db.initCollections();
|
||||
}
|
||||
await db.notes.init();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ async function actAndSync<T>(
|
||||
) {
|
||||
const results = await Promise.all([
|
||||
...actions.filter((a) => !!a),
|
||||
...devices.map((d) => d.waitForSync("synced", "now"))
|
||||
...devices.map((d) => d.waitForSync("completed"))
|
||||
]);
|
||||
|
||||
await Promise.all(devices.map((d) => d.page.waitForTimeout(2000)));
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<style>
|
||||
html[data-theme="dark"] {
|
||||
--bg: #1b1b1b;
|
||||
--bg: #0f0f0f;
|
||||
--fg: #fff;
|
||||
--three-bars-bg: #494949;
|
||||
--gradient-stop-color: #111111;
|
||||
|
||||
206
apps/web/package-lock.json
generated
206
apps/web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"version": "2.5.6",
|
||||
"version": "2.5.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "2.5.6",
|
||||
"version": "2.5.7",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@aws-sdk/util-base64-browser": "^3.208.0",
|
||||
@@ -18,19 +18,18 @@
|
||||
"@mdi/js": "^6.9.96",
|
||||
"@mdi/react": "^1.6.0",
|
||||
"@notesnook-importer/core": "^1.7.1",
|
||||
"@notesnook/localforage-getitems": "^1.4.4",
|
||||
"@react-pdf-viewer/core": "^3.12.0",
|
||||
"@react-pdf-viewer/toolbar": "^3.12.0",
|
||||
"@theme-ui/components": "^0.14.7",
|
||||
"@theme-ui/core": "^0.14.7",
|
||||
"@trpc/client": "^10.29.1",
|
||||
"@trpc/client": "10.31.0",
|
||||
"allotment": "^1.19.0",
|
||||
"axios": "^1.3.4",
|
||||
"clipboard-polyfill": "^3.0.3",
|
||||
"comlink": "^4.3.1",
|
||||
"cronosjs": "^1.7.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"electron-trpc": "^0.5.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"event-source-polyfill": "^1.0.25",
|
||||
"fflate": "^0.8.0",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -38,8 +37,6 @@
|
||||
"hash-wasm": "^4.9.0",
|
||||
"hotkeys-js": "^3.8.3",
|
||||
"immer": "^9.0.6",
|
||||
"localforage": "^1.10.0",
|
||||
"localforage-driver-memory": "^1.0.5",
|
||||
"mac-scrollbar": "^0.10.3",
|
||||
"marked": "^4.1.0",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
@@ -63,7 +60,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@playwright/test": "^1.35.0",
|
||||
"@trpc/server": "^10.29.1",
|
||||
"@trpc/server": "10.31.0",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/marked": "^4.0.7",
|
||||
@@ -72,8 +69,6 @@
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.2",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -93,7 +88,6 @@
|
||||
"vite-plugin-env-compatible": "^1.1.1",
|
||||
"vite-plugin-pwa": "^0.16.3",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vitest": "^0.32.0",
|
||||
"workbox-core": "^7.0.0",
|
||||
"workbox-expiration": "^7.0.0",
|
||||
@@ -1808,36 +1802,6 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz",
|
||||
"integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz",
|
||||
"integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-regenerator": {
|
||||
"version": "7.20.5",
|
||||
"dev": true,
|
||||
@@ -3000,12 +2964,6 @@
|
||||
"spark-md5": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@notesnook/localforage-getitems": {
|
||||
"version": "1.4.4",
|
||||
"dependencies": {
|
||||
"localforage": ">=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/core": {
|
||||
"version": "12.0.1",
|
||||
"dev": true,
|
||||
@@ -3298,23 +3256,6 @@
|
||||
"rollup": "^1.20.0 || ^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-virtual": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.1.tgz",
|
||||
"integrity": "sha512-fK8O0IL5+q+GrsMLuACVNk2x21g3yaw+sG2qn16SnUd3IlBsQyvWxLMGHmCmXRMecPjGRSZ/1LmZB4rjQm68og==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "3.1.0",
|
||||
"dev": true,
|
||||
@@ -3776,20 +3717,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/client": {
|
||||
"version": "10.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.29.1.tgz",
|
||||
"integrity": "sha512-+9Tifg6dtKsYLsqOW0wizqc3iILAkXxn16pyYAeMDPlulPEqNvnI85GDJ0zJOJLIkQnQefkRbtCmtDxLNtV9Eg==",
|
||||
"version": "10.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.31.0.tgz",
|
||||
"integrity": "sha512-VCqbJEvFJb8C4hQFw7AD+dkQTjgEdV/QAzO4D+/cX5e93u5NpfNXI+PKS0QFXwG/zqgwQwVV6OkYc/D/MFwA6g==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@trpc/server": "10.29.1"
|
||||
"@trpc/server": "10.31.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/server": {
|
||||
"version": "10.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.29.1.tgz",
|
||||
"integrity": "sha512-kNXgMh5ya+awuz2tB4eIyVrRs7nVtqGXwSGabzH3l5ZLWz7rbKJquOJ7h6bjvIfWUpaFG62HJNWxxGUtXCRgRw==",
|
||||
"version": "10.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.31.0.tgz",
|
||||
"integrity": "sha512-9EnRTSDE9nF11LZsvSOqNKqkRYzHqFX4ch5AJ6VIu8uta2vxVTN4FxxsNRSOluTzVYZDeaCISbwmOJ5iihCCIg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
@@ -3961,60 +3902,6 @@
|
||||
"version": "0.2.13",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.0.tgz",
|
||||
"integrity": "sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.21.0",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.19.6",
|
||||
"react-refresh": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react-refresh": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-refresh/-/plugin-react-refresh-1.3.6.tgz",
|
||||
"integrity": "sha512-iNR/UqhUOmFFxiezt0em9CgmiJBdWR+5jGxB2FihaoJfqGt76kiwaKoVOJVU5NYcDWMdN06LbyN2VIGIoYdsEA==",
|
||||
"deprecated": "This package has been deprecated in favor of @vitejs/plugin-react",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.14.8",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.14.5",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.14.5",
|
||||
"@rollup/pluginutils": "^4.1.1",
|
||||
"react-refresh": "^0.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react-refresh/node_modules/@rollup/pluginutils": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
|
||||
"integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"estree-walker": "^2.0.1",
|
||||
"picomatch": "^2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react-refresh/node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react-swc": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz",
|
||||
@@ -4027,15 +3914,6 @@
|
||||
"vite": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react/node_modules/react-refresh": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
||||
"integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.0.tgz",
|
||||
@@ -5072,9 +4950,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-trpc": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.0.tgz",
|
||||
"integrity": "sha512-xvnvOpuI0IiecMGS9VOS5/3kLj6IUpjngKhv5VqXU9vP2LzE140G8gqaziX0Tl7MaiYmJZbad/imis9i49n/4A==",
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.2.tgz",
|
||||
"integrity": "sha512-lJTeTwrd/E1C3VGFvdSBQggdmdDeHGrrpnuiB1e3bQR/1f91HmprySMygs9e4i12+CLqR+RIyKTQWLKCNjy5/Q==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
@@ -6512,24 +6390,6 @@
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage-driver-commons": {
|
||||
"version": "1.0.3",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"tslib": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage-driver-memory": {
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"localforage-driver-commons": "^1.0.1",
|
||||
"tslib": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"localforage": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"license": "MIT"
|
||||
@@ -7447,15 +7307,6 @@
|
||||
"react-dom": ">=16.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz",
|
||||
"integrity": "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scroll-sync": {
|
||||
"version": "0.9.0",
|
||||
"license": "MIT",
|
||||
@@ -8335,10 +8186,6 @@
|
||||
"version": "0.0.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"license": "Apache-2.0",
|
||||
@@ -9117,29 +8964,6 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-top-level-await": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.3.1.tgz",
|
||||
"integrity": "sha512-55M1h4NAwkrpxPNOJIBzKZFihqLUzIgnElLSmPNPMR2Fn9+JHKaNg3sVX1Fq+VgvuBksQYxiD3OnwQAUu7kaPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/plugin-virtual": "^3.0.1",
|
||||
"@swc/core": "^1.3.10",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": ">=2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-top-level-await/node_modules/uuid": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/rollup": {
|
||||
"version": "3.24.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.0.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.5.6",
|
||||
"version": "2.5.7",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -23,7 +23,6 @@
|
||||
"@notesnook/crypto-worker": "*",
|
||||
"@notesnook/desktop": "*",
|
||||
"@notesnook/editor": "*",
|
||||
"@notesnook/localforage-getitems": "^1.4.4",
|
||||
"@notesnook/logger": "*",
|
||||
"@notesnook/streamable-fs": "*",
|
||||
"@notesnook/theme": "*",
|
||||
@@ -32,14 +31,14 @@
|
||||
"@react-pdf-viewer/toolbar": "^3.12.0",
|
||||
"@theme-ui/components": "^0.14.7",
|
||||
"@theme-ui/core": "^0.14.7",
|
||||
"@trpc/client": "^10.29.1",
|
||||
"@trpc/client": "10.31.0",
|
||||
"allotment": "^1.19.0",
|
||||
"axios": "^1.3.4",
|
||||
"clipboard-polyfill": "^3.0.3",
|
||||
"comlink": "^4.3.1",
|
||||
"cronosjs": "^1.7.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"electron-trpc": "^0.5.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"event-source-polyfill": "^1.0.25",
|
||||
"fflate": "^0.8.0",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -47,8 +46,6 @@
|
||||
"hash-wasm": "^4.9.0",
|
||||
"hotkeys-js": "^3.8.3",
|
||||
"immer": "^9.0.6",
|
||||
"localforage": "^1.10.0",
|
||||
"localforage-driver-memory": "^1.0.5",
|
||||
"mac-scrollbar": "^0.10.3",
|
||||
"marked": "^4.1.0",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
@@ -72,7 +69,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@playwright/test": "^1.35.0",
|
||||
"@trpc/server": "^10.29.1",
|
||||
"@trpc/server": "10.31.0",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/marked": "^4.0.7",
|
||||
@@ -81,8 +78,6 @@
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.2",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -102,7 +97,6 @@
|
||||
"vite-plugin-env-compatible": "^1.1.1",
|
||||
"vite-plugin-pwa": "^0.16.3",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vitest": "^0.32.0",
|
||||
"workbox-core": "^7.0.0",
|
||||
"workbox-expiration": "^7.0.0",
|
||||
|
||||
@@ -23,7 +23,16 @@ import useSlider from "./hooks/use-slider";
|
||||
import useMobile from "./hooks/use-mobile";
|
||||
import useTablet from "./hooks/use-tablet";
|
||||
|
||||
export default function MobileAppEffects({ sliderId, overlayId, setShow }) {
|
||||
type MobileAppEffectsProps = {
|
||||
sliderId: string;
|
||||
overlayId: string;
|
||||
setShow: (show: boolean) => void;
|
||||
};
|
||||
export default function MobileAppEffects({
|
||||
sliderId,
|
||||
overlayId,
|
||||
setShow
|
||||
}: MobileAppEffectsProps) {
|
||||
const isMobile = useMobile();
|
||||
const isTablet = useTablet();
|
||||
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
|
||||
@@ -40,6 +49,7 @@ export default function MobileAppEffects({ sliderId, overlayId, setShow }) {
|
||||
|
||||
const percent = offset - (position / width) * offset;
|
||||
const overlay = document.getElementById("overlay");
|
||||
if (!overlay) return;
|
||||
if (percent > 0) {
|
||||
overlay.style.opacity = `${percent}%`;
|
||||
overlay.style.pointerEvents = "all";
|
||||
@@ -74,6 +84,7 @@ export default function MobileAppEffects({ sliderId, overlayId, setShow }) {
|
||||
useEffect(() => {
|
||||
if (!overlayId) return;
|
||||
const overlay = document.getElementById(overlayId);
|
||||
if (!overlay) return;
|
||||
overlay.onclick = () => toggleSideMenu(false);
|
||||
return () => {
|
||||
overlay.onclick = null;
|
||||
@@ -20,12 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useEffect } from "react";
|
||||
import { useStore } from "./stores/app-store";
|
||||
import { useStore as useUserStore } from "./stores/user-store";
|
||||
import { useStore as useNotesStore } from "./stores/note-store";
|
||||
import { useStore as useThemeStore } from "./stores/theme-store";
|
||||
import { useStore as useAttachmentStore } from "./stores/attachment-store";
|
||||
import { useStore as useEditorStore } from "./stores/editor-store";
|
||||
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
|
||||
import { resetReminders, scheduleBackups } from "./common/reminders";
|
||||
import { resetNotices, scheduleBackups } from "./common/notices";
|
||||
import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { db } from "./common/db";
|
||||
@@ -47,14 +46,15 @@ import { interruptedOnboarding } from "./dialogs/onboarding-dialog";
|
||||
import { hashNavigate } from "./navigation";
|
||||
import { desktop } from "./common/desktop-bridge";
|
||||
|
||||
export default function AppEffects({ setShow }) {
|
||||
type AppEffectsProps = {
|
||||
setShow: (show: boolean) => void;
|
||||
};
|
||||
export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
const refreshNavItems = useStore((store) => store.refreshNavItems);
|
||||
const updateLastSynced = useStore((store) => store.updateLastSynced);
|
||||
const isFocusMode = useStore((store) => store.isFocusMode);
|
||||
const addReminder = useStore((store) => store.addReminder);
|
||||
const initUser = useUserStore((store) => store.init);
|
||||
const initStore = useStore((store) => store.init);
|
||||
const initNotes = useNotesStore((store) => store.init);
|
||||
const initAttachments = useAttachmentStore((store) => store.init);
|
||||
const setIsVaultCreated = useStore((store) => store.setIsVaultCreated);
|
||||
const setTheme = useThemeStore((store) => store.setTheme);
|
||||
@@ -69,7 +69,7 @@ export default function AppEffects({ setShow }) {
|
||||
function initializeApp() {
|
||||
const userCheckStatusEvent = EV.subscribe(
|
||||
EVENTS.userCheckStatus,
|
||||
async (type) => {
|
||||
async (type: string) => {
|
||||
if (isUserPremium()) {
|
||||
return { type, result: true };
|
||||
} else {
|
||||
@@ -86,7 +86,6 @@ export default function AppEffects({ setShow }) {
|
||||
initStore();
|
||||
initAttachments();
|
||||
refreshNavItems();
|
||||
initNotes();
|
||||
initEditorStore();
|
||||
|
||||
(async function () {
|
||||
@@ -94,8 +93,8 @@ export default function AppEffects({ setShow }) {
|
||||
if (await initUser()) {
|
||||
showUpgradeReminderDialogs();
|
||||
}
|
||||
await resetReminders();
|
||||
setIsVaultCreated(await db.vault.exists());
|
||||
await resetNotices();
|
||||
setIsVaultCreated(await db.vault?.exists());
|
||||
|
||||
await showOnboardingDialog(interruptedOnboarding());
|
||||
await showFeatureDialog("highlights");
|
||||
@@ -113,23 +112,29 @@ export default function AppEffects({ setShow }) {
|
||||
updateLastSynced,
|
||||
refreshNavItems,
|
||||
initUser,
|
||||
initNotes,
|
||||
addReminder,
|
||||
setIsVaultCreated
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const systemTimeInvalidEvent = EV.subscribe(
|
||||
EVENTS.systemTimeInvalid,
|
||||
async ({ serverTime, localTime }) => {
|
||||
await showInvalidSystemTimeDialog({ serverTime, localTime });
|
||||
}
|
||||
);
|
||||
// const systemTimeInvalidEvent = EV.subscribe(
|
||||
// EVENTS.systemTimeInvalid,
|
||||
// async ({ serverTime, localTime }) => {
|
||||
// await showInvalidSystemTimeDialog({ serverTime, localTime });
|
||||
// }
|
||||
// );
|
||||
|
||||
const attachmentsLoadingEvent = EV.subscribe(
|
||||
EVENTS.attachmentsLoading,
|
||||
({ type, total, current }) => {
|
||||
({
|
||||
type,
|
||||
total,
|
||||
current
|
||||
}: {
|
||||
type: ProcessingType;
|
||||
total: number;
|
||||
current: number;
|
||||
}) => {
|
||||
const [key, status] = getProcessingStatusFromType(type);
|
||||
|
||||
if (current === total) {
|
||||
@@ -146,7 +151,15 @@ export default function AppEffects({ setShow }) {
|
||||
|
||||
const progressEvent = AppEventManager.subscribe(
|
||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||
({ type, total, loaded }) => {
|
||||
({
|
||||
type,
|
||||
total,
|
||||
loaded
|
||||
}: {
|
||||
type: ProcessingType;
|
||||
total: number;
|
||||
loaded: number;
|
||||
}) => {
|
||||
const [key, status] = getProcessingStatusFromType(type);
|
||||
if (!key) return;
|
||||
|
||||
@@ -169,7 +182,7 @@ export default function AppEffects({ setShow }) {
|
||||
return () => {
|
||||
attachmentsLoadingEvent.unsubscribe();
|
||||
progressEvent.unsubscribe();
|
||||
systemTimeInvalidEvent.unsubscribe();
|
||||
// systemTimeInvalidEvent.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -223,7 +236,8 @@ export default function AppEffects({ setShow }) {
|
||||
return <React.Fragment />;
|
||||
}
|
||||
|
||||
function getProcessingStatusFromType(type) {
|
||||
type ProcessingType = "download" | "upload" | "encrypt";
|
||||
function getProcessingStatusFromType(type: ProcessingType) {
|
||||
switch (type) {
|
||||
case "download":
|
||||
return ["downloadingAttachments", "Downloading"];
|
||||
@@ -232,6 +246,6 @@ function getProcessingStatusFromType(type) {
|
||||
case "encrypt":
|
||||
return ["encryptingAttachments", "Encrypting"];
|
||||
default:
|
||||
return undefined;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import useMobile from "./hooks/use-mobile";
|
||||
import useTablet from "./hooks/use-tablet";
|
||||
import { LazyMotion, domAnimation } from "framer-motion";
|
||||
import useDatabase from "./hooks/use-database";
|
||||
import { Allotment, LayoutPriority } from "allotment";
|
||||
import { Allotment, AllotmentHandle, LayoutPriority } from "allotment";
|
||||
import { useStore } from "./stores/app-store";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { ViewLoader } from "./components/loaders/view-loader";
|
||||
@@ -38,8 +38,8 @@ import { usePersistentState } from "./hooks/use-persistent-state";
|
||||
|
||||
new WebExtensionRelay();
|
||||
|
||||
const GlobalMenuWrapper = React.lazy(() =>
|
||||
import("./components/global-menu-wrapper")
|
||||
const GlobalMenuWrapper = React.lazy(
|
||||
() => import("./components/global-menu-wrapper")
|
||||
);
|
||||
const AppEffects = React.lazy(() => import("./app-effects"));
|
||||
const MobileAppEffects = React.lazy(() => import("./app-effects.mobile"));
|
||||
@@ -91,24 +91,53 @@ function App() {
|
||||
|
||||
export default App;
|
||||
|
||||
function SuspenseLoader({ condition, props, component: Component, fallback }) {
|
||||
type SuspenseLoaderProps<TComponent extends React.JSXElementConstructor<any>> =
|
||||
{
|
||||
condition: boolean;
|
||||
props?: React.ComponentProps<TComponent>;
|
||||
component: TComponent;
|
||||
fallback: JSX.Element;
|
||||
};
|
||||
|
||||
function SuspenseLoader<TComponent extends React.JSXElementConstructor<any>>({
|
||||
condition,
|
||||
props,
|
||||
component,
|
||||
fallback
|
||||
}: SuspenseLoaderProps<TComponent>) {
|
||||
if (!condition) return fallback;
|
||||
|
||||
const Component = component as (
|
||||
props: any
|
||||
) => React.ReactComponentElement<any, any>;
|
||||
return (
|
||||
<Suspense fallback={fallback}>
|
||||
<Suspense
|
||||
fallback={
|
||||
import.meta.env.REACT_APP_PLATFORM === "desktop" ? null : fallback
|
||||
}
|
||||
>
|
||||
<Component {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopAppContents({ isAppLoaded, show, setShow }) {
|
||||
type DesktopAppContentsProps = {
|
||||
isAppLoaded: boolean;
|
||||
show: boolean;
|
||||
setShow: (show: boolean) => void;
|
||||
};
|
||||
function DesktopAppContents({
|
||||
isAppLoaded,
|
||||
show,
|
||||
setShow
|
||||
}: DesktopAppContentsProps) {
|
||||
const isFocusMode = useStore((store) => store.isFocusMode);
|
||||
const isTablet = useTablet();
|
||||
const [paneSizes, setPaneSizes] = usePersistentState("paneSizes", [
|
||||
isTablet ? 60 : 180,
|
||||
isTablet ? 240 : 380
|
||||
]);
|
||||
const panesRef = useRef();
|
||||
const panesRef = useRef<AllotmentHandle>(null);
|
||||
const [isNarrow, setIsNarrow] = useState(paneSizes[0] <= 55);
|
||||
|
||||
return (
|
||||
@@ -138,7 +167,7 @@ function DesktopAppContents({ isAppLoaded, show, setShow }) {
|
||||
toggleNavigationContainer={(state) => {
|
||||
setShow(state || !show);
|
||||
}}
|
||||
isTablet={isNarrow}
|
||||
isTablet={isTablet || isNarrow}
|
||||
/>
|
||||
</Allotment.Pane>
|
||||
<Allotment.Pane
|
||||
@@ -180,7 +209,7 @@ function DesktopAppContents({ isAppLoaded, show, setShow }) {
|
||||
);
|
||||
}
|
||||
|
||||
function MobileAppContents({ isAppLoaded }) {
|
||||
function MobileAppContents({ isAppLoaded }: { isAppLoaded: boolean }) {
|
||||
return (
|
||||
<FlexScrollContainer
|
||||
id="slider"
|
||||
@@ -33,14 +33,6 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
const { Compressor } = await import("../utils/compressor");
|
||||
db = database;
|
||||
|
||||
database.setup(
|
||||
new NNStorage("Notesnook", persistence),
|
||||
EventSource,
|
||||
FS,
|
||||
new Compressor()
|
||||
);
|
||||
|
||||
// if (isTesting()) {
|
||||
db.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
@@ -48,6 +40,15 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co"
|
||||
});
|
||||
|
||||
database.setup(
|
||||
new NNStorage("Notesnook", persistence),
|
||||
EventSource,
|
||||
FS,
|
||||
new Compressor()
|
||||
);
|
||||
// if (isTesting()) {
|
||||
|
||||
// } else {
|
||||
// db.host({
|
||||
// API_HOST: "http://localhost:5264",
|
||||
@@ -64,10 +65,6 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
// });
|
||||
// }
|
||||
|
||||
// db.eventManager.subscribe(EVENTS.databaseMigrating, async ({ from, to }) => {
|
||||
|
||||
// });
|
||||
|
||||
await db.init();
|
||||
|
||||
logger.measure("Database initialization");
|
||||
|
||||
@@ -17,11 +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/>.
|
||||
*/
|
||||
|
||||
import { type desktop as bridge } from "./bridge";
|
||||
import { type desktop as bridge } from "./index.desktop";
|
||||
|
||||
export const desktop: typeof bridge | undefined =
|
||||
import.meta.env.REACT_APP_PLATFORM === "desktop"
|
||||
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(await import("./bridge")).desktop
|
||||
: undefined;
|
||||
export const desktop: typeof bridge | undefined = undefined;
|
||||
|
||||
@@ -430,7 +430,13 @@ function getDialogData(type: string) {
|
||||
|
||||
export function showPasswordDialog(
|
||||
type: string,
|
||||
validate: ({ password }: { password: string }) => boolean | Promise<boolean>
|
||||
validate: ({
|
||||
password
|
||||
}: {
|
||||
password?: string;
|
||||
oldPassword?: string;
|
||||
newPassword?: string;
|
||||
}) => boolean | Promise<boolean>
|
||||
) {
|
||||
const { title, subtitle, positiveButtonText, checks } = getDialogData(type);
|
||||
return showDialog("PasswordDialog", (Dialog, perform) => (
|
||||
@@ -648,7 +654,7 @@ export function showSettings() {
|
||||
));
|
||||
}
|
||||
|
||||
export function showOnboardingDialog(type: string) {
|
||||
export function showOnboardingDialog(type?: string) {
|
||||
if (!type) return;
|
||||
return showDialog("OnboardingDialog", (Dialog, perform) => (
|
||||
<Dialog type={type} onClose={(res: boolean) => perform(res)} />
|
||||
@@ -689,19 +695,6 @@ export async function showUpdateAvailableNotice({
|
||||
});
|
||||
}
|
||||
|
||||
export async function showUpdateReadyNotice({ version }: { version: string }) {
|
||||
const changelog = await getChangelog(version);
|
||||
return await showUpdateDialog({
|
||||
title: `Update ready for installation`,
|
||||
subtitle: `v${version} is ready to be installed.`,
|
||||
changelog,
|
||||
action: {
|
||||
text: "Install now",
|
||||
onClick: () => installUpdate()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type UpdateDialogProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
|
||||
@@ -1,72 +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/>.
|
||||
*/
|
||||
|
||||
const SINGLE_LINE_HEIGHT = 1.4;
|
||||
const DEFAULT_FONT_SIZE = document.getElementById("p").clientHeight - 1;
|
||||
|
||||
const MAX_HEIGHTS = {
|
||||
note: SINGLE_LINE_HEIGHT * 7 * DEFAULT_FONT_SIZE,
|
||||
notebook: SINGLE_LINE_HEIGHT * 7 * DEFAULT_FONT_SIZE,
|
||||
generic: SINGLE_LINE_HEIGHT * 4 * DEFAULT_FONT_SIZE
|
||||
};
|
||||
|
||||
function getNoteHeight(item) {
|
||||
const { notebooks, headline } = item;
|
||||
let height = SINGLE_LINE_HEIGHT * 3;
|
||||
//if (title.length > 35) height += SINGLE_LINE_HEIGHT;
|
||||
if (headline?.length > 0) height += SINGLE_LINE_HEIGHT * 2;
|
||||
else height += SINGLE_LINE_HEIGHT;
|
||||
if (notebooks?.length) height += SINGLE_LINE_HEIGHT;
|
||||
|
||||
return height * DEFAULT_FONT_SIZE;
|
||||
}
|
||||
|
||||
function getNotebookHeight(item) {
|
||||
const { topics, description, title } = item;
|
||||
// at the minimum we will have a title and the info text
|
||||
let height = SINGLE_LINE_HEIGHT * 3; // 2.8 = 2 lines
|
||||
|
||||
if (title.length > 35) {
|
||||
height += SINGLE_LINE_HEIGHT; // title has become multiline
|
||||
}
|
||||
|
||||
if (topics.length > 0) {
|
||||
height += SINGLE_LINE_HEIGHT;
|
||||
}
|
||||
|
||||
if (description?.length > 0) {
|
||||
height += SINGLE_LINE_HEIGHT * 2;
|
||||
}
|
||||
|
||||
return height * DEFAULT_FONT_SIZE;
|
||||
}
|
||||
|
||||
function getItemHeight(item) {
|
||||
const { title } = item;
|
||||
// at the minimum we will have a title and the info text
|
||||
let height = SINGLE_LINE_HEIGHT * 3; // 2.8 = 2 lines
|
||||
|
||||
if (title.length > 30) {
|
||||
height += SINGLE_LINE_HEIGHT; // title has become multiline
|
||||
}
|
||||
|
||||
return height * DEFAULT_FONT_SIZE;
|
||||
}
|
||||
|
||||
export { getNoteHeight, getNotebookHeight, getItemHeight, MAX_HEIGHTS };
|
||||
@@ -21,18 +21,23 @@ import Config from "../utils/config";
|
||||
import { createBackup, verifyAccount } from "./index";
|
||||
import { db } from "./db";
|
||||
import { store as appStore } from "../stores/app-store";
|
||||
import { Backup, User, Email, Warn } from "../components/icons";
|
||||
import { Backup, User, Email, Warn, Icon } from "../components/icons";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
showBuyDialog,
|
||||
showRecoveryKeyDialog
|
||||
} from "../common/dialog-controller";
|
||||
import { showBuyDialog, showRecoveryKeyDialog } from "./dialog-controller";
|
||||
import { hardNavigate, hashNavigate } from "../navigation";
|
||||
import { isDesktop, isTesting } from "../utils/platform";
|
||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { TaskScheduler } from "../utils/task-scheduler";
|
||||
|
||||
export type NoticeType = "autoBackupsOff" | "login" | "email" | "recoverykey";
|
||||
|
||||
export type Notice = {
|
||||
type: NoticeType;
|
||||
priority: number;
|
||||
params?: any;
|
||||
};
|
||||
|
||||
export const BACKUP_CRON_EXPRESSIONS = [
|
||||
"",
|
||||
"0 0 8 * * *", // daily at 8 AM
|
||||
@@ -41,15 +46,15 @@ export const BACKUP_CRON_EXPRESSIONS = [
|
||||
];
|
||||
|
||||
export async function scheduleBackups() {
|
||||
const backupReminderOffset = Config.get("backupReminderOffset", 0);
|
||||
const backupInterval = Config.get("backupReminderOffset", 0);
|
||||
|
||||
await TaskScheduler.stop("automatic-backups");
|
||||
if (!backupReminderOffset) return false;
|
||||
if (!backupInterval) return false;
|
||||
|
||||
console.log("Scheduling automatic backups");
|
||||
await TaskScheduler.register(
|
||||
"automatic-backups",
|
||||
BACKUP_CRON_EXPRESSIONS[backupReminderOffset],
|
||||
BACKUP_CRON_EXPRESSIONS[backupInterval],
|
||||
() => {
|
||||
console.log("Backing up automatically");
|
||||
saveBackup();
|
||||
@@ -57,9 +62,9 @@ export async function scheduleBackups() {
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldAddAutoBackupsDisabledReminder() {
|
||||
const backupReminderOffset = Config.get("backupReminderOffset", 0);
|
||||
if (!isUserPremium() && backupReminderOffset) {
|
||||
export function shouldAddAutoBackupsDisabledNotice() {
|
||||
const backupInterval = Config.get("backupReminderOffset", 0);
|
||||
if (!isUserPremium() && backupInterval) {
|
||||
Config.set("backupReminderOffset", 0);
|
||||
return true;
|
||||
}
|
||||
@@ -67,23 +72,22 @@ export function shouldAddAutoBackupsDisabledReminder() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function shouldAddBackupReminder() {
|
||||
const backupReminderOffset = Config.get("backupReminderOffset", 0);
|
||||
if (!backupReminderOffset) return false;
|
||||
export async function shouldAddBackupNotice() {
|
||||
const backupInterval = Config.get("backupReminderOffset", 0);
|
||||
if (!backupInterval) return false;
|
||||
|
||||
const lastBackupTime = await db.backup.lastBackupTime();
|
||||
const lastBackupTime = await db.backup?.lastBackupTime();
|
||||
if (!lastBackupTime) {
|
||||
await db.backup.updateBackupTime();
|
||||
await db.backup?.updateBackupTime();
|
||||
return false;
|
||||
}
|
||||
|
||||
const offsetToDays =
|
||||
backupReminderOffset === 1 ? 1 : backupReminderOffset === 2 ? 7 : 30;
|
||||
const offsetToDays = backupInterval === 1 ? 1 : backupInterval === 2 ? 7 : 30;
|
||||
|
||||
return dayjs(lastBackupTime).add(offsetToDays, "d").isBefore(dayjs());
|
||||
}
|
||||
|
||||
export async function shouldAddRecoveryKeyBackupReminder() {
|
||||
export async function shouldAddRecoveryKeyBackupNotice() {
|
||||
if (isIgnored("recoverykey")) return false;
|
||||
|
||||
const recoveryKeyBackupDate = Config.get("recoveryKeyBackupDate", 0);
|
||||
@@ -91,17 +95,26 @@ export async function shouldAddRecoveryKeyBackupReminder() {
|
||||
return dayjs(recoveryKeyBackupDate).add(30, "d").isBefore(dayjs());
|
||||
}
|
||||
|
||||
export async function shouldAddLoginReminder() {
|
||||
const user = await db.user.getUser();
|
||||
export async function shouldAddLoginNotice() {
|
||||
const user = await db.user?.getUser();
|
||||
if (!user) return true;
|
||||
}
|
||||
|
||||
export async function shouldAddConfirmEmailReminder() {
|
||||
const user = await db.user.getUser();
|
||||
export async function shouldAddConfirmEmailNotice() {
|
||||
const user = await db.user?.getUser();
|
||||
return !user?.isEmailConfirmed;
|
||||
}
|
||||
|
||||
export const Reminders = {
|
||||
type NoticeData = {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
action: (params?: any) => void;
|
||||
dismissable?: boolean;
|
||||
icon: Icon;
|
||||
};
|
||||
|
||||
export const NoticesData: Record<NoticeType, NoticeData> = {
|
||||
autoBackupsOff: {
|
||||
key: "autoBackupsOff",
|
||||
title: "Automatic backups disabled",
|
||||
@@ -136,32 +149,32 @@ export const Reminders = {
|
||||
}
|
||||
};
|
||||
|
||||
var openedToast = null;
|
||||
export async function resetReminders() {
|
||||
const reminders = [];
|
||||
export async function resetNotices() {
|
||||
const notices: Notice[] = [];
|
||||
|
||||
if (shouldAddAutoBackupsDisabledReminder()) {
|
||||
reminders.push({ type: "autoBackupsOff", priority: "high" });
|
||||
if (shouldAddAutoBackupsDisabledNotice()) {
|
||||
notices.push({ type: "autoBackupsOff", priority: 3 });
|
||||
}
|
||||
if (await shouldAddBackupReminder()) {
|
||||
if (await shouldAddBackupNotice()) {
|
||||
await saveBackup();
|
||||
}
|
||||
if (await shouldAddLoginReminder()) {
|
||||
reminders.push({ type: "login", priority: "low" });
|
||||
if (await shouldAddLoginNotice()) {
|
||||
notices.push({ type: "login", priority: 1 });
|
||||
}
|
||||
if (await shouldAddConfirmEmailReminder()) {
|
||||
reminders.push({ type: "email", priority: "high" });
|
||||
if (await shouldAddConfirmEmailNotice()) {
|
||||
notices.push({ type: "email", priority: 4 });
|
||||
}
|
||||
if (await shouldAddRecoveryKeyBackupReminder()) {
|
||||
reminders.push({ type: "recoverykey", priority: "high" });
|
||||
if (await shouldAddRecoveryKeyBackupNotice()) {
|
||||
notices.push({ type: "recoverykey", priority: 5 });
|
||||
}
|
||||
appStore.get().setReminders(...reminders);
|
||||
appStore.get().setNotices(...notices);
|
||||
}
|
||||
|
||||
function isIgnored(key) {
|
||||
function isIgnored(key: keyof typeof NoticesData) {
|
||||
return Config.get(`ignored:${key}`, false);
|
||||
}
|
||||
|
||||
var openedToast: { hide: () => void } | null = null;
|
||||
async function saveBackup() {
|
||||
if (isDesktop()) {
|
||||
await createBackup();
|
||||
@@ -174,7 +187,7 @@ async function saveBackup() {
|
||||
{
|
||||
text: "Later",
|
||||
onClick: async () => {
|
||||
await db.backup.updateBackupTime();
|
||||
await db.backup?.updateBackupTime();
|
||||
openedToast?.hide();
|
||||
openedToast = null;
|
||||
},
|
||||
@@ -22,7 +22,7 @@ import { Cross } from "../icons";
|
||||
import { ANALYTICS_EVENTS, trackEvent } from "../../utils/analytics";
|
||||
import AnnouncementBody from "./body";
|
||||
import { useStore as useAnnouncementStore } from "../../stores/announcement-store";
|
||||
import ReminderBar from "../reminder-bar";
|
||||
import Notice from "../notice";
|
||||
|
||||
function Announcements() {
|
||||
const announcements = useAnnouncementStore(
|
||||
@@ -31,7 +31,7 @@ function Announcements() {
|
||||
const dismiss = useAnnouncementStore((store) => store.dismiss);
|
||||
const announcement = announcements[0];
|
||||
|
||||
if (!announcement) return <ReminderBar />;
|
||||
if (!announcement) return <Notice />;
|
||||
return (
|
||||
<Flex
|
||||
mx={1}
|
||||
|
||||
@@ -92,7 +92,6 @@ function DiffViewer(props) {
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
await db.notes.init();
|
||||
let note = db.notes.note(noteId);
|
||||
if (!note) {
|
||||
hashNavigate(`/notes/create`, { replace: true });
|
||||
|
||||
@@ -185,6 +185,7 @@ function TipTap(props: TipTapProps) {
|
||||
autofocus: "start",
|
||||
onFocus,
|
||||
onCreate: ({ editor }) => {
|
||||
if (onLoad) onLoad();
|
||||
if (oldNonce.current !== nonce)
|
||||
editor.commands.focus("start", { scrollIntoView: true });
|
||||
oldNonce.current = nonce;
|
||||
@@ -201,7 +202,6 @@ function TipTap(props: TipTapProps) {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (onLoad) onLoad();
|
||||
editor.commands.refreshSearch();
|
||||
},
|
||||
onUpdate: ({ editor, transaction }) => {
|
||||
|
||||
@@ -36,8 +36,6 @@ import Announcements from "../announcements";
|
||||
import { ListLoader } from "../loaders/list-loader";
|
||||
import ScrollContainer from "../scroll-container";
|
||||
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
|
||||
import { AnimatedFlex } from "../animated";
|
||||
import { domAnimation, LazyMotion } from "framer-motion";
|
||||
|
||||
export const CustomScrollbarsVirtualList = forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -128,149 +126,132 @@ function ListContainer(props: ListContainerProps) {
|
||||
const Component = ListProfiles[type];
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation} strict>
|
||||
<Flex variant="columnFill">
|
||||
{!props.items.length && props.placeholder ? (
|
||||
<>
|
||||
{header}
|
||||
{props.isLoading ? (
|
||||
<ListLoader />
|
||||
) : (
|
||||
<Flex variant="columnCenterFill" data-test-id="list-placeholder">
|
||||
{props.placeholder}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AnimatedFlex
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.98
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1
|
||||
}}
|
||||
transition={{ duration: 0.2, delay: 0.1, ease: "easeInOut" }}
|
||||
ref={listContainerRef}
|
||||
variant="columnFill"
|
||||
data-test-id={`${type}-list`}
|
||||
>
|
||||
<Virtuoso
|
||||
ref={listRef}
|
||||
data={items}
|
||||
computeItemKey={(index) =>
|
||||
items[index].id || items[index].title
|
||||
}
|
||||
defaultItemHeight={DEFAULT_ITEM_HEIGHT}
|
||||
totalCount={items.length}
|
||||
onBlur={() => setFocusedGroupIndex(-1)}
|
||||
onKeyDown={(e) => onKeyDown(e.nativeEvent)}
|
||||
components={{
|
||||
Scroller: CustomScrollbarsVirtualList,
|
||||
Item: (props) => (
|
||||
<div
|
||||
{...props}
|
||||
style={{ paddingBottom: 1 }}
|
||||
onFocus={() => onFocus(props["data-item-index"])}
|
||||
onMouseDown={(e) =>
|
||||
onMouseDown(e.nativeEvent, props["data-item-index"])
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
),
|
||||
Header: () => (header ? header : <Announcements />)
|
||||
}}
|
||||
itemContent={(index, item) => {
|
||||
if (!item) return null;
|
||||
|
||||
switch (item.type) {
|
||||
case "header":
|
||||
if (!groupType) return null;
|
||||
return (
|
||||
<GroupHeader
|
||||
type={groupType}
|
||||
refresh={refresh}
|
||||
title={item.title}
|
||||
isFocused={index === focusedGroupIndex}
|
||||
index={index}
|
||||
onSelectGroup={() => {
|
||||
let endIndex;
|
||||
for (
|
||||
let i = index + 1;
|
||||
i < props.items.length;
|
||||
++i
|
||||
) {
|
||||
if (props.items[i].type === "header") {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setSelectedItems([
|
||||
...selectionStore.get().selectedItems,
|
||||
...props.items.slice(
|
||||
index,
|
||||
endIndex || props.items.length
|
||||
)
|
||||
]);
|
||||
}}
|
||||
groups={groups}
|
||||
onJump={(title: string) => {
|
||||
const index = props.items.findIndex(
|
||||
(v) => v.title === title
|
||||
);
|
||||
if (index < 0) return;
|
||||
listRef.current?.scrollToIndex({
|
||||
index,
|
||||
align: "center",
|
||||
behavior: "auto"
|
||||
});
|
||||
setFocusedGroupIndex(index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Component
|
||||
item={item}
|
||||
context={context}
|
||||
index={index}
|
||||
type={type}
|
||||
compact={compact}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AnimatedFlex>
|
||||
</>
|
||||
)}
|
||||
{button && (
|
||||
<Button
|
||||
variant="primary"
|
||||
data-test-id={`${props.type}-action-button`}
|
||||
onClick={button.onClick}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
display: ["block", "block", "none"],
|
||||
alignSelf: "end",
|
||||
borderRadius: 100,
|
||||
p: 0,
|
||||
m: 0,
|
||||
mb: 2,
|
||||
mr: 2,
|
||||
width: 45,
|
||||
height: 45
|
||||
}}
|
||||
<Flex variant="columnFill">
|
||||
{!props.items.length && props.placeholder ? (
|
||||
<>
|
||||
{header}
|
||||
{props.isLoading ? (
|
||||
<ListLoader />
|
||||
) : (
|
||||
<Flex variant="columnCenterFill" data-test-id="list-placeholder">
|
||||
{props.placeholder}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Flex
|
||||
ref={listContainerRef}
|
||||
variant="columnFill"
|
||||
data-test-id={`${type}-list`}
|
||||
>
|
||||
<Plus color="static" />
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</LazyMotion>
|
||||
<Virtuoso
|
||||
ref={listRef}
|
||||
data={items}
|
||||
computeItemKey={(index) => items[index].id || items[index].title}
|
||||
defaultItemHeight={DEFAULT_ITEM_HEIGHT}
|
||||
totalCount={items.length}
|
||||
onBlur={() => setFocusedGroupIndex(-1)}
|
||||
onKeyDown={(e) => onKeyDown(e.nativeEvent)}
|
||||
components={{
|
||||
Scroller: CustomScrollbarsVirtualList,
|
||||
Item: (props) => (
|
||||
<div
|
||||
{...props}
|
||||
style={{ paddingBottom: 1 }}
|
||||
onFocus={() => onFocus(props["data-item-index"])}
|
||||
onMouseDown={(e) =>
|
||||
onMouseDown(e.nativeEvent, props["data-item-index"])
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
),
|
||||
Header: () => (header ? header : <Announcements />)
|
||||
}}
|
||||
itemContent={(index, item) => {
|
||||
if (!item) return null;
|
||||
|
||||
switch (item.type) {
|
||||
case "header":
|
||||
if (!groupType) return null;
|
||||
return (
|
||||
<GroupHeader
|
||||
type={groupType}
|
||||
refresh={refresh}
|
||||
title={item.title}
|
||||
isFocused={index === focusedGroupIndex}
|
||||
index={index}
|
||||
onSelectGroup={() => {
|
||||
let endIndex;
|
||||
for (let i = index + 1; i < props.items.length; ++i) {
|
||||
if (props.items[i].type === "header") {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setSelectedItems([
|
||||
...selectionStore.get().selectedItems,
|
||||
...props.items.slice(
|
||||
index,
|
||||
endIndex || props.items.length
|
||||
)
|
||||
]);
|
||||
}}
|
||||
groups={groups}
|
||||
onJump={(title: string) => {
|
||||
const index = props.items.findIndex(
|
||||
(v) => v.title === title
|
||||
);
|
||||
if (index < 0) return;
|
||||
listRef.current?.scrollToIndex({
|
||||
index,
|
||||
align: "center",
|
||||
behavior: "auto"
|
||||
});
|
||||
setFocusedGroupIndex(index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Component
|
||||
item={item}
|
||||
context={context}
|
||||
index={index}
|
||||
type={type}
|
||||
compact={compact}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
{button && (
|
||||
<Button
|
||||
variant="primary"
|
||||
data-test-id={`${props.type}-action-button`}
|
||||
onClick={button.onClick}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
display: ["block", "none", "none"],
|
||||
alignSelf: "end",
|
||||
borderRadius: 100,
|
||||
p: 0,
|
||||
m: 0,
|
||||
mb: 2,
|
||||
mr: 2,
|
||||
width: 45,
|
||||
height: 45
|
||||
}}
|
||||
>
|
||||
<Plus color="static" />
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default ListContainer;
|
||||
|
||||
@@ -33,11 +33,6 @@ const SINGLE_LINE_HEIGHT = 1.4;
|
||||
const DEFAULT_LINE_HEIGHT =
|
||||
(document.getElementById("p")?.clientHeight || 16) - 1;
|
||||
export const DEFAULT_ITEM_HEIGHT = SINGLE_LINE_HEIGHT * 2 * DEFAULT_LINE_HEIGHT;
|
||||
// const MAX_HEIGHTS = {
|
||||
// note: SINGLE_LINE_HEIGHT * 7 * DEFAULT_LINE_HEIGHT,
|
||||
// notebook: SINGLE_LINE_HEIGHT * 7 * DEFAULT_LINE_HEIGHT,
|
||||
// generic: SINGLE_LINE_HEIGHT * 4 * DEFAULT_LINE_HEIGHT
|
||||
// };
|
||||
|
||||
export type Item = { id: string; type: string; title: string } & Record<
|
||||
string,
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import useLocation from "../../hooks/use-location";
|
||||
import { FlexScrollContainer } from "../scroll-container";
|
||||
import { isDesktop } from "../../utils/platform";
|
||||
|
||||
type Route = {
|
||||
title: string;
|
||||
@@ -191,7 +192,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
))}
|
||||
{colors.map((color, index) => (
|
||||
<NavigationItem
|
||||
animate
|
||||
animate={!isDesktop()}
|
||||
index={index}
|
||||
isTablet={isTablet}
|
||||
key={color.id}
|
||||
@@ -220,7 +221,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
/>
|
||||
{shortcuts.map((item, index) => (
|
||||
<NavigationItem
|
||||
animate
|
||||
animate={!isDesktop()}
|
||||
index={colors.length - 1 + index}
|
||||
isTablet={isTablet}
|
||||
key={item.id}
|
||||
|
||||
@@ -20,23 +20,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useMemo } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { Reminders } from "../../common/reminders";
|
||||
import { NoticesData } from "../../common/notices";
|
||||
import { Dismiss } from "../icons";
|
||||
import Config from "../../utils/config";
|
||||
|
||||
function ReminderBar() {
|
||||
const reminders = useAppStore((store) => store.reminders);
|
||||
const dismissReminders = useAppStore((store) => store.dismissReminders);
|
||||
const reminder = useMemo(() => {
|
||||
if (!reminders) return null;
|
||||
|
||||
const copy = reminders.slice();
|
||||
const reminder = copy.sort((a, b) => a.priority - b.priority)[0];
|
||||
if (!reminder) return;
|
||||
return Reminders[reminder.type];
|
||||
}, [reminders]);
|
||||
|
||||
if (!reminder) return null;
|
||||
function Notice() {
|
||||
const notices = useAppStore((store) => store.notices);
|
||||
const dismissNotices = useAppStore((store) => store.dismissNotices);
|
||||
const notice = useMemo(() => {
|
||||
if (!notices) return null;
|
||||
return notices.slice().sort((a, b) => a.priority - b.priority)[0];
|
||||
}, [notices]);
|
||||
if (!notice) return null;
|
||||
const NoticeData = NoticesData[notice.type];
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
@@ -47,34 +43,34 @@ function ReminderBar() {
|
||||
minWidth: 250
|
||||
}}
|
||||
p={1}
|
||||
onClick={reminder?.action}
|
||||
onClick={() => NoticeData.action()}
|
||||
mx={1}
|
||||
>
|
||||
<Flex sx={{ flex: 1, alignItems: "center" }}>
|
||||
<reminder.icon
|
||||
<NoticeData.icon
|
||||
size={18}
|
||||
color="primary"
|
||||
sx={{ bg: "shade", mr: 2, p: 2, borderRadius: 80 }}
|
||||
/>
|
||||
<Flex variant="columnCenter" sx={{ alignItems: "flex-start" }}>
|
||||
<Text variant="body" sx={{ fontSize: "body" }}>
|
||||
{reminder.title}
|
||||
{NoticeData.title}
|
||||
</Text>
|
||||
<Text variant="subBody" sx={{ display: "block" }}>
|
||||
{reminder.subtitle}
|
||||
{NoticeData.subtitle}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{reminder.dismissable && (
|
||||
{NoticeData.dismissable && (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const dontShowAgain = window.confirm(
|
||||
"Don't show again on this device?"
|
||||
);
|
||||
dismissReminders(reminder);
|
||||
dismissNotices(notice);
|
||||
if (dontShowAgain) {
|
||||
Config.set(`ignored:${reminder.key}`, true);
|
||||
Config.set(`ignored:${NoticeData.key}`, true);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
@@ -92,4 +88,4 @@ function ReminderBar() {
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default ReminderBar;
|
||||
export default Notice;
|
||||
@@ -22,8 +22,11 @@ import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { verifyAccount } from "../../common";
|
||||
import {
|
||||
show2FARecoveryCodesDialog,
|
||||
showMultifactorDialog
|
||||
showMultifactorDialog,
|
||||
showPasswordDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { db } from "../../common/db";
|
||||
import { showToast } from "../../utils/toast";
|
||||
|
||||
export const AuthenticationSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -41,7 +44,21 @@ export const AuthenticationSettings: SettingsGroup[] = [
|
||||
type: "button",
|
||||
title: "Change password",
|
||||
variant: "secondary",
|
||||
action: () => {}
|
||||
action: async () => {
|
||||
const result = await showPasswordDialog(
|
||||
"change_account_password",
|
||||
async (data) => {
|
||||
await db.user?.clearSessions();
|
||||
return (
|
||||
db.user?.changePassword(
|
||||
data.oldPassword,
|
||||
data.newPassword
|
||||
) || false
|
||||
);
|
||||
}
|
||||
);
|
||||
if (result) showToast("success", "Account password changed!");
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -59,6 +76,30 @@ export const AuthenticationSettings: SettingsGroup[] = [
|
||||
keywords: [],
|
||||
components: []
|
||||
},
|
||||
{
|
||||
key: "primary-2fa-method",
|
||||
title: "Primary method",
|
||||
keywords: ["primary 2fa method"],
|
||||
description: () =>
|
||||
`Your current 2FA method is ${
|
||||
useUserStore.getState().user?.mfa.primaryMethod
|
||||
}.`,
|
||||
onStateChange: (listener) =>
|
||||
useUserStore.subscribe((s) => s.user?.mfa.primaryMethod, listener),
|
||||
components: [
|
||||
{
|
||||
type: "button",
|
||||
title: "Change",
|
||||
action: async () => {
|
||||
if (await verifyAccount()) {
|
||||
await showMultifactorDialog();
|
||||
await useUserStore.getState().refreshUser();
|
||||
}
|
||||
},
|
||||
variant: "secondary"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "fallback-2fa-method",
|
||||
title: "Fallback method",
|
||||
|
||||
@@ -165,7 +165,6 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
onSelectionChanged: async (value) => {
|
||||
if (!db.notes || value === "-") return;
|
||||
|
||||
await db.notes.init();
|
||||
await exportNotes(
|
||||
value as "txt" | "md" | "html",
|
||||
db.notes.all.map((n) => n.id)
|
||||
|
||||
@@ -28,13 +28,13 @@ export const BehaviourSettings: SettingsGroup[] = [
|
||||
key: "general",
|
||||
section: "behaviour",
|
||||
header: "General",
|
||||
isHidden: () => !isUserPremium(),
|
||||
settings: [
|
||||
{
|
||||
key: "default-homepage",
|
||||
title: "Home page",
|
||||
description: "Default screen to open on app startup.",
|
||||
keywords: ["welcome page", "default screen"],
|
||||
isHidden: () => !isUserPremium(),
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.homepage, listener),
|
||||
components: [
|
||||
|
||||
@@ -224,20 +224,28 @@ export default function SettingsDialog(props: SettingsDialogProps) {
|
||||
const isTitleMatch =
|
||||
typeof group.header === "string" &&
|
||||
group.header.toLowerCase().includes(query);
|
||||
const isSectionMatch = group.section.includes(query);
|
||||
|
||||
if (isTitleMatch) {
|
||||
if (isTitleMatch || isSectionMatch) {
|
||||
groups.push(group);
|
||||
continue;
|
||||
}
|
||||
|
||||
const settings = group.settings.filter(
|
||||
(setting) =>
|
||||
setting.description?.toLowerCase().includes(query) ||
|
||||
setting.keywords?.some((keyword) =>
|
||||
keyword.toLowerCase().includes(query)
|
||||
) ||
|
||||
setting.title.toLowerCase().includes(query)
|
||||
);
|
||||
const settings = group.settings.filter((setting) => {
|
||||
const description =
|
||||
typeof setting.description === "function"
|
||||
? setting.description()
|
||||
: setting.description;
|
||||
|
||||
return [
|
||||
description || "",
|
||||
setting.keywords?.join(" ") || "",
|
||||
setting.title
|
||||
]
|
||||
.join(" ")
|
||||
?.toLowerCase()
|
||||
.includes(query);
|
||||
});
|
||||
if (!settings.length) continue;
|
||||
groups.push({ ...group, settings });
|
||||
}
|
||||
@@ -292,7 +300,7 @@ export default function SettingsDialog(props: SettingsDialogProps) {
|
||||
}}
|
||||
>
|
||||
{activeSettings.map((group) => (
|
||||
<SettingsGroupComponent key={group.key} item={group} />
|
||||
<SettingsGroupComponent item={group} />
|
||||
))}
|
||||
</FlexScrollContainer>
|
||||
</Flex>
|
||||
@@ -308,7 +316,7 @@ function SettingsGroupComponent(props: { item: SettingsGroup }) {
|
||||
onRender?.();
|
||||
}, [onRender]);
|
||||
|
||||
if (item.isHidden && item.isHidden()) return null;
|
||||
if (item.isHidden?.()) return null;
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
@@ -395,7 +403,9 @@ function SettingItem(props: { item: Setting }) {
|
||||
variant={"body"}
|
||||
sx={{ mt: 1, color: "fontTertiary", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{item.description}
|
||||
{typeof item.description === "function"
|
||||
? item.description(state)
|
||||
: item.description}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -72,7 +72,7 @@ export type Setting = {
|
||||
key: string;
|
||||
keywords?: string[];
|
||||
title: string;
|
||||
description?: string;
|
||||
description?: string | ((state?: unknown) => string);
|
||||
components: SettingComponent[] | ((state?: unknown) => SettingComponent[]);
|
||||
isHidden?: (state?: unknown) => boolean;
|
||||
onStateChange?: (
|
||||
|
||||
@@ -18,10 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
showUpdateAvailableNotice,
|
||||
showUpdateReadyNotice
|
||||
} from "../common/dialog-controller";
|
||||
import { isDesktop } from "../utils/platform";
|
||||
import { checkForUpdate } from "../utils/updater";
|
||||
import { AppEventManager, AppEvents } from "../common/app-events";
|
||||
@@ -58,9 +54,6 @@ export function useAutoUpdater() {
|
||||
type: "available",
|
||||
version: info.version
|
||||
});
|
||||
showUpdateAvailableNotice({
|
||||
version: info.version
|
||||
});
|
||||
}
|
||||
|
||||
function updateNotAvailable() {
|
||||
@@ -70,7 +63,6 @@ export function useAutoUpdater() {
|
||||
|
||||
function updateDownloadCompleted(info: { version: string }) {
|
||||
changeStatus({ type: "completed", version: info.version });
|
||||
showUpdateReadyNotice({ version: info.version });
|
||||
}
|
||||
|
||||
function updateDownloadProgress(progressInfo: { percent: number }) {
|
||||
|
||||
@@ -28,20 +28,21 @@ if (import.meta.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
const memory = {
|
||||
isAppLoaded: false
|
||||
isDatabaseLoaded: false
|
||||
};
|
||||
export default function useDatabase(persistence: "db" | "memory" = "db") {
|
||||
const [isAppLoaded, setIsAppLoaded] = useState(memory.isAppLoaded);
|
||||
const [isAppLoaded, setIsAppLoaded] = useState(memory.isDatabaseLoaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (memory.isAppLoaded) return;
|
||||
|
||||
(async () => {
|
||||
await initializeDatabase(persistence);
|
||||
setIsAppLoaded(true);
|
||||
memory.isAppLoaded = true;
|
||||
})();
|
||||
loadDatabase(persistence).then(() => setIsAppLoaded(true));
|
||||
}, [persistence]);
|
||||
|
||||
return [isAppLoaded];
|
||||
}
|
||||
|
||||
export async function loadDatabase(persistence: "db" | "memory" = "db") {
|
||||
if (memory.isDatabaseLoaded) return;
|
||||
|
||||
await initializeDatabase(persistence);
|
||||
memory.isDatabaseLoaded = true;
|
||||
}
|
||||
@@ -20,14 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useEffect } from "react";
|
||||
import { NavigationEvents } from "../navigation";
|
||||
|
||||
function useNavigate(routeKey, onNavigation) {
|
||||
function useNavigate(routeKey: string, onNavigation: () => void) {
|
||||
useEffect(() => {
|
||||
onNavigation();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function onNavigate(route) {
|
||||
function onNavigate(route: { key: string }) {
|
||||
if (route?.key === routeKey) {
|
||||
onNavigation();
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import Config from "./utils/config";
|
||||
import { isTesting } from "./utils/platform";
|
||||
import { initalizeLogger, logger } from "./utils/logger";
|
||||
import { AuthProps } from "./views/auth";
|
||||
import { loadDatabase } from "./hooks/use-database";
|
||||
|
||||
initalizeLogger();
|
||||
|
||||
@@ -155,6 +156,8 @@ async function renderApp() {
|
||||
} = getRoute();
|
||||
|
||||
if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
|
||||
if (import.meta.env.REACT_APP_PLATFORM === "desktop")
|
||||
await loadDatabase("db");
|
||||
|
||||
logger.measure("app render");
|
||||
|
||||
@@ -171,13 +174,13 @@ async function renderApp() {
|
||||
}
|
||||
|
||||
async function initializeServiceWorker() {
|
||||
logger.info("Initializing service worker...");
|
||||
const serviceWorker = await import("./service-worker-registration");
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
if (import.meta.env.REACT_APP_PLATFORM !== "desktop") {
|
||||
logger.info("Initializing service worker...");
|
||||
const serviceWorker = await import("./service-worker-registration");
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.register({
|
||||
onUpdate: async (registration: ServiceWorkerRegistration) => {
|
||||
if (!registration.waiting) return;
|
||||
@@ -191,7 +194,7 @@ async function initializeServiceWorker() {
|
||||
}
|
||||
});
|
||||
// window.addEventListener("beforeinstallprompt", () => showInstallNotice());
|
||||
} else serviceWorker.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipInitiation() {
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "web-streams-polyfill/dist/ponyfill";
|
||||
import localforage from "localforage";
|
||||
import { xxhash64, createXXHash64 } from "hash-wasm";
|
||||
import axios, { AxiosProgressEvent } from "axios";
|
||||
import { AppEventManager, AppEvents } from "../common/app-events";
|
||||
@@ -37,6 +36,7 @@ import { Base64DecoderStream } from "../utils/streams/base64-decoder-stream";
|
||||
import { toBlob } from "@notesnook-importer/core/dist/src/utils/stream";
|
||||
import { Cipher, OutputFormat, SerializedKey } from "@notesnook/crypto";
|
||||
import { IDataType } from "hash-wasm/dist/lib/util";
|
||||
import { IndexedDBKVStore } from "./key-value";
|
||||
|
||||
const ABYTES = 17;
|
||||
const CHUNK_SIZE = 512 * 1024;
|
||||
@@ -53,7 +53,7 @@ async function writeEncryptedFile(
|
||||
) {
|
||||
const crypto = await getNNCrypto();
|
||||
|
||||
if (!localforage.supports(localforage.INDEXEDDB))
|
||||
if (!IndexedDBKVStore.isIndexedDBSupported())
|
||||
throw new Error("This browser does not support IndexedDB.");
|
||||
|
||||
if (await streamablefs.exists(hash)) await streamablefs.deleteFile(hash);
|
||||
|
||||
327
apps/web/src/interfaces/key-value.ts
Normal file
327
apps/web/src/interfaces/key-value.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
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 Config from "../utils/config";
|
||||
|
||||
export interface IKVStore {
|
||||
/**
|
||||
* Get a value by its key.
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
get<T>(key: string): Promise<T | undefined>;
|
||||
|
||||
/**
|
||||
* Set a value with a key.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
set(key: string, value: any): Promise<void>;
|
||||
|
||||
/**
|
||||
* Set multiple values at once. This is faster than calling set() multiple times.
|
||||
* It's also atomic – if one of the pairs can't be added, none will be added.
|
||||
*
|
||||
* @param entries Array of entries, where each entry is an array of `[key, value]`.
|
||||
*/
|
||||
setMany(entries: [string, any][]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get multiple values by their keys
|
||||
*
|
||||
* @param keys
|
||||
*/
|
||||
getMany<T>(keys: string[]): Promise<[string, T][]>;
|
||||
|
||||
/**
|
||||
* Delete a particular key from the store.
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
delete(key: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete multiple keys at once.
|
||||
*
|
||||
* @param keys List of keys to delete.
|
||||
*/
|
||||
deleteMany(keys: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Clear all values in the store.
|
||||
*
|
||||
*/
|
||||
clear(): Promise<void>;
|
||||
|
||||
keys(): Promise<string[]>;
|
||||
values<T>(): Promise<T[]>;
|
||||
entries<T>(): Promise<[string, T][]>;
|
||||
}
|
||||
|
||||
export class LocalStorageKVStore implements IKVStore {
|
||||
get<T>(key: string): Promise<T | undefined> {
|
||||
return Promise.resolve(Config.get(key));
|
||||
}
|
||||
set(key: string, value: any): Promise<void> {
|
||||
return Promise.resolve(Config.set(key, value));
|
||||
}
|
||||
setMany(entries: [string, any][]): Promise<void> {
|
||||
for (const entry of entries) {
|
||||
Config.set(entry[0], entry[1]);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
getMany<T>(keys: string[]): Promise<[string, T][]> {
|
||||
const entries: [string, T][] = [];
|
||||
for (const key of keys) {
|
||||
entries.push([key, Config.get(key)]);
|
||||
}
|
||||
return Promise.resolve(entries);
|
||||
}
|
||||
delete(key: string): Promise<void> {
|
||||
return Promise.resolve(Config.remove(key));
|
||||
}
|
||||
deleteMany(keys: string[]): Promise<void> {
|
||||
for (const key of keys) {
|
||||
Config.remove(key);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
clear(): Promise<void> {
|
||||
Config.clear();
|
||||
return Promise.resolve();
|
||||
}
|
||||
keys(): Promise<string[]> {
|
||||
return Promise.resolve(Object.keys(Config.all()));
|
||||
}
|
||||
values<T>(): Promise<T[]> {
|
||||
return Promise.resolve(Object.values<T>(Config.all()));
|
||||
}
|
||||
entries<T>(): Promise<[string, T][]> {
|
||||
return Promise.resolve(Object.entries<T>(Config.all()));
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryKVStore implements IKVStore {
|
||||
private storage: Record<string, any> = {};
|
||||
get<T>(key: string): Promise<T | undefined> {
|
||||
return Promise.resolve(this.storage[key]);
|
||||
}
|
||||
set(key: string, value: any): Promise<void> {
|
||||
this.storage[key] = value;
|
||||
return Promise.resolve();
|
||||
}
|
||||
setMany(entries: [string, any][]): Promise<void> {
|
||||
for (const entry of entries) {
|
||||
this.storage[entry[0]] = entry[1];
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
getMany<T>(keys: string[]): Promise<[string, T][]> {
|
||||
const entries: [string, T][] = [];
|
||||
for (const key of keys) {
|
||||
entries.push([key, this.storage[key]]);
|
||||
}
|
||||
return Promise.resolve(entries);
|
||||
}
|
||||
delete(key: string): Promise<void> {
|
||||
delete this.storage[key];
|
||||
return Promise.resolve();
|
||||
}
|
||||
deleteMany(keys: string[]): Promise<void> {
|
||||
for (const key of keys) {
|
||||
delete this.storage[key];
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
clear(): Promise<void> {
|
||||
this.storage = {};
|
||||
return Promise.resolve();
|
||||
}
|
||||
keys(): Promise<string[]> {
|
||||
return Promise.resolve(Object.keys(this.storage));
|
||||
}
|
||||
values<T>(): Promise<T[]> {
|
||||
return Promise.resolve(Object.values<T>(this.storage));
|
||||
}
|
||||
entries<T>(): Promise<[string, T][]> {
|
||||
return Promise.resolve(Object.entries<T>(this.storage));
|
||||
}
|
||||
}
|
||||
|
||||
export type UseStore = <T>(
|
||||
txMode: IDBTransactionMode,
|
||||
callback: (store: IDBObjectStore) => T | PromiseLike<T>
|
||||
) => Promise<T>;
|
||||
|
||||
export class IndexedDBKVStore implements IKVStore {
|
||||
store: UseStore;
|
||||
|
||||
static isIndexedDBSupported(): boolean {
|
||||
return "indexedDB" in window;
|
||||
}
|
||||
|
||||
constructor(databaseName: string, storeName: string) {
|
||||
this.store = this.createStore(databaseName, storeName);
|
||||
}
|
||||
|
||||
private promisifyRequest<T = undefined>(
|
||||
request: IDBRequest<T> | IDBTransaction
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// @ts-ignore - file size hacks
|
||||
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
||||
// @ts-ignore - file size hacks
|
||||
request.onabort = request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
private createStore(dbName: string, storeName: string): UseStore {
|
||||
const request = indexedDB.open(dbName);
|
||||
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
|
||||
const dbp = this.promisifyRequest(request);
|
||||
|
||||
return (txMode, callback) =>
|
||||
dbp.then((db) =>
|
||||
callback(db.transaction(storeName, txMode).objectStore(storeName))
|
||||
);
|
||||
}
|
||||
|
||||
private eachCursor(
|
||||
store: IDBObjectStore,
|
||||
callback: (cursor: IDBCursorWithValue) => void
|
||||
): Promise<void> {
|
||||
store.openCursor().onsuccess = function () {
|
||||
if (!this.result) return;
|
||||
callback(this.result);
|
||||
this.result.continue();
|
||||
};
|
||||
return this.promisifyRequest(store.transaction);
|
||||
}
|
||||
|
||||
get<T>(key: string): Promise<T | undefined> {
|
||||
return this.store("readonly", (store) =>
|
||||
this.promisifyRequest(store.get(key))
|
||||
);
|
||||
}
|
||||
|
||||
set(key: string, value: any): Promise<void> {
|
||||
return this.store("readwrite", (store) => {
|
||||
store.put(value, key);
|
||||
return this.promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
setMany(entries: [string, any][]): Promise<void> {
|
||||
return this.store("readwrite", (store) => {
|
||||
entries.forEach((entry) => store.put(entry[1], entry[0]));
|
||||
return this.promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
getMany<T>(keys: string[]): Promise<[string, T][]> {
|
||||
return this.store("readonly", (store) =>
|
||||
Promise.all(
|
||||
keys.map(async (key) => [
|
||||
key,
|
||||
await this.promisifyRequest(store.get(key))
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
delete(key: string): Promise<void> {
|
||||
return this.store("readwrite", (store) => {
|
||||
store.delete(key);
|
||||
return this.promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
deleteMany(keys: string[]): Promise<void> {
|
||||
return this.store("readwrite", (store: IDBObjectStore) => {
|
||||
keys.forEach((key: IDBValidKey) => store.delete(key));
|
||||
return this.promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
clear(): Promise<void> {
|
||||
return this.store("readwrite", (store) => {
|
||||
store.clear();
|
||||
return this.promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
keys<KeyType extends IDBValidKey>(): Promise<KeyType[]> {
|
||||
return this.store("readonly", (store) => {
|
||||
// Fast path for modern browsers
|
||||
if (store.getAllKeys) {
|
||||
return this.promisifyRequest(
|
||||
store.getAllKeys() as unknown as IDBRequest<KeyType[]>
|
||||
);
|
||||
}
|
||||
|
||||
const items: KeyType[] = [];
|
||||
|
||||
return this.eachCursor(store, (cursor) =>
|
||||
items.push(cursor.key as KeyType)
|
||||
).then(() => items);
|
||||
});
|
||||
}
|
||||
|
||||
values<T = any>(): Promise<T[]> {
|
||||
return this.store("readonly", (store) => {
|
||||
// Fast path for modern browsers
|
||||
if (store.getAll) {
|
||||
return this.promisifyRequest(store.getAll() as IDBRequest<T[]>);
|
||||
}
|
||||
|
||||
const items: T[] = [];
|
||||
|
||||
return this.eachCursor(store, (cursor) =>
|
||||
items.push(cursor.value as T)
|
||||
).then(() => items);
|
||||
});
|
||||
}
|
||||
|
||||
entries<KeyType extends IDBValidKey, ValueType = any>(): Promise<
|
||||
[KeyType, ValueType][]
|
||||
> {
|
||||
return this.store("readonly", (store) => {
|
||||
// Fast path for modern browsers
|
||||
// (although, hopefully we'll get a simpler path some day)
|
||||
if (store.getAll && store.getAllKeys) {
|
||||
return Promise.all([
|
||||
this.promisifyRequest(
|
||||
store.getAllKeys() as unknown as IDBRequest<KeyType[]>
|
||||
),
|
||||
this.promisifyRequest(store.getAll() as IDBRequest<ValueType[]>)
|
||||
]).then(([keys, values]) => keys.map((key, i) => [key, values[i]]));
|
||||
}
|
||||
|
||||
const items: [KeyType, ValueType][] = [];
|
||||
|
||||
return this.store("readonly", (store) =>
|
||||
this.eachCursor(store, (cursor) =>
|
||||
items.push([cursor.key as KeyType, cursor.value])
|
||||
).then(() => items)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,50 +17,49 @@ 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 localforage from "localforage";
|
||||
import { extendPrototype } from "@notesnook/localforage-getitems";
|
||||
import * as MemoryDriver from "localforage-driver-memory";
|
||||
import {
|
||||
IndexedDBKVStore,
|
||||
LocalStorageKVStore,
|
||||
MemoryKVStore,
|
||||
IKVStore
|
||||
} from "./key-value";
|
||||
import { getNNCrypto } from "./nncrypto.stub";
|
||||
import type { Cipher, SerializedKey } from "@notesnook/crypto/dist/src/types";
|
||||
|
||||
localforage.defineDriver(MemoryDriver);
|
||||
extendPrototype(localforage);
|
||||
|
||||
type EncryptedKey = { iv: Uint8Array; cipher: BufferSource };
|
||||
export type DatabasePersistence = "memory" | "db";
|
||||
|
||||
const APP_SALT = "oVzKtazBo7d8sb7TBvY9jw";
|
||||
|
||||
|
||||
export class NNStorage {
|
||||
database: LocalForage;
|
||||
database: IKVStore;
|
||||
|
||||
constructor(name: string, persistence: DatabasePersistence = "db") {
|
||||
const drivers =
|
||||
this.database =
|
||||
persistence === "memory"
|
||||
? [MemoryDriver._driver]
|
||||
: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE];
|
||||
this.database = localforage.createInstance({
|
||||
name,
|
||||
driver: drivers
|
||||
});
|
||||
? new MemoryKVStore()
|
||||
: IndexedDBKVStore.isIndexedDBSupported()
|
||||
? new IndexedDBKVStore(name, "keyvaluepairs")
|
||||
: new LocalStorageKVStore();
|
||||
}
|
||||
|
||||
read<T>(key: string): Promise<T | null> {
|
||||
if (!key) return Promise.resolve(null);
|
||||
return this.database.getItem(key);
|
||||
read<T>(key: string): Promise<T | undefined> {
|
||||
if (!key) return Promise.resolve(undefined);
|
||||
return this.database.get(key);
|
||||
}
|
||||
|
||||
readMulti(keys: string[]) {
|
||||
if (keys.length <= 0) return [];
|
||||
return this.database.getItems(keys.sort());
|
||||
return this.database.getMany(keys.sort());
|
||||
}
|
||||
|
||||
write<T>(key: string, data: T) {
|
||||
return this.database.setItem(key, data);
|
||||
return this.database.set(key, data);
|
||||
}
|
||||
|
||||
remove(key: string) {
|
||||
return this.database.removeItem(key);
|
||||
return this.database.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
@@ -78,7 +77,11 @@ export class NNStorage {
|
||||
const crypto = await getNNCrypto();
|
||||
const keyData = await crypto.exportKey(password, salt);
|
||||
|
||||
if (this.isIndexedDBSupported() && window?.crypto?.subtle && keyData.key) {
|
||||
if (
|
||||
IndexedDBKVStore.isIndexedDBSupported() &&
|
||||
window?.crypto?.subtle &&
|
||||
keyData.key
|
||||
) {
|
||||
const pbkdfKey = await derivePBKDF2Key(password);
|
||||
await this.write(name, pbkdfKey);
|
||||
const cipheredKey = await aesEncrypt(pbkdfKey, keyData.key);
|
||||
@@ -91,7 +94,7 @@ export class NNStorage {
|
||||
}
|
||||
|
||||
async getCryptoKey(name: string): Promise<string | undefined> {
|
||||
if (this.isIndexedDBSupported() && window?.crypto?.subtle) {
|
||||
if (IndexedDBKVStore.isIndexedDBSupported() && window?.crypto?.subtle) {
|
||||
const pbkdfKey = await this.read<CryptoKey>(name);
|
||||
const cipheredKey = await this.read<EncryptedKey | string>(`${name}@_k`);
|
||||
if (typeof cipheredKey === "string") return cipheredKey;
|
||||
@@ -104,10 +107,6 @@ export class NNStorage {
|
||||
}
|
||||
}
|
||||
|
||||
isIndexedDBSupported(): boolean {
|
||||
return this.database.driver() === "asyncStorage";
|
||||
}
|
||||
|
||||
async generateCryptoKey(
|
||||
password: string,
|
||||
salt?: string
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { db } from "../common/db";
|
||||
import Home from "../views/home";
|
||||
import AllNotes from "../views/all-notes";
|
||||
import Notebooks from "../views/notebooks";
|
||||
import Notes from "../views/notes";
|
||||
import Search from "../views/search";
|
||||
@@ -52,7 +52,7 @@ const routes = defineRoutes({
|
||||
key: "home",
|
||||
type: "notes",
|
||||
title: "Notes",
|
||||
component: Home,
|
||||
component: AllNotes,
|
||||
buttons: {
|
||||
create: CREATE_BUTTON_MAP.notes,
|
||||
search: {
|
||||
|
||||
@@ -72,9 +72,6 @@ class AnnouncementStore extends BaseStore {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {[import("zustand").UseStore<AnnouncementStore>, AnnouncementStore]}
|
||||
*/
|
||||
const [useStore, store] = createStore(AnnouncementStore);
|
||||
export { useStore, store };
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import { store as announcementStore } from "./announcement-store";
|
||||
import { store as settingStore } from "./setting-store";
|
||||
import BaseStore from "./index";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { resetReminders } from "../common/reminders";
|
||||
import { resetNotices } from "../common/notices";
|
||||
import { EV, EVENTS, SYNC_CHECK_IDS } from "@notesnook/core/common";
|
||||
import { logger } from "../utils/logger";
|
||||
import Config from "../utils/config";
|
||||
@@ -65,7 +65,10 @@ class AppStore extends BaseStore {
|
||||
};
|
||||
colors = [];
|
||||
globalMenu = { items: [], data: {} };
|
||||
reminders = [];
|
||||
/**
|
||||
* @type {import("../common/notices").Notice[]}
|
||||
*/
|
||||
notices = [];
|
||||
shortcuts = [];
|
||||
lastSynced = 0;
|
||||
|
||||
@@ -147,7 +150,7 @@ class AppStore extends BaseStore {
|
||||
logger.measure("refreshing app");
|
||||
|
||||
await this.updateLastSynced();
|
||||
await resetReminders();
|
||||
await resetNotices();
|
||||
noteStore.refresh();
|
||||
notebookStore.refresh();
|
||||
reminderStore.refresh();
|
||||
@@ -219,28 +222,22 @@ class AppStore extends BaseStore {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"backup"|"signup"|"email"|"recoverykey"} type
|
||||
* @param {string} title
|
||||
* @param {string} detail
|
||||
* @param {"high"|"medium"|"low"} priority
|
||||
* @param {import("../common/notices").Notice[]} notices
|
||||
*/
|
||||
setReminders = (...reminders) => {
|
||||
setNotices = (...notices) => {
|
||||
this.set((state) => {
|
||||
state.reminders = [];
|
||||
for (let reminder of reminders) {
|
||||
const { priority, type } = reminder;
|
||||
state.reminders.push({
|
||||
type,
|
||||
priority: priority === "high" ? 1 : priority === "medium" ? 2 : 1
|
||||
});
|
||||
for (const notice of notices) {
|
||||
const oldIndex = state.notices.findIndex((a) => a.type === notice.type);
|
||||
if (oldIndex > -1) state.notices.splice(oldIndex, 1);
|
||||
state.notices.push(notice);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
dismissReminders = (...reminders) => {
|
||||
dismissNotices = (...reminders) => {
|
||||
this.set((state) => {
|
||||
for (let reminder of reminders) {
|
||||
state.reminders.splice(state.reminders.indexOf(reminder), 1);
|
||||
state.notices.splice(state.notices.indexOf(reminder), 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -287,8 +284,13 @@ class AppStore extends BaseStore {
|
||||
};
|
||||
|
||||
sync = async (full = true, force = false) => {
|
||||
if (!this.get().isSyncEnabled || !navigator.onLine) return;
|
||||
if (this.isSyncing()) return;
|
||||
if (
|
||||
this.isSyncing() ||
|
||||
!this.get().isSyncEnabled ||
|
||||
!navigator.onLine ||
|
||||
!(await networkCheck.waitForInternet())
|
||||
)
|
||||
return;
|
||||
|
||||
clearTimeout(syncStatusTimeout);
|
||||
this.updateLastSynced();
|
||||
@@ -298,7 +300,7 @@ class AppStore extends BaseStore {
|
||||
const result = await db.sync(full, force);
|
||||
|
||||
if (!result) return this.updateSyncStatus("failed");
|
||||
else if (full) this.updateSyncStatus("completed", true);
|
||||
this.updateSyncStatus("completed", true);
|
||||
|
||||
await this.updateLastSynced();
|
||||
} catch (err) {
|
||||
@@ -312,7 +314,11 @@ class AppStore extends BaseStore {
|
||||
this.updateSyncStatus("failed");
|
||||
}
|
||||
|
||||
if (err?.message?.indexOf("Failed to fetch") > -1) return;
|
||||
if (
|
||||
err?.message?.indexOf("Failed to fetch") > -1 ||
|
||||
err?.message?.indexOf("Could not connect to the Sync server.") > -1
|
||||
)
|
||||
return;
|
||||
|
||||
showToast("error", err.message);
|
||||
}
|
||||
|
||||
@@ -134,8 +134,6 @@ class EditorStore extends BaseStore {
|
||||
};
|
||||
|
||||
openSession = async (noteId, force) => {
|
||||
await db.notes.init();
|
||||
|
||||
const session = this.get().session;
|
||||
|
||||
if (session.id) await db.fs.cancel(session.id);
|
||||
|
||||
@@ -34,6 +34,9 @@ import { groupArray } from "@notesnook/core/utils/grouping";
|
||||
*/
|
||||
class NoteStore extends BaseStore {
|
||||
notes = [];
|
||||
/**
|
||||
* @type {{ type: "tag" | "color" | "notebook" | "topic" | "favorite" | "monographs", notes: any[] } | undefined}
|
||||
*/
|
||||
context = undefined;
|
||||
selectedNote = 0;
|
||||
nonce = 0;
|
||||
@@ -44,15 +47,6 @@ class NoteStore extends BaseStore {
|
||||
Config.set("notes:viewMode", viewMode);
|
||||
};
|
||||
|
||||
init = () => {
|
||||
EV.subscribe(EVENTS.noteRemoved, (id) => {
|
||||
const { session } = editorStore.get();
|
||||
if (session.id === id) {
|
||||
hashNavigate("/notes/create", { addNonce: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setSelectedNote = (id) => {
|
||||
if (!id) selectionStore.get().toggleSelectionMode(false);
|
||||
this.set((state) => (state.selectedNote = id));
|
||||
@@ -89,10 +83,8 @@ class NoteStore extends BaseStore {
|
||||
};
|
||||
|
||||
setContext = (context) => {
|
||||
db.notes.init().then(() => {
|
||||
this.get().context = { ...context, notes: notesFromContext(context) };
|
||||
this._forceUpdate();
|
||||
});
|
||||
this.get().context = { ...context, notes: notesFromContext(context) };
|
||||
this._forceUpdate();
|
||||
};
|
||||
|
||||
delete = async (...ids) => {
|
||||
|
||||
@@ -18,19 +18,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { expose } from "comlink";
|
||||
import { gzipSync, gunzipSync } from "fflate";
|
||||
import { gzip, gunzip } from "fflate";
|
||||
import { fromBase64, toBase64 } from "@aws-sdk/util-base64-browser";
|
||||
|
||||
const module = {
|
||||
gzip: ({ data, level }: { data: string; level: number }) => {
|
||||
return toBase64(
|
||||
gzipSync(new TextEncoder().encode(data), {
|
||||
level: level as any
|
||||
})
|
||||
);
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
gzip(
|
||||
new TextEncoder().encode(data),
|
||||
{ level: level as any },
|
||||
(err, data) => (err ? reject(err) : resolve(toBase64(data)))
|
||||
);
|
||||
});
|
||||
},
|
||||
gunzip: ({ data }: { data: string }) => {
|
||||
return new TextDecoder().decode(gunzipSync(fromBase64(data)));
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
gunzip(fromBase64(data), (err, data) =>
|
||||
err ? reject(err) : resolve(new TextDecoder().decode(data))
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -29,12 +29,16 @@ function get<T>(key: string, def?: T): T {
|
||||
return value ? tryParse(value) : def;
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
|
||||
function all(): Record<string, unknown> {
|
||||
const data: Record<string, unknown> = {};
|
||||
function all<T>(): Record<string, T> {
|
||||
const data: Record<string, T> = {};
|
||||
for (let i = 0; i < window.localStorage.length; ++i) {
|
||||
const key = window.localStorage.key(i);
|
||||
if (!key) continue;
|
||||
@@ -52,5 +56,5 @@ function has(predicate: (key: string) => boolean) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Config = { set, get, clear, all, has };
|
||||
const Config = { set, get, clear, all, has, remove };
|
||||
export default Config;
|
||||
|
||||
@@ -17,6 +17,8 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { debounce } from "@notesnook/common";
|
||||
|
||||
function visibilityChange() {
|
||||
return "msHidden" in document
|
||||
? "msvisibilityChange"
|
||||
@@ -39,25 +41,32 @@ export function onPageVisibilityChanged(
|
||||
bool: boolean
|
||||
) => void
|
||||
) {
|
||||
onDeviceOnline(() => handler("online", false));
|
||||
onDeviceOffline(() => handler("offline", false));
|
||||
|
||||
// Handle page visibility change
|
||||
document.addEventListener(visibilityChange(), () =>
|
||||
handler("visibilitychange", isDocumentHidden())
|
||||
window.addEventListener(
|
||||
"online",
|
||||
debounce((_) => {
|
||||
handler("online", false);
|
||||
}, 1000)
|
||||
);
|
||||
window.addEventListener(
|
||||
"offline",
|
||||
debounce((_) => {
|
||||
handler("offline", false);
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
window.addEventListener("focus", () => handler("focus", false));
|
||||
}
|
||||
// Handle page visibility change
|
||||
document.addEventListener(
|
||||
visibilityChange(),
|
||||
debounce((_) => {
|
||||
handler("visibilitychange", isDocumentHidden());
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
function onDeviceOnline(handler: () => void) {
|
||||
window.addEventListener("online", function () {
|
||||
handler();
|
||||
});
|
||||
}
|
||||
|
||||
function onDeviceOffline(handler: () => void) {
|
||||
window.addEventListener("offline", function () {
|
||||
handler();
|
||||
});
|
||||
}
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
debounce((_) => {
|
||||
if (!window.document.hasFocus()) return;
|
||||
handler("focus", false);
|
||||
}, 1000)
|
||||
);
|
||||
}
|
||||
@@ -41,8 +41,6 @@ export class WebExtensionServer implements Server {
|
||||
}
|
||||
|
||||
async getNotes(): Promise<ItemReference[] | undefined> {
|
||||
await db.notes?.init();
|
||||
|
||||
return db.notes?.all
|
||||
.filter((n) => !n.locked)
|
||||
.map((note) => ({ id: note.id, title: note.title }));
|
||||
|
||||
@@ -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 React, { useEffect, useState } from "react";
|
||||
import { useStore, store } from "../stores/note-store";
|
||||
import React, { useEffect } from "react";
|
||||
import { useStore } from "../stores/note-store";
|
||||
import ListContainer from "../components/list-container";
|
||||
import { db } from "../common/db";
|
||||
import { hashNavigate } from "../navigation";
|
||||
import useNavigate from "../hooks/use-navigate";
|
||||
import Placeholder from "../components/placeholders";
|
||||
@@ -31,32 +30,21 @@ function Home() {
|
||||
const isCompact = useStore((store) => store.viewMode === "compact");
|
||||
const refresh = useStore((store) => store.refresh);
|
||||
const clearContext = useStore((store) => store.clearContext);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useNavigate("home", () => {
|
||||
clearContext();
|
||||
});
|
||||
|
||||
useNavigate("home", clearContext);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const initialized = db.notes.initialized;
|
||||
if (!initialized || !notes.length) {
|
||||
await db.notes.init();
|
||||
store.refresh();
|
||||
setIsLoading(false);
|
||||
}
|
||||
// const note = db.notes.note("62bc3f28a1a1a10000707077").data;
|
||||
// const data = await db.content.raw(note.contentId);
|
||||
|
||||
// const note2 = db.notes.note("62bc3f1ca1a1a10000707075").data;
|
||||
// const data2 = await db.content.raw(note2.contentId);
|
||||
|
||||
// const data3 = { ...data, conflicted: data2 };
|
||||
|
||||
// await db.content.add(data3);
|
||||
// await db.notes.add({ id: note.id, conflicted: true, resolved: false });
|
||||
// console.log(data3);
|
||||
})();
|
||||
}, [notes.length]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ListContainer
|
||||
@@ -64,11 +52,9 @@ function Home() {
|
||||
groupType="home"
|
||||
compact={isCompact}
|
||||
refresh={refresh}
|
||||
isLoading={isLoading}
|
||||
items={notes}
|
||||
placeholder={<Placeholder context="notes" />}
|
||||
button={{
|
||||
content: "Make a new note",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { replace: true, addNonce: true })
|
||||
}}
|
||||
@@ -96,7 +96,7 @@ function EmailConfirmed() {
|
||||
p={5}
|
||||
sx={{ flexDirection: "column", justifyContent: "center" }}
|
||||
>
|
||||
<BlogPromoBanner link="https://blog.notesnook.com/why-another-note-taking-app/" />
|
||||
<BlogPromoBanner />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ThemeProvider>
|
||||
@@ -152,7 +152,6 @@ function BlogPromoBanner() {
|
||||
{social.map((account) => (
|
||||
<account.icon
|
||||
key={account.title}
|
||||
hoverColor={account.hoverColor}
|
||||
title={account.title}
|
||||
onClick={() => {
|
||||
window.open(account.link, "_blank");
|
||||
@@ -37,7 +37,6 @@ function Notebooks() {
|
||||
items={notebooks}
|
||||
placeholder={<Placeholder context="notebooks" />}
|
||||
button={{
|
||||
content: "Create a notebook",
|
||||
onClick: () => hashNavigate("/notebooks/create")
|
||||
}}
|
||||
/>
|
||||
@@ -45,7 +45,7 @@ function Notes() {
|
||||
refresh={refreshContext}
|
||||
compact={isCompact}
|
||||
context={{ ...context, notes: undefined }}
|
||||
items={groupArray(context.notes, db.settings.getGroupOptions(type))}
|
||||
items={groupArray(context.notes, db.settings?.getGroupOptions(type))}
|
||||
placeholder={
|
||||
<Placeholder
|
||||
context={
|
||||
@@ -58,7 +58,6 @@ function Notes() {
|
||||
/>
|
||||
}
|
||||
button={{
|
||||
content: "Make a new note",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true })
|
||||
}}
|
||||
@@ -37,7 +37,6 @@ async function typeToItems(type, context) {
|
||||
return ["notes", notes];
|
||||
}
|
||||
case "notes": {
|
||||
await db.notes.init();
|
||||
if (!context) return ["notes", db.notes.all];
|
||||
const notes = context.notes;
|
||||
let topicNotes = [];
|
||||
|
||||
@@ -23,14 +23,10 @@ import { confirm } from "../common/dialog-controller";
|
||||
import { useStore, store } from "../stores/trash-store";
|
||||
import { showToast } from "../utils/toast";
|
||||
import useNavigate from "../hooks/use-navigate";
|
||||
import { db } from "../common/db";
|
||||
import Placeholder from "../components/placeholders";
|
||||
|
||||
function Trash() {
|
||||
useNavigate("trash", async () => {
|
||||
await db.notes.init();
|
||||
store.refresh();
|
||||
});
|
||||
useNavigate("trash", store.refresh);
|
||||
const items = useStore((store) => store.trash);
|
||||
const refresh = useStore((store) => store.refresh);
|
||||
const clearTrash = useStore((store) => store.clear);
|
||||
@@ -43,9 +39,6 @@ function Trash() {
|
||||
placeholder={<Placeholder context="trash" />}
|
||||
items={items}
|
||||
button={{
|
||||
show: !!items.length,
|
||||
content: "Clear Trash",
|
||||
icon: TrashIcon,
|
||||
onClick: function () {
|
||||
confirm({
|
||||
title: "Clear Trash",
|
||||
@@ -59,10 +52,11 @@ function Trash() {
|
||||
await clearTrash();
|
||||
showToast("success", "Trash cleared successfully!");
|
||||
} catch (e) {
|
||||
showToast(
|
||||
"error",
|
||||
`Could not clear trash. Error: ${e.message}`
|
||||
);
|
||||
if (e instanceof Error)
|
||||
showToast(
|
||||
"error",
|
||||
`Could not clear trash. Error: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -22,8 +22,6 @@ import react from "@vitejs/plugin-react-swc";
|
||||
import svgrPlugin from "vite-plugin-svgr";
|
||||
import envCompatible from "vite-plugin-env-compatible";
|
||||
import { VitePWA, ManifestOptions } from "vite-plugin-pwa";
|
||||
import path from "path";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import autoprefixer from "autoprefixer";
|
||||
|
||||
const WEB_MANIFEST: Partial<ManifestOptions> = {
|
||||
@@ -149,6 +147,7 @@ const isDesktop = process.env.REACT_APP_PLATFORM === "desktop";
|
||||
export default defineConfig({
|
||||
envPrefix: "REACT_APP_",
|
||||
build: {
|
||||
target: isDesktop ? "esnext" : "modules",
|
||||
outDir: "build",
|
||||
minify: "esbuild",
|
||||
cssMinify: true,
|
||||
@@ -157,16 +156,16 @@ export default defineConfig({
|
||||
},
|
||||
logLevel: process.env.NODE_ENV === "production" ? "warn" : "info",
|
||||
resolve: {
|
||||
alias: {
|
||||
react: path.resolve(path.join(__dirname, "node_modules", "react")),
|
||||
"react-dom": path.resolve(
|
||||
path.join(__dirname, "node_modules", "react-dom")
|
||||
),
|
||||
"@mdi/js": path.resolve(path.join(__dirname, "node_modules", "@mdi/js")),
|
||||
"@mdi/react": path.resolve(
|
||||
path.join(__dirname, "node_modules", "@mdi/react")
|
||||
)
|
||||
}
|
||||
dedupe: ["react", "react-dom", "@mdi/js", "@mdi/react", "@emotion/react"],
|
||||
|
||||
alias: [
|
||||
{
|
||||
find: /desktop-bridge/gm,
|
||||
replacement: isDesktop
|
||||
? "desktop-bridge/index.desktop"
|
||||
: "desktop-bridge/index"
|
||||
}
|
||||
]
|
||||
},
|
||||
server: {
|
||||
port: 3000
|
||||
@@ -181,14 +180,7 @@ export default defineConfig({
|
||||
},
|
||||
plugins: [
|
||||
...(isDesktop && process.env.NODE_ENV === "production"
|
||||
? [
|
||||
topLevelAwait({
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: "__tla",
|
||||
// The function to generate import names of top-level await promise in each chunk module
|
||||
promiseImportName: (i) => `__tla_${i}`
|
||||
})
|
||||
]
|
||||
? []
|
||||
: [
|
||||
VitePWA({
|
||||
strategies: "injectManifest",
|
||||
|
||||
10
extensions/web-clipper/package-lock.json
generated
10
extensions/web-clipper/package-lock.json
generated
@@ -20366,11 +20366,6 @@
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webextension-polyfill": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.6.0.tgz",
|
||||
"integrity": "sha512-PlYwiX8e4bNZrEeBFxbFFsLtm0SMPxJliLTGdNCA0Bq2XkWrAn2ejUd+89vZm+8BnfFB1BclJyCz3iKsm2atNg=="
|
||||
},
|
||||
"node_modules/webextension-polyfill-ts": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/webextension-polyfill-ts/-/webextension-polyfill-ts-0.26.0.tgz",
|
||||
@@ -36014,11 +36009,6 @@
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"webextension-polyfill": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.6.0.tgz",
|
||||
"integrity": "sha512-PlYwiX8e4bNZrEeBFxbFFsLtm0SMPxJliLTGdNCA0Bq2XkWrAn2ejUd+89vZm+8BnfFB1BclJyCz3iKsm2atNg=="
|
||||
},
|
||||
"webextension-polyfill-ts": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/webextension-polyfill-ts/-/webextension-polyfill-ts-0.26.0.tgz",
|
||||
|
||||
3
nx.json
3
nx.json
@@ -32,6 +32,9 @@
|
||||
"start": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"start:desktop": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test:e2e": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
|
||||
1172
package-lock.json
generated
1172
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -46,15 +46,13 @@
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"prettier": "^2.8.8",
|
||||
"serve": "^14.0.1",
|
||||
"turbowatch": "^2.29.4"
|
||||
"serve": "^14.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lerna/legacy-package-management": "^7.0.0",
|
||||
"@nrwl/nx-cloud": "^14.6.2",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"typescript": "^5.1.3",
|
||||
"zx": "^7.2.2",
|
||||
"lerna": "^7.0.0",
|
||||
"nx": "^16.3.2",
|
||||
"patch-package": "^7.0.0"
|
||||
|
||||
@@ -80,6 +80,23 @@ class Database {
|
||||
this.storage = storage ? new Storage(storage) : null;
|
||||
this.fs = fs && storage ? new FileStorage(fs, storage) : null;
|
||||
NNEventSource = eventsource;
|
||||
|
||||
this.session = new Session(this.storage);
|
||||
this.user = new UserManager(this.storage, this);
|
||||
this.mfa = new MFAManager(this.storage, this);
|
||||
this.syncer = new Sync(this);
|
||||
this.vault = new Vault(this);
|
||||
this.lookup = new Lookup(this);
|
||||
this.backup = new Backup(this);
|
||||
this.settings = new Settings(this);
|
||||
this.migrations = new Migrations(this);
|
||||
this.outbox = new Outbox(this);
|
||||
this.monographs = new Monographs(this);
|
||||
this.offers = new Offers();
|
||||
this.debug = new Debug();
|
||||
this.pricing = new Pricing();
|
||||
this.subscriptions = new Subscriptions(this.user.tokenManager);
|
||||
this.trash = new Trash(this);
|
||||
}
|
||||
|
||||
async _validate() {
|
||||
@@ -105,40 +122,12 @@ class Database {
|
||||
await this.fs.clear();
|
||||
this.disconnectSSE();
|
||||
});
|
||||
EV.subscribe(EVENTS.databaseCollectionInitiated, async (collectionName) => {
|
||||
switch (collectionName) {
|
||||
case "notes": {
|
||||
await this.monographs.init();
|
||||
await this.trash.init();
|
||||
await this.relations.cleanup();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.session = new Session(this.storage);
|
||||
await this._validate();
|
||||
|
||||
this.user = new UserManager(this.storage, this);
|
||||
this.mfa = new MFAManager(this.storage, this);
|
||||
this.syncer = new Sync(this);
|
||||
this.vault = new Vault(this);
|
||||
this.lookup = new Lookup(this);
|
||||
this.backup = new Backup(this);
|
||||
this.settings = new Settings(this);
|
||||
this.migrations = new Migrations(this);
|
||||
this.outbox = new Outbox(this);
|
||||
this.monographs = new Monographs(this);
|
||||
this.offers = new Offers();
|
||||
this.debug = new Debug();
|
||||
this.pricing = new Pricing();
|
||||
this.subscriptions = new Subscriptions(this.user.tokenManager);
|
||||
|
||||
await this.initCollections();
|
||||
|
||||
await this.settings.init();
|
||||
await this.outbox.init();
|
||||
await this.user.init();
|
||||
await this.migrations.init();
|
||||
this.isInitialized = true;
|
||||
if (this.migrations.required()) {
|
||||
@@ -147,9 +136,8 @@ class Database {
|
||||
}
|
||||
|
||||
async initCollections() {
|
||||
await this.settings.init();
|
||||
// collections
|
||||
/** @type {Notes} */
|
||||
this.notes = await Notes.new(this, "notes", true, true);
|
||||
/** @type {Notebooks} */
|
||||
this.notebooks = await Notebooks.new(this, "notebooks");
|
||||
/** @type {Tags} */
|
||||
@@ -168,8 +156,13 @@ class Database {
|
||||
this.reminders = await Reminders.new(this, "reminders");
|
||||
/**@type {Relations} */
|
||||
this.relations = await Relations.new(this, "relations");
|
||||
/** @type {Notes} */
|
||||
this.notes = await Notes.new(this, "notes");
|
||||
|
||||
this.trash = new Trash(this);
|
||||
await this.trash.init();
|
||||
await this.relations.cleanup();
|
||||
|
||||
this.monographs.init().catch(console.error);
|
||||
}
|
||||
|
||||
disconnectSSE() {
|
||||
|
||||
@@ -65,6 +65,7 @@ export default class SyncManager {
|
||||
*/
|
||||
constructor(db) {
|
||||
this.sync = new Sync(db);
|
||||
this._db = db;
|
||||
}
|
||||
|
||||
async start(full, force) {
|
||||
@@ -76,6 +77,19 @@ export default class SyncManager {
|
||||
var isHubException = e.message.includes("HubException:");
|
||||
if (isHubException) {
|
||||
var actualError = /HubException: (.*)/gm.exec(e.message);
|
||||
|
||||
// NOTE: sometimes there's the case where the user has already
|
||||
// confirmed their email but the server still thinks that it
|
||||
// isn't confirmed. This check is added to trigger a force
|
||||
// update of the access token.
|
||||
if (
|
||||
actualError.includes("Please confirm your email ") &&
|
||||
(await this._db.user.getUser()).isEmailConfirmed
|
||||
) {
|
||||
await this._db.user.tokenManager._refreshToken(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actualError.length > 1) throw new Error(actualError[1]);
|
||||
}
|
||||
throw e;
|
||||
|
||||
@@ -85,7 +85,8 @@ export default class Attachments extends Collection {
|
||||
...oldAttachment,
|
||||
...oldAttachment.metadata,
|
||||
...attachmentArg,
|
||||
noteIds
|
||||
noteIds,
|
||||
key: attachmentArg.key || oldAttachment.key
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { NEWLINE_STRIP_REGEX, formatTitle } from "../utils/title-format";
|
||||
export default class Notes extends Collection {
|
||||
constructor(db, name, cached) {
|
||||
super(db, name, cached);
|
||||
this.topicReferences = new NoteIdCache(this._db);
|
||||
this.topicReferences = new NoteIdCache(this);
|
||||
}
|
||||
|
||||
async init() {
|
||||
@@ -440,16 +440,16 @@ function getNoteHeadline(note, content) {
|
||||
class NoteIdCache {
|
||||
/**
|
||||
*
|
||||
* @param {import("../api/index").default} db
|
||||
* @param {Notes} notes
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
constructor(notes) {
|
||||
this.notes = notes;
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
rebuild() {
|
||||
this.cache = new Map();
|
||||
const notes = this._db.notes.all;
|
||||
const notes = this.notes.all;
|
||||
|
||||
for (const note of notes) {
|
||||
const { notebooks } = note;
|
||||
|
||||
@@ -26,10 +26,7 @@ export default class Trash {
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
this.collections = {
|
||||
notes: db.notes,
|
||||
notebooks: db.notebooks
|
||||
};
|
||||
this.collections = ["notes", "notebooks"];
|
||||
}
|
||||
|
||||
async init() {
|
||||
@@ -48,16 +45,16 @@ export default class Trash {
|
||||
|
||||
get all() {
|
||||
let trashItems = [];
|
||||
for (let key in this.collections) {
|
||||
const collection = this.collections[key];
|
||||
for (const key of this.collections) {
|
||||
const collection = this._db[key];
|
||||
trashItems.push(...collection.deleted);
|
||||
}
|
||||
return trashItems;
|
||||
}
|
||||
|
||||
_getItem(id) {
|
||||
for (let key in this.collections) {
|
||||
const collection = this.collections[key]._collection;
|
||||
for (const key of this.collections) {
|
||||
const collection = this._db[key]._collection;
|
||||
if (collection.has(id)) return [collection.getItem(id), collection];
|
||||
}
|
||||
return [];
|
||||
@@ -67,7 +64,7 @@ export default class Trash {
|
||||
const collection = collectionNameFromItem(item);
|
||||
if (!item || !item.type || !collection) return;
|
||||
|
||||
await this.collections[collection]._collection.updateItem({
|
||||
await this._db[collection]._collection.updateItem({
|
||||
...item,
|
||||
id: item.itemId || item.id,
|
||||
type: "trash",
|
||||
@@ -104,9 +101,9 @@ export default class Trash {
|
||||
delete item.itemType;
|
||||
|
||||
if (item.type === "note") {
|
||||
await this.collections.notes.add(item);
|
||||
await this._db.notes.add(item);
|
||||
} else if (item.type === "notebook") {
|
||||
await this.collections.notebooks.add(item);
|
||||
await this._db.notebooks.add(item);
|
||||
}
|
||||
}
|
||||
this._db.notes.topicReferences.rebuild();
|
||||
|
||||
@@ -48,7 +48,6 @@ export default class CachedCollection extends IndexedCollection {
|
||||
// }
|
||||
|
||||
this.map = new MapStub.Map(data, this.type);
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
async clear() {
|
||||
|
||||
11
packages/editor/package-lock.json
generated
11
packages/editor/package-lock.json
generated
@@ -34,6 +34,7 @@
|
||||
"@tiptap/extension-underline": "2.0.3",
|
||||
"@tiptap/pm": "2.0.3",
|
||||
"@tiptap/starter-kit": "2.0.3",
|
||||
"clipboard-polyfill": "^4.0.0",
|
||||
"detect-indent": "^7.0.0",
|
||||
"katex": "^0.16.2",
|
||||
"nanoid": "^4.0.1",
|
||||
@@ -1893,6 +1894,11 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clipboard-polyfill": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clipboard-polyfill/-/clipboard-polyfill-4.0.0.tgz",
|
||||
"integrity": "sha512-U4KPNJqAYuyOtixCZZUyWTcj+wlI66j07g5ggMRE2DR1VFu/3ZWXkjLAslmme8i065gBSCUblHET7DKQ2Xg3RA=="
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@@ -5096,6 +5102,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"clipboard-polyfill": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clipboard-polyfill/-/clipboard-polyfill-4.0.0.tgz",
|
||||
"integrity": "sha512-U4KPNJqAYuyOtixCZZUyWTcj+wlI66j07g5ggMRE2DR1VFu/3ZWXkjLAslmme8i065gBSCUblHET7DKQ2Xg3RA=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"clipboard-polyfill": "^4.0.0",
|
||||
"@emotion/react": "^11.10.0",
|
||||
"@notesnook/theme": "*",
|
||||
"@notesnook/core": "*",
|
||||
@@ -74,8 +75,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"prebuild": "zx ./scripts/build.mjs",
|
||||
"prewatch": "zx ./scripts/build.mjs",
|
||||
"prebuild": "node ./scripts/build.mjs",
|
||||
"prewatch": "node ./scripts/build.mjs",
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"prepublishOnly": "npm run build && npm run test",
|
||||
|
||||
@@ -17,9 +17,13 @@ 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 "zx/globals";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { langen } from "./langen.mjs";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const ROOT_DIR = path.resolve(path.join(__dirname, ".."));
|
||||
|
||||
const pathsToCopy = {
|
||||
@@ -31,8 +35,8 @@ const pathsToCopy = {
|
||||
for (const name in pathsToCopy) {
|
||||
const src = pathsToCopy[name];
|
||||
const fullPath = path.join(ROOT_DIR, src);
|
||||
fs.copySync(fullPath, path.join(ROOT_DIR, "styles", name), {
|
||||
overwrite: true,
|
||||
fs.cpSync(fullPath, path.join(ROOT_DIR, "styles", name), {
|
||||
force: true,
|
||||
recursive: true,
|
||||
errorOnExist: false
|
||||
});
|
||||
|
||||
@@ -21,7 +21,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* Quick & dirty script to download & transform all language details from
|
||||
* the PrismJS repo.
|
||||
*/
|
||||
require("isomorphic-fetch");
|
||||
import "isomorphic-fetch";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export async function langen(rootDirectory, saveDirectory) {
|
||||
const response = await fetch(
|
||||
|
||||
@@ -27,18 +27,25 @@ import { CodeBlockAttributes } from "./code-block";
|
||||
import { ReactNodeViewProps } from "../react/types";
|
||||
import { ResponsivePresenter } from "../../components/responsive";
|
||||
import { Popup } from "../../toolbar/components/popup";
|
||||
import { useIsMobile } from "../../toolbar/stores/toolbar-store";
|
||||
import { Button } from "../../components/button";
|
||||
import { useTimer } from "../../hooks/use-timer";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
|
||||
export function CodeblockComponent(
|
||||
props: ReactNodeViewProps<CodeBlockAttributes>
|
||||
) {
|
||||
const isMobile = useIsMobile();
|
||||
const { editor, updateAttributes, node, forwardRef } = props;
|
||||
const { language, indentLength, indentType, caretPosition } = node.attrs;
|
||||
const { language, indentLength, indentType, caretPosition, lines } =
|
||||
node.attrs;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// const [caretPosition, setCaretPosition] = useState<CaretPosition>();
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { enabled, start } = useTimer(1000);
|
||||
|
||||
const languageDefinition = Languages.find(
|
||||
(l) => l.filename === language || l.alias?.some((a) => a === language)
|
||||
);
|
||||
@@ -74,7 +81,7 @@ export function CodeblockComponent(
|
||||
display: "flex",
|
||||
px: 2,
|
||||
pt: 2,
|
||||
pb: 1
|
||||
pb: 2
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
@@ -144,6 +151,38 @@ export function CodeblockComponent(
|
||||
</Text>
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{node.textContent.length > 0 ? (
|
||||
<Button
|
||||
variant={"tool"}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
opacity: isMobile ? 1 : 0,
|
||||
right: 0,
|
||||
p: 1,
|
||||
mr: 1,
|
||||
my: 1,
|
||||
bg: "bgSecondary",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
border: "1px solid var(--codeBorder)",
|
||||
":hover": { bg: "bgSecondaryHover" },
|
||||
borderColor: enabled ? "primary" : "codeBorder",
|
||||
"div:hover > &": { opacity: isMobile ? 0 : 1 }
|
||||
}}
|
||||
title={enabled ? "Copied to clipboard" : "Copy to clipboard"}
|
||||
onClick={() => {
|
||||
writeText(node.textContent);
|
||||
start();
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
path={enabled ? Icons.check : Icons.copy}
|
||||
size={"big"}
|
||||
color={enabled ? "primary" : "icon"}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
</Flex>
|
||||
<ResponsivePresenter
|
||||
isOpen={isOpen}
|
||||
|
||||
@@ -99,7 +99,7 @@ export function ImageComponent(
|
||||
[src, dataurl, imageRef, downloadOptions]
|
||||
);
|
||||
|
||||
if (source) IMAGE_SOURCE_CACHE[hash] = source;
|
||||
if (source && hash) IMAGE_SOURCE_CACHE[hash] = source;
|
||||
const relativeHeight = aspectRatio
|
||||
? editor.view.dom.clientWidth / aspectRatio
|
||||
: undefined;
|
||||
|
||||
45
packages/editor/src/hooks/use-timer.ts
Normal file
45
packages/editor/src/hooks/use-timer.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
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 { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export function useTimer(duration: number) {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const timeout = useRef<number>();
|
||||
|
||||
const cancelTimeout = () => {
|
||||
setEnabled(false);
|
||||
clearTimeout(timeout.current);
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
cancelTimeout();
|
||||
setEnabled(true);
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
cancelTimeout();
|
||||
}, duration) as unknown as number;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return cancelTimeout;
|
||||
}, []);
|
||||
|
||||
return { enabled, start };
|
||||
}
|
||||
@@ -40,7 +40,8 @@ function _SplitButton(props: PropsWithChildren<SplitButtonProps>) {
|
||||
ref={ref}
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
flexShrink: 0
|
||||
flexShrink: 0,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<ToolButton
|
||||
|
||||
Reference in New Issue
Block a user