Compare commits

..

54 Commits

Author SHA1 Message Date
Abdullah Atta
92bafbafa7 web: bump version to 2.5.7 2023-07-05 17:22:43 +05:00
Abdullah Atta
2705caf6bd web: do not show update available dialog 2023-07-05 17:18:01 +05:00
Abdullah Atta
8380c7d994 web: rename reminder to notice for in-app notices 2023-07-05 17:00:23 +05:00
Ammar Ahmed
550c0f3854 mobile: fix topic sheet hidden on tablet (#2886) 2023-07-05 16:36:04 +05:00
Abdullah Atta
04cd135295 web: nav menu should be narrow on tablet 2023-07-05 15:46:47 +05:00
Abdullah Atta
e212a080c2 web: hide duplicate buttons on tablet 2023-07-05 15:46:30 +05:00
Abdullah Atta
533abb58db web: various fixes to settings 2023-07-05 15:46:10 +05:00
Abdullah Atta
941a542cc6 desktop: fix start command 2023-07-05 15:44:24 +05:00
ammarahm-ed
5bcda2cd3a editor: fix error Cannot find name 'Writeable' 2023-07-05 13:52:08 +05:00
Abdullah Atta
869d8ba8f9 config: set git.rebaseWhenSync to true 2023-07-05 13:41:06 +05:00
Abdullah Atta
984e8f8780 global: use node scripts directly everywhere 2023-07-05 13:40:09 +05:00
ammarahm-ed
750b61fb75 mobile: fix size scaling on tablets 2023-07-05 13:29:08 +05:00
Abdullah Atta
4a2defa300 web: allow changing primary 2fa method 2023-07-05 13:24:09 +05:00
Felipe Muñoz Mazur
5c9f7a8bcb editor: add clipboard button to the codeblock (#2710)
Signed-off-by: Felipe Muñoz Mazur <felipe.munoz.mazur@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-07-05 13:09:12 +05:00
Abdullah Atta
b66caa9848 web: fix sync test 2023-07-05 12:14:58 +05:00
Ammar Ahmed
81cac1e54f mobile: fix saving note from share extension (#2874) 2023-07-05 12:07:48 +05:00
Abdullah Atta
36a99cd64f web: add missing change password logic 2023-07-05 11:56:41 +05:00
Abdullah Atta
8c494642e4 editor: do not cache images without hash 2023-07-05 11:56:00 +05:00
Ammar Ahmed
7ab4088331 mobile: optimize screens for very small displays (#2885) 2023-07-05 11:52:13 +05:00
Abdullah Atta
580af33caf Merge branch 'master' of https://github.com/streetwriters/notesnook 2023-07-05 11:49:08 +05:00
Abdullah Atta
bbeeacc480 desktop: use node directly for dev server instead of turbowatch 2023-07-05 11:45:13 +05:00
Ammar Ahmed
51efef5005 mobile: fix tablet mode (#2884) 2023-07-05 09:51:32 +05:00
Abdullah Atta
ee030c8f61 web: fix @trpc/client version 2023-07-05 01:07:44 +05:00
Abdullah Atta
20835e1fb3 web: migrate views to typescript 2023-07-04 22:52:23 +05:00
Abdullah Atta
b542680e7a web: convert home view to typescript (rename to all-notes) 2023-07-04 22:48:14 +05:00
Abdullah Atta
fb32aedd69 web: convert more code to typescript 2023-07-04 22:44:55 +05:00
Abdullah Atta
56f6d34995 web: fix @trpc/{client,server} versions 2023-07-04 22:44:38 +05:00
Abdullah Atta
c68e380a0f web: remove unnecessary code 2023-07-04 22:05:15 +05:00
Abdullah Atta
fc5507bdd3 web: ignore Could not connect to the Sync server error 2023-07-04 18:06:17 +05:00
Abdullah Atta
3f0a656a36 web: force update token if email is already confirmed (#2879) 2023-07-04 16:50:52 +05:00
Abdullah Atta
60667149d4 web: check for internet reachability before sync 2023-07-04 16:43:40 +05:00
Abdullah Atta
cdb3ede207 config: update vscode settings 2023-07-04 16:33:52 +05:00
Abdullah Atta
445494ce95 web: set status as completed whenever sync completes 2023-07-04 16:33:31 +05:00
Abdullah Atta
972f52fd01 core: key should always be present in old attachment 2023-07-04 15:14:40 +05:00
ammarahm-ed
f5e86d668f mobile: set hosts before db setup 2023-07-04 12:19:18 +05:00
Abdullah Atta
0e817236a1 Merge branch 'master' of https://github.com/streetwriters/notesnook 2023-07-04 11:12:31 +05:00
Abdullah Atta
0ea3b3ac2a web: set hosts before database setup 2023-07-04 11:12:24 +05:00
Abdullah Atta
7bfaef82d8 web: debounce page visibility events 2023-07-04 11:11:55 +05:00
ammarahm-ed
f51220c9a8 mobile: remove db.notes.init 2023-07-04 11:02:19 +05:00
Abdullah Atta
88e9ca721a desktop: further improve window creation time 2023-07-04 09:43:17 +05:00
Abdullah Atta
c1ab444646 clipper: update lockfile 2023-07-04 09:43:17 +05:00
Abdullah Atta
b497b53ef2 desktop: fix run command on all platforms 2023-07-04 09:43:17 +05:00
Abdullah Atta
80decaae8a desktop: use app icon path from asset manager 2023-07-04 09:43:17 +05:00
Abdullah Atta
41197cb52c desktop: do not use asar for faster startup 2023-07-04 09:43:17 +05:00
Abdullah Atta
d14abcbba7 desktop: restructure window creation for faster loading 2023-07-04 09:43:17 +05:00
Abdullah Atta
cd36fc8d4a desktop: remove unneeded stat call when loading files 2023-07-04 09:43:17 +05:00
Abdullah Atta
e872487724 desktop: use vitejs for loading the correct bridge 2023-07-04 09:43:17 +05:00
Abdullah Atta
5e7e91caf0 desktop: stop unnecessary animations 2023-07-04 09:43:17 +05:00
Abdullah Atta
a971a29ff2 desktop: load db before app render 2023-07-04 09:43:17 +05:00
Abdullah Atta
a967482c4f core: init notes with database 2023-07-04 09:43:17 +05:00
Abdullah Atta
f91ada3c48 web: use own kvstore instead of localforage
localforage is unmaintained and also has a huge footprint not to mention
the performance issues.
2023-07-04 09:43:17 +05:00
Abdullah Atta
44782d552d web: make compression async 2023-07-04 09:43:17 +05:00
Ammar Ahmed
a51dfb4033 editor: fix toolbar buttons alignment (#2876) 2023-07-03 23:01:35 +05:00
Ammar Ahmed
ccb18e9335 mobile: fix changing font family causes editor crash (#2875) 2023-07-03 21:54:24 +05:00
100 changed files with 1505 additions and 2395 deletions

View File

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

View File

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

View File

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

View File

@@ -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`);
}
}

View 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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,11 +90,7 @@ const AuthModal = () => {
style={{
width: "100%"
}}
contentContainerStyle={{
height: Platform.OS === "android" ? "100%" : "100%"
}}
keyboardShouldPersistTaps="handled"
bounces={false}
>
{currentAuthMode !== AuthMode.login ? (
<Signup

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 }),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 [];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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?: (

View File

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

View File

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

View File

@@ -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();
}

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -72,9 +72,6 @@ class AnnouncementStore extends BaseStore {
};
}
/**
* @type {[import("zustand").UseStore<AnnouncementStore>, AnnouncementStore]}
*/
const [useStore, store] = createStore(AnnouncementStore);
export { useStore, store };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,7 +37,6 @@ function Notebooks() {
items={notebooks}
placeholder={<Placeholder context="notebooks" />}
button={{
content: "Create a notebook",
onClick: () => hashNavigate("/notebooks/create")
}}
/>

View File

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

View File

@@ -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 = [];

View File

@@ -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}`
);
}
}
});

View File

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

View File

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

View File

@@ -32,6 +32,9 @@
"start": {
"dependsOn": ["^build"]
},
"start:desktop": {
"dependsOn": ["^build"]
},
"test:e2e": {
"dependsOn": ["^build"]
},

1172
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -85,7 +85,8 @@ export default class Attachments extends Collection {
...oldAttachment,
...oldAttachment.metadata,
...attachmentArg,
noteIds
noteIds,
key: attachmentArg.key || oldAttachment.key
};
const {

View File

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

View File

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

View File

@@ -48,7 +48,6 @@ export default class CachedCollection extends IndexedCollection {
// }
this.map = new MapStub.Map(data, this.type);
this.invalidateCache();
}
async clear() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 };
}

View File

@@ -40,7 +40,8 @@ function _SplitButton(props: PropsWithChildren<SplitButtonProps>) {
ref={ref}
sx={{
borderRadius: "default",
flexShrink: 0
flexShrink: 0,
alignItems: "center"
}}
>
<ToolButton