Compare commits

..

74 Commits

Author SHA1 Message Date
Ammar Ahmed
dda177a944 mobile: fix encryption on ios 2024-12-21 14:15:29 +05:00
Ammar Ahmed
cc75f6588a core: fix fallback key encryption 2024-12-21 14:14:24 +05:00
Abdullah Atta
ee8701eff8 core: add support for fallback hashing
on ios some users using passwords with multi byte characters
generates the wrong hash. This commit adds support for fallback
to allow those users to continue to login once the fix has been applied.
2024-12-21 14:14:24 +05:00
Ammar Ahmed
fe3c5484c3 mobile: fix encryption on ios 2024-12-21 14:14:24 +05:00
Ammar Ahmed
1e837f2a9c mobile: fix undo/redo buttons in editor 2024-12-21 14:13:07 +05:00
Ammar Ahmed
765ebbe10c mobile: allow saving empty string input 2024-12-21 14:12:24 +05:00
Ammar Ahmed
e95d1601a6 mobile: fix sharing files on android 2024-12-21 14:10:55 +05:00
Ammar Ahmed
c6dd1d1615 mobile: fix snooze time not rendered properly 2024-12-21 14:10:34 +05:00
Ammar Ahmed
bb50850995 Merge pull request #7008 from streetwriters/fix-localization-error 2024-12-21 14:10:05 +05:00
Ammar Ahmed
83083134e9 Merge branch 'master' into fix-localization-error
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-12-21 14:09:22 +05:00
01zulfi
61b8960aa5 web: fix mfa fallback string (#7123)
* also add selected style to mfa type selector button

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-20 15:55:42 +05:00
01zulfi
5229408156 web: fix editor toolbar scroll styling (#7113)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-20 12:30:08 +05:00
01zulfi
0921fef267 web: show account password label in delete vault dialog (#7085)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-11 11:30:42 +05:00
luis-411
60989359e2 theme: make link color align with theme (#7077)
Signed-off-by: Luis Kriner <luis@kriner.info>
2024-12-10 11:01:04 +05:00
luis-411
8fe381afa9 editor: prevent creation of multiple empty notes (#6922)
Signed-off-by: Luis Kriner <luis@kriner.info>
2024-12-10 10:30:19 +05:00
01zulfi
fea9c4e42b web: fix links in report issue dialog (#7071)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-10 10:28:17 +05:00
01zulfi
1d78402d5d web: fix tags not showing in locked note (#7061)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-10 09:58:13 +05:00
01zulfi
aa72c33aea webclipper: handle default case in listener (#7058)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-10 09:57:23 +05:00
01zulfi
69504a150e core: handle error in parseInternalLink (#7065)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-10 09:56:47 +05:00
dyw770
a78a2e4ae2 core: fix object-id is sometimes less than 24 chars in length (#6991) (#6994)
Signed-off-by: dyw770 <dyw770@qq.com>
2024-12-06 10:11:57 +05:00
luis-411
0ffbf83141 editor: hide edit link button when in read only mode (#7018)
Signed-off-by: Luis Kriner <luis@kriner.info>
2024-12-06 09:46:44 +05:00
01zulfi
75c346a460 docs: add README.md for web clipper (#7054)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-05 10:55:28 +05:00
01zulfi
93af79926d editor: disable spellcheck for inline code (#7036)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-05 10:49:40 +05:00
01zulfi
1b354e4bca web: hide spin buttons in mfa code input (#7048)
* they overlap with the resend button

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-05 10:26:54 +05:00
01zulfi
6d53b78f6a web: round off zoom value
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-28 09:59:32 +05:00
Ammar Ahmed
8e90adccab mobile: fix localization issues 2024-11-27 14:02:22 +05:00
Abdullah Atta
c19635b16a web: fix direct index access of strings 2024-11-27 13:14:44 +05:00
Abdullah Atta
94e4b00294 intl: fix direct access of strings in object 2024-11-27 13:14:12 +05:00
Abdullah Atta
76908b1fbd intl: generate action for when data type is unknown 2024-11-27 12:27:09 +05:00
Ammar Ahmed
7409c5382d mobile: fix localization error on searching notebook/tags 2024-11-27 10:41:52 +05:00
01zulfi
2462eb1631 monograph: remove callout headings from toc (#6875) (#6993)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-26 16:27:32 +05:00
Abdullah Atta
986ff32293 monograph: bump version to 1.2.2 2024-11-26 16:23:00 +05:00
Abdullah Atta
8f7fd9d8ee monograph: use fallback fonts when rendering og image 2024-11-26 16:22:48 +05:00
01zulfi
54d6a5c307 monograph: fix PUBLIC_URL on client
* use loader to get PUBLIC_URL

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-26 15:27:43 +05:00
Abdullah Atta
d672a43a83 monograph: bump version to 1.2.1 2024-11-26 11:58:54 +05:00
Abdullah Atta
48343e5d5c web: do not zoom if e.deltaY is 0 2024-11-25 16:26:02 +05:00
Levi Ob
0d3a8c88c8 web: fix inconsistent zoom amount when adjusting with scroll wheel. (#6990)
Zoom steps are always in increments of 10 when using a mouse wheel or trackpad.

Signed-off-by: Levi Ob <leviob.msrni@slmail.me>
2024-11-25 12:36:32 +05:00
Abdullah Atta
31367066f7 web: run prerequisite checks on app start
this gives useful errors to users if their browser doesn't support something
2024-11-25 12:33:05 +05:00
Abdullah Atta
f7c097988a web: fix cannot read property clear of undefined when pressing fix it 2024-11-25 12:31:38 +05:00
Abdullah Atta
b7f1f36090 web: fix async sqlite
async sqlite mode uses indexeddb and has some limitations like:
- running 1 query at a time
This commit uses a mutex to run all queries sequentially
2024-11-25 12:31:11 +05:00
01zulfi
8756299d80 web: always show desktop backup directory path (#6867)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-11-25 10:26:30 +05:00
dyw770
3525d60f8c monograph: add monograph response charset header
Signed-off-by: dyw770 <dyw770@qq.com>
2024-11-24 23:19:36 +05:00
Abdullah Atta
d08a485111 desktop: extract better-sqlite3 patching to its own module 2024-11-24 22:01:28 +05:00
Abdullah Atta
c203b035c4 monograph: bump version to 1.2.0 2024-11-24 15:38:20 +05:00
Abdullah Atta
0f4b2bdae7 monograph: fix Cannot find module '@remix-run/server-runtime' 2024-11-24 15:24:46 +05:00
Abdullah Atta
cb0bc39cd1 monograph: use bun in prod 2024-11-24 15:18:52 +05:00
Abdullah Atta
84abfa3478 monograph: speed up og image generation
this gets rid of satori and uses custom logic for drawing using @napi-rs/canvas
2024-11-24 15:18:52 +05:00
thecodrr
a40a48e2c1 server(theme): add installs.json to gitignore 2024-11-23 17:48:34 +00:00
Abdullah Atta
124499cabb monograph: use sharp output as buffer instead of stream 2024-11-23 22:23:42 +05:00
Ammar Ahmed
e874d1efd4 mobile: changelog 2024-11-23 22:20:05 +05:00
Ammar Ahmed
fb1a17566f mobile: release v3.0.23 2024-11-23 22:17:42 +05:00
Abdullah Atta
8dd8447212 monograph: cache satori output in lru cache
satori is sloooooow so we need to cache.
find another solution for og generation
2024-11-23 22:16:47 +05:00
Ammar Ahmed
eba7341d45 mobile: restore editor state only if not restored already 2024-11-23 22:15:57 +05:00
Ammar Ahmed
08bd36d518 mobile: fix deps 2024-11-23 22:15:57 +05:00
Ammar Ahmed
7a45522ff3 mobile: make attachment item pressable 2024-11-23 22:15:57 +05:00
Ammar Ahmed
4ee2923181 mobile: fix attachment downloads failing on mobile 2024-11-23 22:15:57 +05:00
Abdullah Atta
48459efdb1 monograph: mark sharp as external in ssr build 2024-11-23 21:45:44 +05:00
Abdullah Atta
c848f3c588 monograph: mark sharp as external dep in vite 2024-11-23 21:44:24 +05:00
Abdullah Atta
52a75cfdaf monograph: install sharp with optional deps during docker 2024-11-23 21:42:28 +05:00
Abdullah Atta
9331b5f919 monograph: bump version to 1.1.1 2024-11-23 21:33:37 +05:00
Abdullah Atta
2ff53d4650 monograph: use sharp for generating og images 2024-11-23 21:33:21 +05:00
Abdullah Atta
7f2c8ca05a web: fix checkout snapshots 2024-11-23 14:56:58 +05:00
Abdullah Atta
9e752e9406 web: bump version to 3.0.22 2024-11-23 14:53:31 +05:00
Abdullah Atta
b8c3e2098f web: fix one-time discount wrongly shown as recurring 2024-11-23 14:39:45 +05:00
Abdullah Atta
4308e3bf49 web: fix editor zoom on trackpads 2024-11-23 14:03:31 +05:00
Abdullah Atta
25956ccadb web: remove auto item reveal when opening items 2024-11-23 13:31:23 +05:00
Abdullah Atta
af3fba5176 core: lazily use db in config & kv stores 2024-11-23 13:28:55 +05:00
Ammar Ahmed
4afaa6a36e mobile: fix reminder visual issues 2024-11-23 13:00:57 +05:00
Ammar Ahmed
e98e1a5f36 mobile: fix pdf-preview crash 2024-11-23 12:24:17 +05:00
01zulfi
abfdad0fec web: fix setting item width (#6909) (#6935)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2024-11-23 12:16:26 +05:00
dyw770
99e50ddad6 monograph: fix monograph version endpoints cors
Signed-off-by: dyw770 <dyw770@qq.com>
2024-11-23 12:05:29 +05:00
Ammar Ahmed
67a61dd3f3 mobile: release v3.0.22 2024-11-20 12:40:37 +05:00
Ammar Ahmed
13df01d55a mobile: fix readonly editor not loading 2024-11-20 12:38:17 +05:00
Ammar Ahmed
af4ea036c8 mobile: fix sync on iOS 2024-11-20 12:37:38 +05:00
155 changed files with 5024 additions and 4039 deletions

View File

@@ -10,7 +10,7 @@ const authors = readFileSync("AUTHORS", "utf-8");
const isAuthor = authors.includes(`<${authorEmail}>`);
const SCOPES = [
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
"mobile",
"web",
@@ -36,7 +36,8 @@ const SCOPES = [
"global",
"docs",
"themebuilder",
"intl"
"intl",
"webclipper"
];
module.exports = {

View File

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

View File

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

View File

@@ -18,19 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import fs, { readFile, writeFile } from "fs/promises";
import { existsSync, readFileSync } from "fs";
import fs from "fs/promises";
import { existsSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
import * as childProcess from "child_process";
import { fileURLToPath } from "url";
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
);
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
@@ -46,12 +44,6 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
// temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3();
// if (os.platform() === "win32")
// await exec(
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
// path.join(__dirname, "..", "node_modules", "sodium-native")
// );
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
@@ -83,21 +75,3 @@ async function exec(cmd, cwd) {
cwd: cwd || process.cwd()
});
}
async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}

View File

@@ -0,0 +1,48 @@
/*
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 { readFile, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}
if (process.argv[1] === __filename) {
console.log("Patching better-sqlite3");
patchBetterSQLite3();
}

View File

@@ -17,15 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
import { SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { MMKV } from "./mmkv";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
@@ -62,6 +63,7 @@ const KEYSTORE_CONFIG = Platform.select({
function generatePassword() {
const length = 80;
//@ts-ignore
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
@@ -78,27 +80,27 @@ function generatePassword() {
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
const key = getDatabaseKey();
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
const key = (await getDatabaseKey()) as string;
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = await encrypt(appLockCredentials, key);
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = await decrypt(
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = (await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
)) as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
@@ -110,13 +112,16 @@ export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
return true;
}
export async function setAppLockVerificationCipher(appLockPassword) {
export async function setAppLockVerificationCipher(appLockPassword: string) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = await encrypt(appLockCredentials, generatePassword());
const encrypted = (await encrypt(
appLockCredentials,
generatePassword()
)) as Cipher;
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
@@ -129,9 +134,9 @@ export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword) {
export async function validateAppLockPassword(appLockPassword: string) {
try {
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
@@ -146,17 +151,18 @@ export async function validateAppLockPassword(appLockPassword) {
}
}
let DB_KEY;
let DB_KEY: string | undefined;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword) {
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKeyCipher: Cipher =
CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
@@ -172,13 +178,12 @@ export async function getDatabaseKey(appLockPassword) {
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
let credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
KEYSTORE_CONFIG
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = credentials.password;
DB_KEY = (credentials as Keychain.UserCredentials).password;
}
}
@@ -190,7 +195,7 @@ export async function getDatabaseKey(appLockPassword) {
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key;
DB_KEY = derivedDatabaseKey.key as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
@@ -202,18 +207,17 @@ export async function getDatabaseKey(appLockPassword) {
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
"notesnook"
);
if (userKeyCredentials) {
const userKeyCipher = await encrypt(
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
);
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
@@ -223,45 +227,84 @@ export async function getDatabaseKey(appLockPassword) {
return DB_KEY;
} catch (e) {
ToastManager.error(e, "Error getting database key");
ToastManager.error(e as Error, "Error getting database key");
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKey(data) {
export async function deriveCryptoKeyFallback(data: SerializedKey) {
if (Platform.OS !== "ios") return;
try {
let credentials = await Sodium.deriveKey(data.password, data.salt);
const userKeyCipher = await encrypt(
if (!data.password || !data.salt)
throw new Error(
"Invalid password and salt provided to deriveCryptoKeyFallback"
);
const credentials = await Sodium.deriveKeyFallback?.(
data.password,
data.salt
);
if (!credentials) return;
const userKeyCipher = (await encrypt(
{
key: await getDatabaseKey(),
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key
);
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key fallback stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey(_name) {
export async function deriveCryptoKey(data: SerializedKey) {
try {
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
const credentials = (await Sodium.deriveKey(
data.password,
data.salt
)) as Password;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey() {
try {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return null;
return undefined;
}
const key = await decrypt(
{
key: await getDatabaseKey(),
key: (await getDatabaseKey()) as string,
salt: keyCipher.salt
},
keyCipher
@@ -274,7 +317,7 @@ export async function getCryptoKey(_name) {
}
}
export async function removeCryptoKey(_name) {
export async function removeCryptoKey() {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
@@ -284,44 +327,69 @@ export async function removeCryptoKey(_name) {
}
}
export async function getRandomBytes(length) {
export async function getRandomBytes(length: number) {
return await generateSecureRandom(length);
}
export async function hash(password, email) {
let result = await Sodium.hashPassword(password, email);
return result;
}
export async function hash(
password: string,
email: string,
options?: { usesFallback?: boolean }
) {
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
export async function generateCryptoKey(password, salt) {
try {
let credentials = await Sodium.deriveKey(password, salt || null);
return credentials;
} catch (e) {
DatabaseLogger.error(e);
if (options?.usesFallback && Platform.OS !== "ios") {
return null;
}
return (
options?.usesFallback
? await Sodium.hashPasswordFallback?.(password, email)
: await Sodium.hashPassword(password, email)
) as string;
}
export function getAlgorithm(base64Variant) {
export async function generateCryptoKey(password: string, salt?: string) {
return (await Sodium.deriveKey(password, salt)) as Promise<SerializedKey>;
}
export function getAlgorithm(base64Variant: number) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let _data = { ...data };
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
const _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decrypt(key, _data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decrypt(fallbackKey, _data);
} else {
throw e;
}
}
}
return await Sodium.decrypt(password, _data);
}
export async function decryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
export async function decryptMulti(
password: Password,
data: Cipher<"base64">[]
) {
data = data.map((d) => {
d.output = "plain";
return d;
@@ -331,10 +399,30 @@ export async function decryptMulti(password, data) {
password.salt = data[0].salt;
}
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decryptMulti(key, data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt as string
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decryptMulti(fallbackKey, data);
} else {
throw e;
}
}
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg) {
export function parseAlgorithm(alg: string) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
@@ -346,16 +434,11 @@ export function parseAlgorithm(alg) {
};
}
export async function encrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let message = {
export async function encrypt(password: SerializedKey, plainText: string) {
const result = await Sodium.encrypt<"base64">(password, {
type: "plain",
data: data
};
let result = await Sodium.encrypt(password, message);
data: plainText
});
return {
...result,
@@ -363,14 +446,13 @@ export async function encrypt(password, data) {
};
}
export async function encryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let results = await Sodium.encryptMulti(
export async function encryptMulti(
password: SerializedKey,
plainText: string[]
) {
const results = await Sodium.encryptMulti<"base64">(
password,
data.map((item) => ({
plainText.map((item) => ({
type: "plain",
data: item
}))

View File

@@ -16,26 +16,26 @@ 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 "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger } from "@notesnook/core";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { logger as dbLogger, ICompressor } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import SettingsService from "../../services/settings";
import { strings } from "@notesnook/intl";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { FileStorage } from "../filesystem";
import { getDatabaseKey } from "./encryption";
import "./logger";
import { RNSqliteDriver } from "./sqlite.kysely";
import { Storage } from "./storage";
export async function setupDatabase(password) {
export async function setupDatabase(password?: string) {
const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed());
@@ -47,17 +47,21 @@ export async function setupDatabase(password) {
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
...(SettingsService.getProperty("serverUrls") || {})
});
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: () => ({
compress: Gzip.deflate,
decompress: Gzip.inflate
}),
eventsource: (Platform.OS === "ios"
? EventSource
: AndroidEventSource) as any,
fs: FileStorage,
compressor: async () =>
({
compress: Gzip.deflate,
decompress: Gzip.inflate
} as ICompressor),
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({

View File

@@ -26,7 +26,7 @@ import { CompiledQuery } from "@streetwriters/kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
import { strings } from "@notesnook/intl";
type Config = { dbName: string; async: boolean; location: string };
type Config = { dbName: string; async: boolean; location?: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;

View File

@@ -16,58 +16,51 @@ 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 { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { IStorage } from "@notesnook/core";
import { MMKVInstance } from "react-native-mmkv-storage";
import {
decrypt,
decryptMulti,
deriveCryptoKey,
deriveCryptoKeyFallback,
encrypt,
encryptMulti,
generateCryptoKey,
getCryptoKey,
getRandomBytes,
hash,
removeCryptoKey
hash
} from "./encryption";
import { MMKV } from "./mmkv";
export class KV {
/**
* @type {typeof MMKV}
*/
storage = null;
constructor(storage) {
storage: MMKVInstance;
constructor(storage: MMKVInstance) {
this.storage = storage;
}
async read(key) {
if (!key) return null;
let data = this.storage.getString(key);
if (!data) return null;
async read<T>(key: string, isArray?: boolean) {
if (!key) return undefined;
const data = this.storage.getString(key);
if (!data) return undefined;
try {
let parse = JSON.parse(data);
return parse;
return JSON.parse(data) as T;
} catch (e) {
return data;
return data as T;
}
}
async write(key, data) {
async write<T>(key: string, data: T) {
this.storage.setString(
key,
typeof data === "string" ? data : JSON.stringify(data)
);
return true;
}
async readMulti(keys) {
async readMulti<T>(keys: string[]) {
if (keys.length <= 0) {
return [];
} else {
try {
let data = await this.storage.getMultipleItemsAsync(
const data = await this.storage.getMultipleItemsAsync<any>(
keys.slice(),
"string"
);
@@ -79,24 +72,24 @@ export class KV {
obj = value;
}
return [key, obj];
});
}) as [string, T][];
} catch (e) {
console.log(e);
return [];
}
}
}
async remove(key) {
return this.storage.removeItem(key);
async remove(key: string) {
this.storage.removeItem(key);
}
async removeMulti(keys) {
if (!keys) return true;
return this.storage.removeItems(keys);
async removeMulti(keys: string[]) {
if (!keys) return;
this.storage.removeItems(keys);
}
async clear() {
return this.storage.clearStore();
this.storage.clearStore();
}
async getAllKeys() {
@@ -113,54 +106,45 @@ export class KV {
return keys;
}
async writeMulti(items) {
return this.storage.setMultipleItemsAsync(items, "object");
async writeMulti(items: [string, any][]) {
await this.storage.setMultipleItemsAsync(items, "object");
}
}
const DefaultStorage = new KV(MMKV);
async function requestPermission() {
if (Platform.OS === "ios") return true;
return true;
}
async function checkAndCreateDir(path) {
let dir =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.DocumentDir + path
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
try {
let exists = await RNFetchBlob.fs.exists(dir);
let isDir = await RNFetchBlob.fs.isDir(dir);
if (!exists || !isDir) {
await RNFetchBlob.fs.mkdir(dir);
}
} catch (e) {
await RNFetchBlob.fs.mkdir(dir);
}
return dir;
}
export default {
read: (key) => DefaultStorage.read(key),
write: (key, value) => DefaultStorage.write(key, value),
readMulti: (keys) => DefaultStorage.readMulti(keys),
remove: (key) => DefaultStorage.remove(key),
clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
writeMulti: (items) => DefaultStorage.writeMulti(items),
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
export const Storage: IStorage = {
write<T>(key: string, data: T): Promise<void> {
return DefaultStorage.write(key, data);
},
writeMulti<T>(entries: [string, T][]): Promise<void> {
return DefaultStorage.writeMulti(entries);
},
readMulti<T>(keys: string[]): Promise<[string, T][]> {
return DefaultStorage.readMulti(keys);
},
read<T>(key: string, isArray?: boolean): Promise<T | undefined> {
return DefaultStorage.read(key, isArray);
},
remove(key: string): Promise<void> {
return DefaultStorage.remove(key);
},
removeMulti(keys: string[]): Promise<void> {
return DefaultStorage.removeMulti(keys);
},
clear(): Promise<void> {
return DefaultStorage.clear();
},
getAllKeys(): Promise<string[]> {
return DefaultStorage.getAllKeys();
},
hash,
getCryptoKey,
encrypt,
encryptMulti,
decrypt,
decryptMulti,
getRandomBytes,
checkAndCreateDir,
requestPermission,
deriveCryptoKey,
getCryptoKey,
removeCryptoKey,
hash,
generateCryptoKey,
encryptMulti
deriveCryptoKeyFallback
};

View File

@@ -16,7 +16,6 @@ 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 Sodium from "@ammarahmed/react-native-sodium";
import { getFileNameWithExtension } from "@notesnook/core";
import { strings } from "@notesnook/intl";
@@ -26,11 +25,11 @@ import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import Storage from "../database/storage";
import filesystem from "../filesystem";
import { createCacheDir, exists } from "./io";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
@@ -51,23 +50,23 @@ export async function downloadAllAttachments() {
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachments
* @param {string[]} attachmentIds
* @param onProgress
* @returns
*/
export async function downloadAttachments(attachments) {
export async function downloadAttachments(attachmentIds: string[]) {
await createCacheDir();
if (!attachments || !attachments.length) return;
if (!attachmentIds || !attachmentIds.length) return;
const groupId = `download-all-${Date.now()}`;
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
let file = await ScopedStorage.openDocumentTree(true);
const file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await Storage.checkAndCreateDir("/downloads/");
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
@@ -83,7 +82,7 @@ export async function downloadAttachments(attachments) {
await RNFetchBlob.fs.mkdir(zipSourceFolder);
const isCancelled = () => {
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
useAttachmentStore.getState().setDownloading({
groupId,
@@ -97,19 +96,21 @@ export async function downloadAttachments(attachments) {
}
};
for (let i = 0; i < attachments.length; i++) {
for (let i = 0; i < attachmentIds.length; i++) {
if (isCancelled()) return;
let attachment = await db.attachments.attachment(attachments[i]);
const attachment = await db.attachments.attachment(attachmentIds[i]);
if (!attachment) continue;
const hash = attachment.hash;
try {
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: i + 1,
total: attachments.length,
total: attachmentIds.length,
filename: attachment.hash
});
// Download to cache
let uri = await downloadAttachment(hash, false, {
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
@@ -184,7 +185,7 @@ export async function downloadAttachments(attachments) {
});
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e, "Error zipping attachments");
ToastManager.error(e as Error, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -194,49 +195,71 @@ export async function downloadAttachments(attachments) {
}
export default async function downloadAttachment(
hash,
hashOrId: string,
global = true,
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
options?: {
silent?: boolean;
cache?: boolean;
throwError?: boolean;
groupId?: string;
base64?: boolean;
text?: boolean;
}
) {
await createCacheDir();
let attachment = await db.attachments.attachment(hash);
const attachment = await db.attachments.attachment(hashOrId);
if (!attachment) {
DatabaseLogger.log("Attachment not found");
DatabaseLogger.log("Attachment not found", {
hash: hashOrId
});
return;
}
let folder = {};
if (!options.cache) {
let folder: {
uri: string;
} | null = null;
if (!options?.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
folder = {
uri: await filesystem.checkAndCreateDir("/downloads/")
};
}
}
try {
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 0,
total: 1,
filename: attachment.filename
});
await db
.fs()
.downloadFile(
options.groupId || attachment.hash,
options?.groupId || attachment.hash,
attachment.hash,
attachment.chunkSize
);
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 1,
total: 1,
filename: attachment.filename,
success: true
});
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
}
if (options.base64 || options.text) {
if (options?.base64 || options?.text) {
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
@@ -244,14 +267,16 @@ export default async function downloadAttachment(
);
}
let filename = await getFileNameWithExtension(
const filename = await getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
let key = await db.attachments.decryptKey(attachment.key);
const key = await db.attachments.decryptKey(attachment.key);
let info = {
if (!key) return;
const info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
@@ -259,18 +284,18 @@ export default async function downloadAttachment(
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
fileName: options?.cache ? undefined : filename,
uri: options?.cache ? undefined : folder?.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
};
let fileUri = await Sodium.decryptFile(
key,
info,
options.cache ? "cache" : "file"
options?.cache ? "cache" : "file"
);
if (!options.silent) {
if (!options?.silent) {
ToastManager.show({
heading: strings.network.downloadSuccess(),
message: strings.network.fileDownloaded(filename),
@@ -278,15 +303,15 @@ export default async function downloadAttachment(
});
}
if (Platform.OS === "ios" && !options.cache) {
fileUri = folder.uri + `/${filename}`;
if (Platform.OS === "ios" && !options?.cache) {
fileUri = folder?.uri + `/${filename}`;
}
if (!options.silent) {
if (!options?.silent) {
presentSheet({
title: strings.network.fileDownloaded(),
paragraph: strings.fileSaved(filename, Platform.OS),
icon: "download",
context: global ? null : attachment.hash,
context: global ? "global" : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
@@ -301,9 +326,17 @@ export default async function downloadAttachment(
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.catch(console.log);
}
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 0,
total: 0,
filename: attachment.filename,
success: false
});
DatabaseLogger.error(e);
useAttachmentStore.getState().remove(attachment.hash);
if (options.throwError) {
if (options?.throwError) {
throw e;
}
}

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { RequestOptions } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
@@ -26,7 +27,13 @@ import { DatabaseLogger, db } from "../database";
import { createCacheDir, exists } from "./io";
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
export async function downloadFile(filename, requestOptions, cancelToken) {
export async function downloadFile(
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) {
if (!requestOptions) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: No requestOptions`
@@ -36,9 +43,11 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
let { url, headers, chunkSize } = requestOptions;
let tempFilePath = `${cacheDir}/${filename}_temp`;
let originalFilePath = `${cacheDir}/${filename}`;
const { url, headers, chunkSize } = requestOptions;
const tempFilePath = `${cacheDir}/${filename}_temp`;
const originalFilePath = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
DatabaseLogger.log(`File Exists already: ${filename}`);
@@ -46,6 +55,8 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
const size = await getUploadedFileSize(filename);
if (size === -1) {
@@ -71,21 +82,21 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
throw new Error(error);
}
let res = await fetch(url, {
const resolveUrlResponse = await fetch(url, {
method: "GET",
headers
});
if (!res.ok) {
if (!resolveUrlResponse.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
);
throw new Error(
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
);
}
const downloadUrl = await res.text();
const downloadUrl = await resolveUrlResponse.text();
if (!downloadUrl) {
DatabaseLogger.log(
@@ -95,12 +106,12 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
}
DatabaseLogger.log(`Download starting: ${filename}`);
let request = RNFetchBlob.config({
const request = RNFetchBlob.config({
path: tempFilePath,
IOSBackgroundTask: true,
overwrite: true
})
.fetch("GET", downloadUrl, null)
.fetch("GET", downloadUrl)
.progress(async (recieved, total) => {
useAttachmentStore
.getState()
@@ -109,15 +120,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
cancelToken.cancel = () => {
cancelToken.cancel = async (reason) => {
useAttachmentStore.getState().remove(filename);
request.cancel();
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
DatabaseLogger.log(`Download cancelled: ${filename}`);
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
};
let response = await request;
console.log(response.info().headers);
const response = await request;
const contentType =
response.info().headers?.["content-type"] ||
@@ -128,10 +138,10 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
throw new Error(`[${error.Code}] ${error.Message}`);
}
let status = response.info().status;
const status = response.info().status;
useAttachmentStore.getState().remove(filename);
if (exists(originalFilePath)) {
if (await exists(originalFilePath)) {
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
}
@@ -143,11 +153,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
if (
(e as Error).message !== "canceled" &&
!(e as Error).message.includes("NoSuchKey")
) {
const toast = {
heading: strings.downloadError(),
message: e.message,
type: "error",
heading: strings.downloadError((e as Error).message),
message: (e as Error).message,
type: "error" as const,
context: "global"
};
ToastManager.show(toast);
@@ -158,15 +171,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
DatabaseLogger.error(e, {
url,
headers
DatabaseLogger.error(e, "Download failed: ", {
url
});
return false;
}
}
export async function checkAttachment(hash) {
export async function checkAttachment(hash: string) {
const internetState = await NetInfo.fetch();
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
@@ -184,7 +196,7 @@ export async function checkAttachment(hash) {
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
};
} catch (e) {
return { failed: e?.message };
return { failed: (e as Error)?.message };
}
return { success: true };
}

View File

@@ -17,24 +17,40 @@ 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 { IFileStorage } from "@notesnook/core";
import { checkAttachment, downloadFile } from "./download";
import {
bulkExists,
clearCache,
clearFileStorage,
deleteCacheFileByName,
deleteCacheFileByPath,
deleteFile,
exists,
readEncrypted,
writeEncryptedBase64,
getCacheSize,
hashBase64,
readEncrypted,
writeEncryptedBase64
} from "./io";
import { uploadFile } from "./upload";
import {
cancelable,
checkAndCreateDir,
getUploadedFileSize,
requestPermission
} from "./utils";
export default {
checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
bulkExists,
getCacheSize
} from "./io";
import { uploadFile } from "./upload";
import { cancelable, getUploadedFileSize } from "./utils";
getCacheSize,
requestPermission,
checkAndCreateDir
};
export default {
export const FileStorage: IFileStorage = {
readEncrypted,
writeEncryptedBase64,
hashBase64,
@@ -44,10 +60,5 @@ export default {
exists,
clearFileStorage,
getUploadedFileSize,
checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
bulkExists,
getCacheSize
bulkExists
};

View File

@@ -18,6 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import {
FileEncryptionMetadataWithHash,
FileEncryptionMetadataWithOutputType,
Output,
RequestOptions
} from "@notesnook/core";
import { DataFormat, SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { eSendEvent } from "../../services/event-manager";
@@ -25,17 +32,21 @@ import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted(filename, key, cipherData) {
export async function readEncrypted<TOutputFormat extends DataFormat>(
filename: string,
key: SerializedKey,
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
) {
await migrateFilesFromCache();
DatabaseLogger.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
const path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
return false;
return;
}
let output = await Sodium.decryptFile(
const output = await Sodium.decryptFile(
key,
{
...cipherData,
@@ -47,15 +58,14 @@ export async function readEncrypted(filename, key, cipherData) {
DatabaseLogger.log("File decrypted...");
return output;
return output as Output<TOutputFormat>;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e);
return false;
}
}
export async function hashBase64(data) {
export async function hashBase64(data: string) {
const hash = await Sodium.hashFile({
type: "base64",
data,
@@ -67,61 +77,70 @@ export async function hashBase64(data) {
};
}
export async function writeEncryptedBase64(data, key) {
export async function writeEncryptedBase64(
data: string,
encryptionKey: SerializedKey,
mimeType: string
): Promise<FileEncryptionMetadataWithHash> {
await createCacheDir();
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
let output = await Sodium.encryptFile(key, {
const output = await Sodium.encryptFile(encryptionKey, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
output.size = output.length;
delete output.length;
return {
...output,
alg: "xcha-stream"
};
}
export async function deleteFile(filename, data) {
export async function deleteFile(
filename: string,
requestOptions?: RequestOptions
): Promise<boolean> {
await createCacheDir();
let delFilePath = cacheDir + `/${filename}`;
if (!data) {
if (!filename) return;
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
const localFilePath = cacheDir + `/${filename}`;
if (!requestOptions) {
RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
return true;
}
let { url, headers } = data;
const { url, headers } = requestOptions;
try {
let response = await RNFetchBlob.fetch("DELETE", url, headers);
let status = response.info().status;
let ok = status >= 200 && status < 300;
const response = await RNFetchBlob.fetch("DELETE", url, headers);
const status = response.info().status;
const ok = status >= 200 && status < 300;
if (ok) {
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
}
return ok;
} catch (e) {
console.log("delete file: ", e, url, headers);
DatabaseLogger.error(e, "Delete file", {
url: url
});
return false;
}
}
export async function clearFileStorage() {
try {
let files = await RNFetchBlob.fs.ls(cacheDir);
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
const files = await RNFetchBlob.fs.ls(cacheDir);
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of files) {
for (const file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
}
for (let file of oldCache) {
for (const file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
}
} catch (e) {
console.log("clearFileStorage", e);
DatabaseLogger.error(e, "clearFileStorage");
}
}
@@ -146,11 +165,11 @@ export async function migrateFilesFromCache() {
return;
}
let files = await RNFetchBlob.fs.ls(cacheDir);
const files = await RNFetchBlob.fs.ls(cacheDir);
console.log("Files to migrate:", files.join(","));
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of oldCache) {
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
@@ -159,7 +178,7 @@ export async function migrateFilesFromCache() {
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
console.log("migrateFilesFromCache", e);
DatabaseLogger.error(e, "migrateFilesFromCache");
}
}
@@ -169,14 +188,14 @@ export async function clearCache() {
eSendEvent("cache-cleared");
}
export async function deleteCacheFileByPath(path) {
export async function deleteCacheFileByPath(path: string) {
await RNFetchBlob.fs.unlink(path).catch(console.log);
}
export async function deleteCacheFileByName(name) {
export async function deleteCacheFileByName(name: string) {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${name}`;
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
@@ -192,12 +211,12 @@ export async function deleteDCacheFiles() {
}
}
export async function exists(filename) {
let path = `${cacheDir}/${filename}`;
export async function exists(filename: string) {
const path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
@@ -211,6 +230,7 @@ export async function exists(filename) {
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
@@ -234,7 +254,7 @@ export async function exists(filename) {
return exists;
}
export async function bulkExists(files) {
export async function bulkExists(files: string[]) {
try {
await createCacheDir();
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
@@ -243,7 +263,7 @@ export async function bulkExists(files) {
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter(
@@ -262,8 +282,8 @@ export async function getCacheSize() {
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
console.log("Total files", stat.length);
stat.forEach((s) => {
total += parseInt(s.size);
stat.forEach((file) => {
total += parseInt(file.size as unknown as string);
});
return total;
}

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
@@ -24,11 +25,22 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir } from "./io";
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
import {
cacheDir,
checkUpload,
FileSizeResult,
getUploadedFileSize
} from "./utils";
export async function uploadFile(filename, requestOptions, cancelToken) {
export async function uploadFile(
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) {
if (!requestOptions) return false;
let { url, headers } = requestOptions;
const { url, headers } = requestOptions;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
@@ -39,7 +51,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
if (!exists && Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
filePath = appGroupPath;
@@ -54,14 +66,15 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
let remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === -1) return false;
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
const remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === FileSizeResult.Error) return false;
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let uploadUrlResponse = await fetch(url, {
const uploadUrlResponse = await fetch(url, {
method: "PUT",
headers
});
@@ -78,7 +91,8 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.info(`Starting upload: ${filename}`);
let uploadRequest = RNFetchBlob.config({
const uploadRequest = RNFetchBlob.config({
//@ts-ignore
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
@@ -98,14 +112,14 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
);
});
cancelToken.cancel = () => {
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
uploadRequest.cancel();
};
let uploadResponse = await uploadRequest;
let status = uploadResponse.info().status;
let uploaded = status >= 200 && status < 300;
const uploadResponse = await uploadRequest;
const status = uploadResponse.info().status;
const uploaded = status >= 200 && status < 300;
useAttachmentStore.getState().remove(filename);
@@ -118,12 +132,13 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
);
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
return uploaded;
} catch (e) {
useAttachmentStore.getState().remove(filename);
ToastManager.error(e, "File upload failed");
ToastManager.error(e as Error, "File upload failed");
DatabaseLogger.error(e, "File upload failed", {
filename
});

View File

@@ -17,11 +17,11 @@ 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 { hosts, RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { DatabaseLogger, db } from "../database";
import { hosts } from "@notesnook/core";
export const ABYTES = 17;
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
@@ -31,18 +31,13 @@ export const cacheDir =
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
export function getRandomId(prefix) {
export function getRandomId(prefix: string) {
return Math.random()
.toString(36)
.replace("0.", prefix || "");
}
/**
*
* @param {string | undefined} data
* @returns
*/
export function parseS3Error(data) {
export function parseS3Error(data?: string) {
const xml = typeof data === "string" ? data : null;
const error = {
@@ -70,11 +65,19 @@ export function parseS3Error(data) {
}
}
export function cancelable(operation) {
export function cancelable(
operation: (
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) => Promise<boolean>
) {
const cancelToken = {
cancel: () => {}
cancel: async (reason?: string) => {}
};
return (filename, requestOptions) => {
return (filename: string, requestOptions: RequestOptions) => {
return {
execute: () => operation(filename, requestOptions, cancelToken),
cancel: async () => {
@@ -84,29 +87,35 @@ export function cancelable(operation) {
};
}
export function copyFileAsync(source, dest) {
export function copyFileAsync(source: string, dest: string) {
return new Promise((resolve, reject) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
//@ts-ignore
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
if (e) {
reject(e);
return;
}
resolve();
resolve(true);
});
});
}
export async function releasePermissions(path) {
export async function releasePermissions(path: string) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
for (const uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}
export async function getUploadedFileSize(hash) {
export const FileSizeResult = {
Empty: 0,
Error: -1
};
export async function getUploadedFileSize(hash: string) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
@@ -115,16 +124,20 @@ export async function getUploadedFileSize(hash) {
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
attachmentInfo.headers?.get("content-length") || "0"
);
return isNaN(contentLength) ? 0 : contentLength;
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
} catch (e) {
DatabaseLogger.error(e);
return -1;
return FileSizeResult.Error;
}
}
export async function checkUpload(filename, chunkSize, expectedSize) {
export async function checkUpload(
filename: string,
chunkSize: number,
expectedSize: number
) {
const size = await getUploadedFileSize(filename);
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
@@ -138,3 +151,25 @@ export async function checkUpload(filename, chunkSize, expectedSize) {
: undefined;
if (error) throw new Error(error);
}
export async function requestPermission() {
if (Platform.OS === "ios") return true;
return true;
}
export async function checkAndCreateDir(path: string) {
const dir =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.DocumentDir + path
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
try {
const exists = await RNFetchBlob.fs.exists(dir);
const isDir = await RNFetchBlob.fs.isDir(dir);
if (!exists || !isDir) {
await RNFetchBlob.fs.mkdir(dir);
}
} catch (e) {
await RNFetchBlob.fs.mkdir(dir);
}
return dir;
}

View File

@@ -63,9 +63,11 @@ const Actions = ({
attachment,
close,
setAttachments,
fwdRef
fwdRef,
context
}: {
attachment: Attachment;
context: string;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close?: () => void;
fwdRef: RefObject<ActionSheetRef>;
@@ -79,7 +81,6 @@ const Actions = ({
const [loading, setLoading] = useState<{
name?: string;
}>({});
const actions = [
{
name: strings.network.download(),
@@ -88,7 +89,7 @@ const Actions = ({
await db.fs().cancel(attachment.hash);
useAttachmentStore.getState().remove(attachment.hash);
}
downloadAttachment(attachment.hash, false);
downloadAttachment(attachment.hash, context === "global");
fwdRef.current?.hide();
},
icon: "download"
@@ -375,6 +376,7 @@ Actions.present = (
setAttachments={set}
close={close}
attachment={attachment}
context={context || "global"}
/>
)
});

View File

@@ -31,6 +31,7 @@ import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
import { strings } from "@notesnook/intl";
import { Pressable } from "../ui/pressable";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
@@ -69,8 +70,7 @@ export const AttachmentItem = ({
};
return errorOnly && attachment && !attachment?.failed ? null : (
<TouchableOpacity
activeOpacity={0.9}
<Pressable
onPress={onPress}
style={{
flexDirection: "row",
@@ -186,6 +186,6 @@ export const AttachmentItem = ({
)}
</>
)}
</TouchableOpacity>
</Pressable>
);
};

View File

@@ -206,7 +206,7 @@ export const AttachmentDialog = ({
errorOnly={currentFilter === "errors"}
attachments={attachments}
id={index}
context="global"
context={!isSheet ? "global" : "attachments-list"}
/>
);

View File

@@ -68,9 +68,14 @@ export const ChangePassword = () => {
}
setLoading(true);
try {
const result = await BackupService.run(false, "change-password-dialog");
if (!result.error)
const result = await BackupService.run(
false,
"change-password-dialog",
"partial"
);
if (result.error) {
throw new Error(strings.backupFailed() + `: ${result.error}`);
}
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);

View File

@@ -188,7 +188,7 @@ const PDFPreview = () => {
}}
color={colors.static.white}
>
{strings.loadingWithProgress(progress.percent)}
{strings.loadingWithProgress(progress?.percent)}
</Paragraph>
</Animated.View>
) : (

View File

@@ -460,7 +460,7 @@ export class VaultDialog extends Component {
async _deleteNote() {
try {
await db.vault.remove(this.state.note.id, this.password);
await deleteItems([this.state.note.id], "note");
await deleteItems("note", [this.state.note.id]);
this.close();
} catch (e) {
this._takeErrorAction(e);

View File

@@ -19,9 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef } from "react";
import { Platform, StyleSheet, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import Menu from "react-native-reanimated-material-menu";
import { Menu } from "react-native-material-menu";
import { notesnook } from "../../../e2e/test.ids";
import {
HeaderRightButton,
@@ -97,14 +96,16 @@ export const RightMenus = ({
style={{
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: -40
marginTop: 35
}}
onRequestClose={() => {
//@ts-ignore
menuRef.current?.hide();
}}
anchor={
<IconButton
onPress={() => {
//@ts-ignore
menuRef.current?.show();
}}
name="dots-vertical"
@@ -116,9 +117,10 @@ export const RightMenus = ({
{headerRightButtons.map((item) => (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0
borderRadius: 0,
alignSelf: "flex-start",
width: "100%"
}}
type="plain"
buttonType={{
@@ -127,6 +129,7 @@ export const RightMenus = ({
key={item.title}
title={item.title}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();

View File

@@ -57,10 +57,7 @@ export const SectionHeader = React.memo<
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
const groupBy =
strings.groupByStrings[
groupOptions.groupBy as keyof typeof strings.groupByStrings
]?.();
const groupBy = strings.groupByStrings[groupOptions.groupBy]();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
);

View File

@@ -81,7 +81,8 @@ const ReminderItem = React.memo(
<View
style={{
flexDirection: "row",
flexWrap: "wrap"
flexWrap: "wrap",
marginTop: 5
}}
>
{item.disabled ? (
@@ -149,10 +150,8 @@ const ReminderItem = React.memo(
fontSize={SIZE.xs}
style={{
justifyContent: "flex-start",
borderWidth: 0,
height: 30,
alignSelf: "flex-start",
marginTop: 5
height: 25,
alignSelf: "flex-start"
}}
/>
</View>

View File

@@ -70,6 +70,7 @@ const SelectionWrapper = ({
}
const onLongPress = () => {
if (isSheet) return;
if (useSelectionStore.getState().selectionMode !== item.type) {
useSelectionStore.getState().setSelectionMode(item.type);
}
@@ -78,7 +79,7 @@ const SelectionWrapper = ({
return (
<Pressable
customColor={isSheet ? colors.primary.hover : "transparent"}
customColor={isSheet ? colors.secondary.background : "transparent"}
testID={testID}
onLongPress={onLongPress}
onPress={onPress}

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef } from "react";
import {
@@ -26,7 +27,7 @@ import {
Platform,
View
} from "react-native";
import Menu from "react-native-reanimated-material-menu/src/Menu";
import { Menu } from "react-native-material-menu";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
@@ -46,7 +47,6 @@ import { MoveNotebookSheet } from "../sheets/move-notebook";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import { strings } from "@notesnook/intl";
export const SelectionHeader = React.memo(
({
@@ -101,9 +101,11 @@ export const SelectionHeader = React.memo(
};
const deleteItem = async () => {
if (!type) return;
presentDialog({
title: strings.doActions.delete[type](selectedItemsList.length),
paragraph: strings.actionConfirmations.delete[type](
title: strings.doActions.delete.unknown(type, selectedItemsList.length),
paragraph: strings.actionConfirmations.delete.unknown(
type,
selectedItemsList.length
),
positiveText: strings.delete(),
@@ -218,14 +220,16 @@ export const SelectionHeader = React.memo(
style={{
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: -20
marginTop: 35
}}
onRequestClose={() => {
//@ts-ignore
menuRef.current?.hide();
}}
anchor={
<IconButton
onPress={() => {
//@ts-ignore
menuRef.current?.show();
}}
name="dots-vertical"
@@ -292,15 +296,35 @@ export const SelectionHeader = React.memo(
{
title: strings.moveToTrash(),
onPress: async () => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible: type !== "trash",
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" && type !== "note" && type !== "notebook",
icon: "delete"
},
{
@@ -319,9 +343,10 @@ export const SelectionHeader = React.memo(
!item.visible ? null : (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0
borderRadius: 0,
alignSelf: "flex-start",
width: "100%"
}}
type="plain"
buttonType={{
@@ -331,6 +356,7 @@ export const SelectionHeader = React.memo(
key={item.title}
title={item.title}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();

View File

@@ -301,8 +301,8 @@ export const NotebookSheet = () => {
}}
onPress={async () => {
await deleteItems(
useItemSelectionStore.getState().getSelectedItemIds(),
"notebook"
"notebook",
useItemSelectionStore.getState().getSelectedItemIds()
);
useSelectionStore.getState().clearSelection();
useItemSelectionStore.setState({

View File

@@ -17,12 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { createRef } from "react";
import { Platform, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import {
eSubscribeEvent,
eUnSubscribeEvent,
@@ -30,8 +35,6 @@ import {
} from "../../../services/event-manager";
import { clearMessage } from "../../../services/message";
import SettingsService from "../../../services/settings";
import { db } from "../../../common/database";
import Storage from "../../../common/database/storage";
import { eOpenRecoveryKeyDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
@@ -41,9 +44,6 @@ import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import RNFetchBlob from "react-native-blob-util";
import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
class RecoveryKeySheet extends React.Component {
constructor(props) {
@@ -126,7 +126,7 @@ class RecoveryKeySheet extends React.Component {
"base64"
);
} else {
path = await Storage.checkAndCreateDir("/");
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, data, "base64");
}
ToastManager.show({
@@ -157,7 +157,7 @@ class RecoveryKeySheet extends React.Component {
if (!file) return;
path = file.uri;
} else {
path = await Storage.checkAndCreateDir("/");
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, this.state.key, "utf8");
path = path + fileName;
}

View File

@@ -88,14 +88,14 @@ export const RelationsList = ({
sortDirection: "desc"
})
.then((grouped) => {
setItems(grouped);
setTimeout(() => {
setItems(grouped);
}, 300);
});
}, [relationType, referenceType, item?.id, item?.type]);
}, [relationType, referenceType, item?.id, item?.type, updater]);
return (
<View
style={{ paddingHorizontal: 12, height: hasNoRelations ? 300 : "100%" }}
>
<View style={{ paddingHorizontal: 12, height: "100%" }}>
<SheetProvider context="local" />
<DialogHeader
title={title}

View File

@@ -68,19 +68,19 @@ export default function ReminderNotify({
const QuickActions = [
{
title: `5 ${strings.timeShort.minute}`,
title: `5 ${strings.timeShort.minute()}`,
time: 5
},
{
title: `15 ${strings.timeShort.minute}`,
title: `15 ${strings.timeShort.minute()}`,
time: 15
},
{
title: `30 ${strings.timeShort.minute}`,
title: `30 ${strings.timeShort.minute()}`,
time: 30
},
{
title: `1 ${strings.timeShort.hour}`,
title: `1 ${strings.timeShort.hour()}`,
time: 60
}
];

View File

@@ -124,9 +124,11 @@ export default function ReminderSheet({
const referencedItem = reference ? (reference as Note) : null;
const title = useRef<string | undefined>(
reminder?.title || referencedItem?.title
!reminder ? referencedItem?.title : reminder?.title
);
const details = useRef<string | undefined>(
!reminder ? referencedItem?.headline : reminder?.description
);
const details = useRef<string | undefined>(reminder?.description);
const titleRef = useRef<TextInput>(null);
const timer = useRef<NodeJS.Timeout>();
@@ -543,7 +545,7 @@ export default function ReminderSheet({
/>
{reminderMode === ReminderModes.Permanent ? null : (
<ScrollView
<RNScrollView
style={{
flexDirection: "row",
marginTop: 12,
@@ -555,7 +557,7 @@ export default function ReminderSheet({
<Button
key={mode}
title={strings.reminderNotificationModes[
mode as keyof typeof strings.reminderNotificationModes
mode as keyof typeof ReminderNotificationModes
]()}
style={{
marginRight: 12,
@@ -588,7 +590,7 @@ export default function ReminderSheet({
}}
/>
))}
</ScrollView>
</RNScrollView>
)}
</ScrollView>

View File

@@ -51,7 +51,6 @@ export const ReminderTime = ({
<Button
title={time}
key={reminder.id}
height={20}
icon="bell"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
@@ -67,12 +66,10 @@ export const ReminderTime = ({
marginRight: 0
}}
style={{
height: "auto",
borderRadius: 5,
marginRight: 5,
borderWidth: 0.5,
borderColor: colors.primary.border,
paddingHorizontal: 6,
marginBottom: 5,
...(style as ViewStyle)
}}
{...props}

View File

@@ -254,8 +254,8 @@ export const useActions = ({
item.type === "color"
) {
presentDialog({
title: strings.doActions.delete[item.type](1),
paragraph: strings.actionConfirmations.delete[item.type](1),
title: strings.doActions.delete.unknown(item.type, 1),
paragraph: strings.actionConfirmations.delete.unknown(item.type, 1),
positivePress: async () => {
if (item.type === "reminder") {
await db.reminders.remove(item.id);
@@ -290,7 +290,7 @@ export const useActions = ({
});
} else {
try {
await deleteItems([item.id], item.type);
await deleteItems(item.type, [item.id]);
} catch (e) {
console.error(e);
}
@@ -303,8 +303,8 @@ export const useActions = ({
close();
await sleep(300);
presentDialog({
title: strings.doActions.delete[item.itemType](1),
paragraph: strings.actionConfirmations.delete[item.itemType](1),
title: strings.doActions.delete.unknown(item.itemType, 1),
paragraph: strings.actionConfirmations.delete.unknown(item.itemType, 1),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: async () => {
@@ -313,10 +313,7 @@ export const useActions = ({
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading:
strings.actions.deleted[
item.itemType as keyof typeof strings.actions.deleted
](1),
heading: strings.actions.deleted.unknown(item.itemType, 1),
type: "success",
context: "local"
});
@@ -957,11 +954,10 @@ export const useActions = ({
id: "trash",
title:
item.type !== "notebook" && item.type !== "note"
? strings.doActions.delete[
item.type === "trash"
? item.itemType
: (item.type as keyof typeof strings.doActions.delete)
](1)
? strings.doActions.delete.unknown(
item.type === "trash" ? item.itemType : item.type,
1
)
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",

View File

@@ -17,13 +17,11 @@
"phone": "^3.1.14",
"qclone": "^1.2.0",
"react-native-actions-sheet": "0.9.7",
"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",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-progress": "^5.0.0",
"react-native-qrcode-svg": "^6.0.6",
"react-native-reanimated-material-menu": "github:ammarahm-ed/react-native-reanimated-material-menu",
"react-native-reanimated-progress-bar": "1.0.1",
"react-native-swiper-flatlist": "3.2.2",
"timeago.js": "4.0.2",
@@ -35,15 +33,13 @@
"@readme/data-urls": "3.0.0",
"react-native-wheel-color-picker": "^1.3.1",
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "10.45.2",
"@trpc/react-query": "10.45.2",
"@trpc/server": "10.45.2",
"@streetwriters/kysely": "^0.27.4",
"pathe": "1.1.2",
"react-native-format-currency": "0.0.5",
"@lingui/react": "4.11.2",
"@lingui/core": "4.11.2"
"@lingui/core": "4.11.2",
"react-native-check-version": "^1.3.0",
"react-native-material-menu": "^2.0.0"
},
"sideEffects": false
}

View File

@@ -34,6 +34,7 @@ import { EditorEvents } from "./tiptap/utils";
import { useThemeColors } from "@notesnook/theme";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { db } from "../../common/database";
import { i18n } from "@lingui/core";
const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => {
if (request.url.includes("https")) {
@@ -143,10 +144,17 @@ export function ReadonlyEditor(props: {
ref={editorRef}
key={"readonly-editor:" + props.editorId}
nestedScrollEnabled
injectedJavaScriptBeforeContentLoaded={`globalThis.readonlyEditor=true;`}
injectedJavaScript="globalThis.readonlyEditor=true;"
injectedJavaScript={`
globalThis.__DEV__ = ${__DEV__}
globalThis.readonlyEditor=true;
globalThis.LINGUI_LOCALE = "${i18n.locale}";
globalThis.LINGUI_LOCALE_DATA = ${JSON.stringify({
[i18n.locale]: i18n.messages
})};
globalThis.loadApp();`}
useSharedProcessPool={false}
javaScriptEnabled={true}
webviewDebuggingEnabled={__DEV__}
focusable={true}
setSupportMultipleWindows={false}
overScrollMode="never"

View File

@@ -37,6 +37,7 @@ export type EditorState = {
scrollPosition: number;
overlay?: boolean;
initialLoadCalled?: boolean;
editorStateRestored?: boolean;
};
export type Settings = {

View File

@@ -861,9 +861,13 @@ export const useEditor = (
state.current.currentlyEditing = true;
state.current.movedAway = false;
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1, false);
if (!state.current.editorStateRestored) {
state.current.isRestoringState = true;
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1, false);
}
}
clearAppState();
state.current.isRestoringState = false;
}, []);

View File

@@ -52,7 +52,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
<SelectionHeader id={route.name} items={notes} type="note" />
<Header
renderedInRoute={route.name}
title={strings.routes[route.name as keyof typeof strings.routes]()}
title={strings.routes[route.name]()}
canGoBack={false}
hasSearch={true}
onSearch={() => {
@@ -72,9 +72,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
dataType="note"
renderedInRoute={route.name}
loading={loading || !isFocused}
headerTitle={strings.routes[
route.name as keyof typeof strings.routes
]?.()}
headerTitle={strings.routes[route.name]()}
placeholder={{
title: route.name?.toLowerCase(),
paragraph: strings.notesEmpty(),

View File

@@ -196,8 +196,7 @@ const NotesPage = ({
<Header
renderedInRoute={route.name}
title={
title ||
strings.routes[route.name as unknown as keyof typeof strings.routes]()
route.name === "Monographs" ? strings.routes[route.name]() : title
}
canGoBack={params?.current?.canGoBack}
hasSearch={true}

View File

@@ -29,6 +29,7 @@ import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { SIZE } from "../../utils/size";
import { strings } from "@notesnook/intl";
export const AttachmentGroupProgress = (props: { groupId?: string }) => {
const { colors } = useThemeColors();
@@ -90,11 +91,12 @@ export const AttachmentGroupProgress = (props: { groupId?: string }) => {
}}
numberOfLines={1}
>
Downloading {file?.filename} {formatBytes(file?.size || 0)}{" "}
{strings.downloading()} {file?.filename}{" "}
{formatBytes(file?.size || 0)}{" "}
{fileProgress?.percent ? `(${fileProgress.percent})` : ""}
</Paragraph>
<Paragraph size={10} color={colors.secondary.paragraph}>
Group: {props.groupId}
{strings.group()}: {props.groupId}
</Paragraph>
</View>
{props.groupId === "offline-mode" ? null : (

View File

@@ -17,24 +17,24 @@ 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 Clipboard from "@react-native-clipboard/clipboard";
import { LogMessage } from "@notesnook/logger";
import { sanitizeFilename } from "@notesnook/common";
import { format, LogLevel, logManager } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { LogMessage } from "@notesnook/logger";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "react-native-blob-util";
import Storage from "../../common/database/storage";
import * as ScopedStorage from "react-native-scoped-storage";
import filesystem from "../../common/filesystem";
import { presentDialog } from "../../components/dialog/functions";
import { IconButton } from "../../components/ui/icon-button";
import { Notice } from "../../components/ui/notice";
import Paragraph from "../../components/ui/typography/paragraph";
import useTimer from "../../hooks/use-timer";
import { ToastManager } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { hexToRGBA } from "../../utils/colors";
import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
export default function DebugLogs() {
const { colors } = useThemeColors();
@@ -148,7 +148,7 @@ export default function DebugLogs() {
if (!file) return;
path = file.uri;
} else {
path = await Storage.checkAndCreateDir("/");
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName + ".txt", data, "utf8");
path = path + fileName;
}

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import Menu, { MenuItem } from "react-native-reanimated-material-menu";
import { Menu, MenuItem } from "react-native-material-menu";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { Dialog } from "../../../components/dialog";
import { Pressable } from "../../../components/ui/pressable";
@@ -146,7 +146,7 @@ export function SettingsPicker<T>({
onChange(item);
}
}}
underlayColor={colors.primary.hover}
pressColor={colors.primary.hover}
style={{
backgroundColor: compareValue(currentValue, item)
? colors.selected.background

View File

@@ -57,7 +57,7 @@ export const HomePicker = createSettingsPicker({
});
},
formatValue: (item) => {
return strings.routes[typeof item === "object" ? item.name : item]();
return strings.routes[typeof item === "object" ? item.name : item]?.();
},
getItemKey: (item) => item.name,
options: MenuItemsList.slice(0, MenuItemsList.length - 1),

View File

@@ -28,7 +28,7 @@ import DocumentPicker from "react-native-document-picker";
import * as ScopedStorage from "react-native-scoped-storage";
import { unzip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../../../common/database";
import storage from "../../../common/database/storage";
import filesystem from "../../../common/filesystem";
import { deleteCacheFileByName } from "../../../common/filesystem/io";
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
import { presentDialog } from "../../../components/dialog/functions";
@@ -300,7 +300,7 @@ export const RestoreBackup = () => {
return;
}
} else {
const path = await storage.checkAndCreateDir("/backups/");
const path = await filesystem.checkAndCreateDir("/backups/");
files = await RNFetchBlob.fs.lstat(path);
}
files = files

View File

@@ -203,19 +203,15 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
<Input
{...item.inputProperties}
onSubmit={(e) => {
if (e.nativeEvent.text) {
SettingsService.set({
[item.property as string]: e.nativeEvent.text
});
}
SettingsService.set({
[item.property as string]: e.nativeEvent.text
});
item.inputProperties?.onSubmitEditing?.(e);
}}
onChangeText={(text) => {
if (text) {
SettingsService.set({
[item.property as string]: text
});
}
SettingsService.set({
[item.property as string]: text
});
item.inputProperties?.onSubmitEditing?.(text as any);
}}
containerStyle={{ marginTop: 12 }}

View File

@@ -1166,7 +1166,9 @@ export const settingsGroups: SettingSection[] = [
type: "component",
hidden: () => !useUserStore.getState().user,
name: strings.automaticBackupsWithAttachments(),
description: strings.automaticBackupsWithAttachmentsDesc(),
description: [
...strings.automaticBackupsWithAttachmentsDesc()
].join("\n"),
component: "autobackupsattachments"
},
{

View File

@@ -30,24 +30,12 @@ import SettingsService from "./settings";
async function doInBackground(callback: () => Promise<void>) {
if (Platform.OS === "ios") {
try {
const bgTaskId = await beginBackgroundTask();
const result = await callback();
await endBackgroundTask(bgTaskId);
return result;
} catch (e) {
return (e as Error).message;
}
const bgTaskId = await beginBackgroundTask();
const result = await callback();
await endBackgroundTask(bgTaskId);
return result;
} else {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
try {
const result = await callback();
resolve(result);
} catch (e) {
resolve((e as Error).message);
}
});
return await callback();
}
}

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { sanitizeFilename } from "@notesnook/common";
import { formatDate } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
@@ -26,20 +27,14 @@ import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { zip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../common/database";
import storage from "../common/database/storage";
import filesystem from "../common/filesystem";
import filesystem, { FileStorage } from "../common/filesystem";
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
import { presentDialog } from "../components/dialog/functions";
import {
endProgress,
startProgress,
updateProgress
} from "../components/dialogs/progress";
import { endProgress, updateProgress } from "../components/dialogs/progress";
import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
import { strings } from "@notesnook/intl";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -178,7 +173,7 @@ async function run(
let path;
if (Platform.OS === "ios") {
path = await storage.checkAndCreateDir("/backups");
path = await filesystem.checkAndCreateDir("/backups");
}
const backupFileName = sanitizeFilename(
@@ -232,7 +227,7 @@ async function run(
updateProgress({
progress: `Saving attachments in backup... ${file.hash}`
});
if (await filesystem.exists(file.hash)) {
if (await FileStorage.exists(file.hash)) {
await RNFetchBlob.fs.cp(
`${cacheDir}/${file.hash}`,
`${attachmentsDir}/${file.hash}`
@@ -299,7 +294,7 @@ async function run(
path: path
};
} catch (e) {
ToastManager.error(e, strings.backupFailed(), context || "global");
ToastManager.error(e as Error, strings.backupFailed(), context || "global");
if (
(e as Error)?.message?.includes("android.net.Uri") &&

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Platform } from "react-native";
import FingerprintScanner, {
AuthenticateIOS
} from "react-native-fingerprint-scanner";
} from "@ammarahmed/react-native-fingerprint-scanner";
import * as Keychain from "react-native-keychain";
import { MMKV } from "../common/database/mmkv";
import Storage from "../common/database/storage";

View File

@@ -123,6 +123,7 @@ export type PresentSheetOptions = {
};
export function presentSheet(data: Partial<PresentSheetOptions>) {
console.log("PRESENTING...");
eSendEvent(eOpenSheet, data);
}

View File

@@ -23,7 +23,6 @@ import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
import * as ScopedStorage from "react-native-scoped-storage";
import { zip } from "react-native-zip-archive";
import { DatabaseLogger } from "../common/database/index";
import Storage from "../common/database/storage";
import {
exportNote as _exportNote,
@@ -31,13 +30,13 @@ import {
ExportableNote,
exportNotes
} from "@notesnook/common";
import { Note } from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core";
import { basename, dirname, join, extname } from "pathe";
import { FilteredSelector, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { basename, dirname, extname, join } from "pathe";
import filesystem from "../common/filesystem";
import downloadAttachment from "../common/filesystem/download-attachment";
import { cacheDir } from "../common/filesystem/utils";
import { unlockVault } from "../utils/unlock-vault";
import { strings } from "@notesnook/intl";
const FolderNames: { [name: string]: string } = {
txt: "Text",
@@ -49,7 +48,7 @@ const FolderNames: { [name: string]: string } = {
async function getPath(type: string) {
let path =
Platform.OS === "ios" &&
(await Storage.checkAndCreateDir(`/exported/${type}/`));
(await filesystem.checkAndCreateDir(`/exported/${type}/`));
if (Platform.OS === "android") {
const file = await ScopedStorage.openDocumentTree(true);

View File

@@ -32,7 +32,7 @@ export const STORE_LINK =
export const GROUP = {
default: "default",
None: "none",
none: "none",
abc: "abc",
year: "year",
week: "week",

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 { ItemType } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { Linking } from "react-native";
import { db } from "../common/database";
import { presentDialog } from "../components/dialog/functions";
@@ -24,16 +26,18 @@ import { eSendEvent, ToastManager } from "../services/event-manager";
import Navigation from "../services/navigation";
import { useMenuStore } from "../stores/use-menu-store";
import { useRelationStore } from "../stores/use-relation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useTagStore } from "../stores/use-tag-store";
import { eOnNotebookUpdated, eUpdateNoteInEditor } from "./events";
import { getParentNotebookId } from "./notebooks";
import { useTagStore } from "../stores/use-tag-store";
import { strings } from "@notesnook/intl";
function confirmDeleteAllNotes(items, type, context) {
return new Promise((resolve) => {
function confirmDeleteAllNotes(
items: string[],
type: "notebook",
context?: string
) {
return new Promise<{ delete: boolean; deleteNotes: boolean }>((resolve) => {
presentDialog({
title: strings.doActions.delete[type](items.length),
title: strings.doActions.delete.notebook(items.length),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: (_inputValue, value) => {
@@ -43,22 +47,21 @@ function confirmDeleteAllNotes(items, type, context) {
},
onClose: () => {
setTimeout(() => {
resolve({ delete: false });
resolve({ delete: false, deleteNotes: false });
});
},
context: context,
check: {
info: `Move all notes in ${
items.length > 1 ? `these ${type}s` : `this ${type}`
} to trash`,
info: strings.deleteContainingNotes(items.length),
type: "transparent"
}
});
});
}
async function deleteNotebook(id, deleteNotes) {
async function deleteNotebook(id: string, deleteNotes: boolean) {
const notebook = await db.notebooks.notebook(id);
if (!notebook) return;
const parentId = getParentNotebookId(id);
if (deleteNotes) {
const noteRelations = await db.relations.from(notebook, "note").get();
@@ -74,14 +77,16 @@ async function deleteNotebook(id, deleteNotes) {
}
}
export const deleteItems = async (items, type, context) => {
const ids = items ? items : useSelectionStore.getState().selectedItemsList;
export const deleteItems = async (
type: ItemType,
itemIds: string[],
context?: string
) => {
if (type === "reminder") {
await db.reminders.remove(...ids);
await db.reminders.remove(...itemIds);
useRelationStore.getState().update();
} else if (type === "note") {
for (const id of ids) {
for (const id of itemIds) {
if (db.monographs.isPublished(id)) {
ToastManager.show({
heading: strings.someNotesPublished(),
@@ -104,20 +109,20 @@ export const deleteItems = async (items, type, context) => {
);
}
} else if (type === "notebook") {
const result = await confirmDeleteAllNotes(ids, "notebook", context);
const result = await confirmDeleteAllNotes(itemIds, "notebook", context);
if (!result.delete) return;
for (const id of ids) {
for (const id of itemIds) {
await deleteNotebook(id, result.deleteNotes);
eSendEvent(eOnNotebookUpdated, await getParentNotebookId(id));
}
} else if (type === "tag") {
presentDialog({
title: strings.doActions.delete.tag(ids.length),
title: strings.doActions.delete.tag(itemIds.length),
positiveText: strings.delete(),
negativeText: strings.cancel(),
paragraph: strings.actionConfirmations.delete.tag(2),
positivePress: async () => {
await db.tags.remove(...ids);
await db.tags.remove(...itemIds);
useTagStore.getState().refresh();
useRelationStore.getState().update();
},
@@ -126,9 +131,9 @@ export const deleteItems = async (items, type, context) => {
return;
}
let deletedIds = [...ids];
const deletedIds = [...itemIds];
if (type === "notebook" || type === "note") {
let message = strings.actions.movedToTrash[type](ids.length);
const message = strings.actions.movedToTrash[type](itemIds.length);
ToastManager.show({
heading: message,
type: "success",
@@ -148,28 +153,21 @@ export const deleteItems = async (items, type, context) => {
});
} else {
ToastManager.show({
heading: strings.deleted(type, ids.length),
heading: strings.actions.deleted.unknown(type, itemIds.length),
type: "success"
});
}
Navigation.queueRoutesForUpdate();
if (!items) {
useSelectionStore.getState().clearSelection();
}
useMenuStore.getState().setColorNotes();
if (type === "notebook") {
ids.forEach(async (id) => {
itemIds.forEach(async (id) => {
eSendEvent(eOnNotebookUpdated, await getParentNotebookId(id));
});
useMenuStore.getState().setMenuPins();
}
};
export const openLinkInBrowser = async (link) => {
try {
Linking.openURL(link);
} catch (error) {
console.log(error.message);
}
export const openLinkInBrowser = async (link: string) => {
Linking.openURL(link);
};

View File

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

View File

@@ -1063,7 +1063,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2118;
CURRENT_PROJECT_VERSION = 2120;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1137,7 +1137,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.21;
MARKETING_VERSION = 3.0.23;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1168,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2118;
CURRENT_PROJECT_VERSION = 2120;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1242,7 +1242,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.21;
MARKETING_VERSION = 3.0.23;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1401,7 +1401,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2118;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1413,7 +1413,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.21;
MARKETING_VERSION = 3.0.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1444,7 +1444,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2118;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1457,7 +1457,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.21;
MARKETING_VERSION = 3.0.23;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1487,7 +1487,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2118;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1561,7 +1561,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.21;
MARKETING_VERSION = 3.0.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1592,7 +1592,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2118;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1667,7 +1667,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.21;
MARKETING_VERSION = 3.0.23;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1744,7 +1744,10 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
@@ -1808,7 +1811,10 @@
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;

View File

@@ -970,6 +970,8 @@ PODS:
- React
- react-native-background-actions (2.6.7):
- React-Core
- react-native-begin-background-task (0.1.0):
- React
- react-native-blob-util (0.18.3):
- React-Core
- react-native-config (1.5.1):
@@ -981,7 +983,7 @@ PODS:
- react-native-document-picker (7.1.3):
- React-Core
- react-native-fingerprint-scanner (5.0.0):
- React
- React-Core
- react-native-get-random-values (1.9.0):
- React-Core
- react-native-gzip (1.1.0):
@@ -1019,7 +1021,7 @@ PODS:
- SDWebImage (~> 5.11.1)
- react-native-share-extension (2.6.0):
- React
- react-native-sodium (1.5.6):
- react-native-sodium (1.6.1):
- React
- react-native-theme-switch-animation (0.6.0):
- DoubleConversion
@@ -1486,11 +1488,12 @@ DEPENDENCIES:
- React-Mapbuffer (from `../../node_modules/react-native/ReactCommon`)
- react-native-actions-shortcuts (from `../../node_modules/react-native-actions-shortcuts`)
- react-native-background-actions (from `../../node_modules/react-native-background-actions`)
- react-native-begin-background-task (from `../../node_modules/react-native-begin-background-task`)
- react-native-blob-util (from `../../node_modules/react-native-blob-util`)
- react-native-config (from `../../node_modules/react-native-config`)
- react-native-date-picker (from `../../node_modules/react-native-date-picker`)
- react-native-document-picker (from `../../node_modules/react-native-document-picker`)
- react-native-fingerprint-scanner (from `../../node_modules/react-native-fingerprint-scanner`)
- "react-native-fingerprint-scanner (from `../../node_modules/@ammarahmed/react-native-fingerprint-scanner`)"
- react-native-get-random-values (from `../../node_modules/react-native-get-random-values`)
- react-native-gzip (from `../../node_modules/react-native-gzip`)
- react-native-html-to-pdf-lite (from `../../node_modules/react-native-html-to-pdf-lite`)
@@ -1642,6 +1645,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native-actions-shortcuts"
react-native-background-actions:
:path: "../../node_modules/react-native-background-actions"
react-native-begin-background-task:
:path: "../../node_modules/react-native-begin-background-task"
react-native-blob-util:
:path: "../../node_modules/react-native-blob-util"
react-native-config:
@@ -1651,7 +1656,7 @@ EXTERNAL SOURCES:
react-native-document-picker:
:path: "../../node_modules/react-native-document-picker"
react-native-fingerprint-scanner:
:path: "../../node_modules/react-native-fingerprint-scanner"
:path: "../../node_modules/@ammarahmed/react-native-fingerprint-scanner"
react-native-get-random-values:
:path: "../../node_modules/react-native-get-random-values"
react-native-gzip:
@@ -1833,11 +1838,12 @@ SPEC CHECKSUMS:
React-Mapbuffer: 6c1cacdbf40b531f549eba249e531a7d0bfd8e7f
react-native-actions-shortcuts: 5d9cf0c9c308333dfcc1e05c3f9afa8c428e2533
react-native-background-actions: 2c251c986f23347f9c1722f05fd296938f60edb1
react-native-begin-background-task: 3b889e07458afc5822a7277cf9cbc7cd077e39ee
react-native-blob-util: 2d36383bb52c15c5451be81cb7ddf22bc34a12a6
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
react-native-date-picker: 93e43b3084cea595b4d68b1405d6d99849663bd6
react-native-document-picker: ec07866a30707f23660c0f3ae591d669d3e89096
react-native-fingerprint-scanner: 3e8c46641b5751a31f45e66789d4b6f342be5e6d
react-native-fingerprint-scanner: be63e626b31fb951780a5fac5328b065a61a3d6e
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-gzip: c5e87ee9e359f02350e3a2ee52eb35eddc398868
react-native-html-to-pdf-lite: 21bfb169bf4cbcd7bec9f736975ee1b3f5292d4a
@@ -1853,7 +1859,7 @@ SPEC CHECKSUMS:
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
react-native-screenguard: 8b36a3df84c76cd2b82c477f71c26fa1c8cc14a0
react-native-share-extension: 25437eb1039f7409be6e80a7edf8d02b42e1dc99
react-native-sodium: 605c1523ec8ff5fbff5e9e7769bbacceb571a3c6
react-native-sodium: 4cb76086943a7f60c42b40ebca866695b360a196
react-native-theme-switch-animation: d3eb50365a3829ce5572628888fa514752703f61
react-native-webview: 553abd09f58e340fdc7746c9e2ae096839e99911
React-nativeconfig: ba9a2e54e2f0882cf7882698825052793ed4c851

View File

@@ -25,7 +25,6 @@
"react-native": "0.74.5",
"react-native-actions-shortcuts": "^1.0.1",
"react-native-background-actions": "^2.6.6",
"react-native-begin-background-task": "https://github.com/blockfirm/react-native-begin-background-task.git",
"react-native-blob-util": "^0.18.3",
"react-native-bootsplash": "4.1.4",
"react-native-config": "^1.4.6",
@@ -33,9 +32,7 @@
"react-native-device-info": "^8.4.1",
"react-native-document-picker": "^7.1.1",
"@ammarahmed/react-native-eventsource": "1.1.0",
"react-native-exit-app": "https://github.com/ammarahm-ed/react-native-exit-app.git",
"react-native-file-viewer": "^2.1.1",
"react-native-fingerprint-scanner": "https://github.com/ammarahm-ed/react-native-fingerprint-scanner.git",
"react-native-gesture-handler": "^2.18.0",
"react-native-reanimated": "3.14.0",
"react-native-safe-area-context": "^4.10.8",
@@ -52,9 +49,7 @@
"react-native-modal-datetime-picker": "14.0.0",
"react-native-navigation-bar-color": "2.0.2",
"react-native-notification-sounds": "0.5.5",
"react-native-orientation": "https://github.com/yamill/react-native-orientation.git",
"react-native-pdf": "6.6.2",
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
"react-native-scoped-storage": "^1.9.5",
"react-native-securerandom": "^1.0.1",
"react-native-share": "^7.2.0",
@@ -70,13 +65,18 @@
"react-native-screenguard": "^1.0.0",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-pluralrules": "5.2.14",
"detox": "^20.27.6",
"@ammarahmed/react-native-share-extension": "^2.6.0",
"@ammarahmed/react-native-sodium": "1.5.6",
"@ammarahmed/react-native-sodium": "^1.6.1",
"react-native-mmkv-storage": "^0.10.2",
"@react-native-community/datetimepicker": "^8.2.0"
"@react-native-community/datetimepicker": "^8.2.0",
"react-native-exit-app": "github:ammarahm-ed/react-native-exit-app",
"react-native-orientation": "github:yamill/react-native-orientation",
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
"@ammarahmed/react-native-share-extension": "^2.7.0"
},
"devDependencies": {
"detox": "^20.27.6",
"@babel/core": "^7.20.0",
"@babel/eslint-parser": "^7.16.5",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.16.5",

View File

@@ -217,7 +217,7 @@ module.exports = (env) => {
/node_modules(.*[/\\])+@tanstack[/\\]react-query/,
/node_modules(.*[/\\])+@trpc[/\\]react-query/,
/node_modules(.*[/\\])+katex/,
/node_modules(.*[/\\])+mime/,
/node_modules(.*[/\\])+react-native-material-menu/,
/node_modules(.*[/\\])+@notesnook[/\\]core/,
/node_modules(.*[/\\])+whatwg-url-without-unicode/,
/node_modules(.*[/\\])+whatwg-url/,

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.21",
"version": "3.0.23",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -43,9 +43,13 @@
"@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes",
"@notesnook/crypto": "file:../../packages/crypto",
"diffblazer": "^1.0.1",
"react": "18.2.0",
"react-native": "0.74.5",
"react-native-actions-sheet": "^0.9.7"
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.36.1"
}
}

View File

@@ -1,11 +0,0 @@
diff --git a/node_modules/react-native-fingerprint-scanner/android/build.gradle b/node_modules/react-native-fingerprint-scanner/android/build.gradle
index eaf3829..6c392ba 100644
--- a/node_modules/react-native-fingerprint-scanner/android/build.gradle
+++ b/node_modules/react-native-fingerprint-scanner/android/build.gradle
@@ -49,5 +49,5 @@ dependencies {
// 1.2.3 is the minimum version compatible with androidx.
// See https://github.com/uccmawei/FingerprintIdentify/issues/74
// (translation https://translate.google.com/translate?sl=zh-CN&tl=en&u=https://github.com/uccmawei/FingerprintIdentify/issues/74)
- implementation "com.wei.android.lib:fingerprintidentify:${safeExtGet("fingerprintidentify", "1.2.6")}"
+ implementation "com.github.uccmawei:FingerprintIdentify:1.2.6"
}

View File

@@ -0,0 +1,19 @@
diff --git a/node_modules/react-native-material-menu/dist/Menu.js b/node_modules/react-native-material-menu/dist/Menu.js
index 64fbf13..c7fcee0 100644
--- a/node_modules/react-native-material-menu/dist/Menu.js
+++ b/node_modules/react-native-material-menu/dist/Menu.js
@@ -132,10 +132,13 @@ class Menu extends react_1.default.Component {
else if (left < SCREEN_INDENT) {
left = SCREEN_INDENT;
}
+ console.log(top, windowHeight - menuHeight - SCREEN_INDENT);
// Flip by Y axis if menu hits bottom screen border
if (top > windowHeight - menuHeight - SCREEN_INDENT) {
+ const diff = top - (windowHeight - menuHeight - SCREEN_INDENT);
+ const fraction = (diff / menuHeight);
transforms.push({
- translateY: react_native_1.Animated.multiply(menuSizeAnimation.y, -1),
+ translateY: react_native_1.Animated.multiply(menuSizeAnimation.y, -(fraction + 1)),
});
top = windowHeight - SCREEN_INDENT;
top = Math.min(windowHeight - SCREEN_INDENT, top + buttonHeight);

View File

@@ -1,600 +0,0 @@
diff --git a/node_modules/react-native-reanimated-material-menu/src/Menu.js b/node_modules/react-native-reanimated-material-menu/src/Menu.js
deleted file mode 100644
index 69065f0..0000000
--- a/node_modules/react-native-reanimated-material-menu/src/Menu.js
+++ /dev/null
@@ -1,268 +0,0 @@
-import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
-import {
- Dimensions, I18nManager, Modal,
- Platform,
- StatusBar,
- StyleSheet,
- TouchableWithoutFeedback,
- View
-} from 'react-native';
-import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
-
-const STATES = {
- HIDDEN: 'HIDDEN',
- ANIMATING: 'ANIMATING',
- SHOWN: 'SHOWN',
-};
-
-const EASING = Easing.bezier(0.4, 0, 0.2, 1);
-const SCREEN_INDENT = 8;
-
-const Menu = ({ animationDuration = 300, ...props }, ref) => {
- const container = useRef();
- const [state, setState] = useState({
- menuState: STATES.HIDDEN,
-
- top: 0,
- left: 0,
-
- menuWidth: 0,
- menuHeight: 0,
-
- buttonWidth: 0,
- buttonHeight: 0,
- });
- const menuSizeXAnimation = useSharedValue(0);
- const menuSizeYAnimation = useSharedValue(0);
- const opacityAnimation = useSharedValue(0);
-
- const _onMenuLayout = e => {
- if (state.menuState === STATES.ANIMATING) {
- return;
- }
- const { width, height } = e.nativeEvent.layout;
- setState(state => {
- return {
- ...state,
- menuState: STATES.ANIMATING,
- menuWidth: width,
- menuHeight: height,
- }
- }
-
- );
- };
-
- useEffect(() => {
- if (menuState === STATES.ANIMATING) {
-
- menuSizeXAnimation.value = withTiming(state.menuWidth, {
- duration: animationDuration * 2,
- easing: EASING
- })
- menuSizeYAnimation.value = withTiming(state.menuHeight, {
- duration: animationDuration,
- easing: EASING
- })
- opacityAnimation.value = withTiming(1, {
- duration: animationDuration + 25,
- easing: EASING
- })
- }
- }, [state])
-
- const _onDismiss = () => {
- if (props.onHidden) {
- props.onHidden();
- }
- };
- useImperativeHandle(ref, () => {
- return {
- show: () => {
- container.current.measureInWindow((left, top, buttonWidth, buttonHeight) => {
-
- setState(state => {
-
- return {
- ...state,
- buttonHeight,
- buttonWidth,
- left,
- menuState: STATES.SHOWN,
- top,
- }
- });
- });
- },
-
- hide: hide
- }
-
- }, [hide, container])
-
- const hide = onHidden => {
- opacityAnimation.value = withTiming(0, {
- duration: animationDuration,
- easing: EASING
- });
- setTimeout(() => {
- setState(
- state => {
- return {
- ...state,
- menuState: STATES.HIDDEN,
- }
- }
- );
- menuSizeXAnimation.value = 0;
- menuSizeYAnimation.value = 0;
- opacityAnimation.value = 0;
-
- onHidden && onHidden();
- if (Platform.OS !== 'ios' && props.onHidden) {
- props.onHidden();
- }
- }, props.animationDuration)
-
- };
-
- // @@ TODO: Rework this
- const _hide = () => {
- hide();
- };
-
-
- const menuSize = useAnimatedStyle(() => {
- return {
- width: menuSizeXAnimation.value,
- height: menuSizeYAnimation.value,
- }
- });
- const shadowMenuContainerStyle = useAnimatedStyle(() => {
- return {
- opacity: opacityAnimation.value,
- };
- }, [state])
-
- const { isRTL } = I18nManager;
- const dimensions = Dimensions.get('window');
- const { width: windowWidth } = dimensions;
- const windowHeight = dimensions.height - (StatusBar.currentHeight || 0);
- const {
- menuWidth,
- menuHeight,
- buttonWidth,
- buttonHeight,
- } = state;
-
- // Adjust position of menu
- let { left, top } = state;
- const transforms = [];
- if (
- (isRTL && left + buttonWidth - menuWidth > SCREEN_INDENT) ||
- (!isRTL && left + menuWidth > windowWidth - SCREEN_INDENT)
- ) {
- transforms.push({
- translateX: menuSizeXAnimation.value * -1,
- });
-
- left = Math.min(windowWidth - SCREEN_INDENT, left + buttonWidth);
- } else if (left < SCREEN_INDENT) {
- left = SCREEN_INDENT;
- }
-
-
- // Flip by Y axis if menu hits bottom screen border
- if (top > windowHeight - menuHeight - SCREEN_INDENT) {
- transforms.push({
- translateY: menuSizeYAnimation.value * -1,
- });
-
- top = windowHeight - SCREEN_INDENT;
- top = Math.min(windowHeight - SCREEN_INDENT, top + buttonHeight);
- } else if (top < SCREEN_INDENT) {
- top = SCREEN_INDENT;
- }
-
- const extraStyles = {
- transform: transforms,
- top,
-
- // Switch left to right for rtl devices
- ...(isRTL ? { right: left } : { left }),
- }
-
- const { menuState } = state;
- const animationStarted = menuState === STATES.ANIMATING;
- const modalVisible = menuState === STATES.SHOWN || animationStarted;
-
- const { testID, button, style, children } = props;
-
- return <View ref={container} collapsable={false} testID={testID}>
- <View>{button}</View>
-
- <Modal
- visible={modalVisible}
- onRequestClose={_hide}
- supportedOrientations={[
- 'portrait',
- 'portrait-upside-down',
- 'landscape',
- 'landscape-left',
- 'landscape-right',
- ]}
- transparent
- onDismiss={_onDismiss}
- >
- <TouchableWithoutFeedback onPress={_hide} accessible={false}>
- <View style={StyleSheet.absoluteFill}>
- <Animated.View
- onLayout={_onMenuLayout}
- style={[
- styles.shadowMenuContainer,
- shadowMenuContainerStyle,
- extraStyles,
- style,
- ]}
- >
- <Animated.View
- style={[styles.menuContainer, animationStarted && menuSize]}
- >
- {children}
- </Animated.View>
- </Animated.View>
- </View>
- </TouchableWithoutFeedback>
- </Modal>
- </View>
-
-}
-
-
-const styles = StyleSheet.create({
- shadowMenuContainer: {
- position: 'absolute',
- backgroundColor: 'white',
- borderRadius: 4,
- opacity: 0,
- overflow: "hidden",
-
- // Shadow
- ...Platform.select({
- ios: {
- shadowColor: 'black',
- shadowOffset: { width: 0.3 * 5, height: 0.5 * 5 },
- shadowOpacity: 0.2,
- shadowRadius: 0.7 * 5,
- },
- android: {
- elevation: 8,
- },
- }),
- },
- menuContainer: {
- overflow: 'hidden',
- },
-});
-
-export default forwardRef(Menu);
diff --git a/node_modules/react-native-reanimated-material-menu/src/Menu.tsx b/node_modules/react-native-reanimated-material-menu/src/Menu.tsx
new file mode 100644
index 0000000..c3bc1c9
--- /dev/null
+++ b/node_modules/react-native-reanimated-material-menu/src/Menu.tsx
@@ -0,0 +1,303 @@
+import React from 'react';
+
+import {
+ Animated,
+ Dimensions,
+ Easing,
+ I18nManager,
+ LayoutChangeEvent,
+ Modal,
+ Platform,
+ ScrollView,
+ StatusBar,
+ StyleSheet,
+ TouchableWithoutFeedback,
+ View,
+ ViewStyle,
+} from 'react-native';
+
+export interface MenuProps {
+ children?: React.ReactNode;
+ anchor?: React.ReactNode;
+ style?: ViewStyle;
+ onRequestClose?(): void;
+ animationDuration?: number;
+ testID?: string;
+ visible?: boolean;
+}
+
+enum States {
+ Hidden,
+ Animating,
+ Shown,
+}
+
+interface State {
+ buttonHeight: number;
+ buttonWidth: number;
+ left: number;
+ menuHeight: number;
+ menuSizeAnimation: Animated.ValueXY;
+ menuState: States;
+ menuWidth: number;
+ opacityAnimation: Animated.Value;
+ top: number;
+}
+
+const EASING = Easing.bezier(0.4, 0, 0.2, 1);
+const SCREEN_INDENT = 8;
+const SCREEN_INDENT_VERTICAL = 80;
+
+class Menu extends React.Component<MenuProps, State> {
+ _container: View | null = null;
+
+ static defaultProps = {
+ animationDuration: 300,
+ };
+
+ constructor(props: MenuProps) {
+ super(props);
+
+ this.state = {
+ menuState: States.Hidden,
+
+ top: 0,
+ left: 0,
+
+ menuWidth: 0,
+ menuHeight: 0,
+
+ buttonWidth: 0,
+ buttonHeight: 0,
+
+ menuSizeAnimation: new Animated.ValueXY({ x: 0, y: 0 }),
+ opacityAnimation: new Animated.Value(0),
+ };
+ }
+
+ componentDidMount() {
+ if (!this.props.visible) {
+ return;
+ }
+
+ this.show();
+ }
+
+ componentDidUpdate(prevProps: MenuProps) {
+ if (prevProps.visible === this.props.visible) {
+ return;
+ }
+
+ if (this.props.visible) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ }
+
+ private setContainerRef = (ref: View) => {
+ this._container = ref;
+ };
+
+ // Start menu animation
+ private onMenuLayout = (e: LayoutChangeEvent) => {
+ if (this.state.menuState === States.Animating) {
+ return;
+ }
+
+ const { width, height } = e.nativeEvent.layout;
+ let timeout:any = 0;
+ this.setState(
+ {
+ menuState: States.Animating,
+ menuWidth: width,
+ menuHeight: height,
+ },
+ () => {
+ Animated.parallel([
+ Animated.timing(this.state.menuSizeAnimation, {
+ toValue: { x: width, y: height },
+ duration: this.props.animationDuration,
+ easing: EASING,
+ useNativeDriver: false,
+ }),
+ Animated.timing(this.state.opacityAnimation, {
+ toValue: 1,
+ duration: this.props.animationDuration,
+ easing: EASING,
+ useNativeDriver: false,
+ }),
+ ]).start(({finished}) => {
+ if (finished) {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ this.setState({
+ menuState: States.Shown
+ })
+ },20)
+
+ }
+ });
+ },
+ );
+ };
+
+ show = () => {
+ this._container?.measureInWindow((left, top, buttonWidth, buttonHeight) => {
+ this.setState({
+ buttonHeight,
+ buttonWidth,
+ left,
+ menuState: States.Shown,
+ top,
+ });
+ });
+ };
+
+ hide = () => {
+ Animated.timing(this.state.opacityAnimation, {
+ toValue: 0,
+ duration: this.props.animationDuration,
+ easing: EASING,
+ useNativeDriver: false,
+ }).start(() => {
+ // Reset state
+ this.setState({
+ menuState: States.Hidden,
+ menuSizeAnimation: new Animated.ValueXY({ x: 0, y: 0 }),
+ opacityAnimation: new Animated.Value(0),
+ });
+ });
+ };
+
+ private onRequestClose = () => {
+ this.props.onRequestClose?.();
+ };
+
+ render() {
+ const { isRTL } = I18nManager;
+
+ const dimensions = Dimensions.get('window');
+ const { width: windowWidth } = dimensions;
+ const windowHeight = dimensions.height - (StatusBar.currentHeight || 0);
+
+ const {
+ menuSizeAnimation,
+ menuWidth,
+ menuHeight,
+ buttonWidth,
+ buttonHeight,
+ opacityAnimation,
+ } = this.state;
+ const menuSize = {
+ width: menuSizeAnimation.x,
+ height: menuSizeAnimation.y,
+ };
+
+ // Adjust position of menu
+ let { left, top } = this.state;
+ const transforms:any[] = [];
+
+ if (
+ (isRTL && left + buttonWidth - menuWidth > SCREEN_INDENT) ||
+ (!isRTL && left + menuWidth > windowWidth - SCREEN_INDENT)
+ ) {
+ transforms.push({
+ translateX: Animated.multiply(menuSizeAnimation.x, -1),
+ } as never);
+
+ left = Math.min(windowWidth - SCREEN_INDENT, left + buttonWidth);
+ } else if (left < SCREEN_INDENT) {
+ left = SCREEN_INDENT;
+ }
+
+ // Flip by Y axis if menu hits bottom screen border
+ if (top > windowHeight - menuHeight - SCREEN_INDENT_VERTICAL) {
+ transforms.push({
+ translateY: Animated.multiply(menuSizeAnimation.y, -1),
+ } as never);
+
+ top = windowHeight - SCREEN_INDENT_VERTICAL;
+ top = Math.min(windowHeight - SCREEN_INDENT_VERTICAL, top + buttonHeight);
+ } else if (top < SCREEN_INDENT_VERTICAL) {
+ top = SCREEN_INDENT_VERTICAL;
+ }
+
+ const shadowMenuContainerStyle = {
+ opacity: opacityAnimation,
+ transform: transforms,
+ maxHeight: 500,
+ top,
+
+ // Switch left to right for rtl devices
+ ...(isRTL ? { right: left } : { left }),
+ };
+ const { menuState } = this.state;
+ const animationStarted = menuState === States.Animating;
+ const modalVisible = menuState === States.Shown || animationStarted;
+
+ const { testID, anchor, style, children } = this.props;
+
+ return (
+ <View ref={this.setContainerRef} collapsable={false} testID={testID}>
+ {anchor}
+
+ <Modal
+ visible={modalVisible}
+ onRequestClose={this.onRequestClose}
+ supportedOrientations={[
+ 'portrait',
+ 'portrait-upside-down',
+ 'landscape',
+ 'landscape-left',
+ 'landscape-right',
+ ]}
+ transparent
+ >
+ <TouchableWithoutFeedback onPress={this.onRequestClose} accessible={false}>
+ <View style={StyleSheet.absoluteFill}>
+ <Animated.View
+ onLayout={this.onMenuLayout}
+ style={[styles.shadowMenuContainer, shadowMenuContainerStyle, style]}
+ >
+ <Animated.View style={[styles.menuContainer, animationStarted && menuSize]}>
+ <ScrollView showsVerticalScrollIndicator={menuState !== States.Animating} >
+ {children}
+ </ScrollView>
+ </Animated.View>
+ </Animated.View>
+ </View>
+ </TouchableWithoutFeedback>
+ </Modal>
+ </View>
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ shadowMenuContainer: {
+ position: 'absolute',
+ backgroundColor: 'white',
+ borderRadius: 4,
+ opacity: 0,
+
+ // Shadow
+ ...Platform.select({
+ ios: {
+ shadowColor: 'black',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.14,
+ shadowRadius: 2,
+ },
+ android: {
+ elevation: 8,
+ },
+ }),
+ },
+ menuContainer: {
+ overflow: 'hidden',
+ },
+});
+
+
+export default Menu
\ No newline at end of file
diff --git a/node_modules/react-native-reanimated-material-menu/src/MenuItem.js b/node_modules/react-native-reanimated-material-menu/src/MenuItem.js
index 120a870..d6459c0 100644
--- a/node_modules/react-native-reanimated-material-menu/src/MenuItem.js
+++ b/node_modules/react-native-reanimated-material-menu/src/MenuItem.js
@@ -14,6 +14,11 @@ const Touchable =
? TouchableNativeFeedback
: TouchableHighlight;
+/**
+ *
+ * @param {any} param0
+ * @returns
+ */
function MenuItem({
children,
disabled,

View File

@@ -6,3 +6,5 @@ node_modules
.dev.vars
.wrangler
/output
/fonts

View File

@@ -1,18 +1,14 @@
FROM --platform=$BUILDPLATFORM node:20-alpine
FROM --platform=$BUILDPLATFORM oven/bun:1.1.36-alpine
RUN mkdir -p /home/node/app && chown -R node:node /home/node/app
RUN mkdir -p /home/bun/app && chown -R bun:bun /home/bun/app
WORKDIR /home/node/app
WORKDIR /home/bun/app
USER node
USER bun
COPY --chown=node:node build ./build
COPY --chown=bun:bun output .
RUN mv build/package.json .
RUN bun install
RUN npm install
RUN ls
CMD [ "npm", "run", "start" ]
CMD [ "bun", "run", "start" ]

View File

@@ -21,9 +21,10 @@ import { mdiTimerOutline } from "@mdi/js";
import { Icon } from "@notesnook/ui";
import { Flex, Image, Link, Text } from "@theme-ui/components";
import { SxProp } from "@theme-ui/core";
import { PUBLIC_URL } from "../utils/env";
export function MonographChat({ sx }: SxProp) {
type Props = SxProp & { publicUrl: string };
export function MonographChat({ sx, publicUrl }: Props) {
return (
<Flex
sx={{
@@ -62,7 +63,7 @@ export function MonographChat({ sx }: SxProp) {
borderRadius: 10,
width: "100%"
}}
src={`${PUBLIC_URL}/api/og.png?title=Open+sourcing&description=VGhpcyBtb25vZ3JhcGggaXMgZW5jcnlwdGVkLiBFbnRlciBwYXNzd29yZCB0byB2aWV3IGNvbnRlbnRzLg%3D%3D&date=Saturday%2C+February+18%2C+2023`}
src={`${publicUrl}/api/og.jpg?title=Open+sourcing&description=VGhpcyBtb25vZ3JhcGggaXMgZW5jcnlwdGVkLiBFbnRlciBwYXNzd29yZCB0byB2aWV3IGNvbnRlbnRzLg%3D%3D&date=Saturday%2C+February+18%2C+2023`}
/>
<Text
mt="12px"
@@ -76,10 +77,10 @@ export function MonographChat({ sx }: SxProp) {
color: "accent",
overflowWrap: "anywhere"
}}
href={`${PUBLIC_URL}/62db75572020209c36f9f9fb`}
href={`${publicUrl}/62db75572020209c36f9f9fb`}
target="_blank"
>
{PUBLIC_URL}/62db75572020209c36f9f9fb
{publicUrl}/62db75572020209c36f9f9fb
</Link>
</Text>
<Timer time={"5m ago"} sx={{ mb: 1, mr: 1, alignSelf: "end" }} />

View File

@@ -73,6 +73,11 @@ function generateTableOfContents() {
let currentHeading = 0;
for (const heading of headings) {
const isCalloutHeading = heading.closest(".callout");
if (isCalloutHeading) {
continue;
}
const text = heading.textContent || "<empty>";
const nodeName = heading.nodeName;
const headingLevel = levelsMap[nodeName];

View File

@@ -60,7 +60,7 @@ export default function handleRequest(
const styles = constructStyleTagsFromChunks(extractCriticalToChunks(body));
const html = `<!DOCTYPE html><html><head><!--start head-->${head}${styles}<!--end head--></head><body><div id="root">${body}</div></body></html>`;
responseHeaders.set("Content-Type", "text/html");
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
return new Response(html, {
status: responseStatusCode,

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type { MetaFunction, LoaderFunctionArgs } from "@remix-run/node";
import { Cipher } from "@notesnook/crypto";
import { convert } from "html-to-text";
import { Flex, Text } from "@theme-ui/components";
import { useLoaderData } from "@remix-run/react";
import { MonographPage } from "../components/monographpost";
@@ -46,12 +45,13 @@ type Monograph = {
type MonographResponse = Omit<Monograph, "content"> & { content: string };
export const meta: MetaFunction<typeof loader> = ({ data }) => {
const imageUrl = `${PUBLIC_URL}/api/og.png?${new URLSearchParams({
if (!data || !data.metadata || !data.monograph) return [];
const imageUrl = `${PUBLIC_URL}/api/og.jpg?${new URLSearchParams({
title: data?.metadata?.title || "",
description: Buffer.from(
data?.metadata?.fullDescription || "",
"utf-8"
).toString("base64"),
description: data?.metadata?.fullDescription
? Buffer.from(data.metadata.fullDescription, "utf-8").toString("base64")
: "",
date: data?.metadata?.datePublished || ""
}).toString()}`;
@@ -88,7 +88,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
metadata
};
} catch (e) {
console.error(e);
// console.error(e);
return {
monograph: null,
metadata: {
@@ -134,15 +134,6 @@ export default function MonographPost() {
);
}
const extractParagraph = (html: string) => {
if (!html) return "";
return convert(html, {
wordwrap: false,
preserveNewlines: false,
decodeEntities: true
});
};
type Metadata = {
title: string;
fullDescription: string;
@@ -150,6 +141,21 @@ type Metadata = {
datePublished: string;
};
function extractFirstWords(html: string, numWords = 30): string {
// Strip HTML tags and normalize whitespace
const plainText = html
.replace(/<[^>]*>/g, " ")
.replace(/\s+/g, " ")
.trim();
// Split into words and take first N
const words = plainText.split(" ").slice(0, numWords);
// Add ellipsis if text was truncated
const excerpt = words.join(" ");
return words.length < plainText.split(" ").length ? excerpt + "..." : excerpt;
}
function trimDescription(
str: string,
length: number,
@@ -174,12 +180,12 @@ function addPeriod(str: string) {
return str + "...";
}
function getMonographMetadata(monograph?: Monograph): Metadata {
function getMonographMetadata(monograph: Monograph): Metadata {
const title = monograph?.title || "Not found";
const text = monograph?.encryptedContent
? "This monograph is encrypted. Enter password to view contents."
: monograph?.content
? extractParagraph(monograph?.content.data)
? extractFirstWords(monograph?.content.data, 100)
: "";
const shortDescription = trimDescription(text, 150, true);
const fullDescription = trimDescription(text, 300, true);

View File

@@ -38,6 +38,7 @@ import { Footer } from "../components/footer";
import { Header } from "../components/header";
import { ForwardRef } from "@theme-ui/components/dist/declarations/src/types";
import { PUBLIC_URL } from "../utils/env";
import { useLoaderData } from "@remix-run/react";
export const meta: MetaFunction = () => {
return generateMetaDescriptors({
@@ -51,6 +52,10 @@ export const meta: MetaFunction = () => {
});
};
export async function loader() {
return { publicUrl: PUBLIC_URL };
}
const ButtonLink = Button as ForwardRef<
HTMLButtonElement,
ButtonProps & LinkProps
@@ -74,6 +79,8 @@ const features = [
}
];
export default function Monograph() {
const { publicUrl } = useLoaderData<typeof loader>();
return (
<Flex
sx={{
@@ -146,6 +153,7 @@ export default function Monograph() {
border: "1px solid var(--border)",
boxShadow: "0px -5px 15px 0px rgba(0,0,0,0.05)"
}}
publicUrl={publicUrl}
/>
</Flex>
<Box

View File

@@ -21,6 +21,6 @@ import { LoaderFunctionArgs } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
url.pathname += ".png";
url.pathname += ".jpg";
return Response.redirect(url, 308);
}

View File

@@ -0,0 +1,52 @@
/*
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 { LoaderFunctionArgs } from "@remix-run/node";
import { makeImage } from "../utils/generate-og-image.server";
import { formatDate } from "@notesnook/core";
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const title = url.searchParams.get("title") || "Not found";
const description = url.searchParams.get("description") || "";
const date =
url.searchParams.get("date") ||
formatDate(new Date(), {
type: "date-time",
dateFormat: "YYYY-MM-DD",
timeFormat: "24-hour"
});
return new Response(
await makeImage(
{
date,
description,
title
},
url.search
),
{
status: 200,
headers: {
"Content-Type": "image/jpeg"
}
}
);
}

View File

@@ -18,32 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { LoaderFunctionArgs } from "@remix-run/node";
import { makeImage } from "../utils/generate-og-image.server";
import { formatDate } from "@notesnook/core";
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const title = url.searchParams.get("title") || "Not found";
const description = url.searchParams.get("description") || "";
const date =
url.searchParams.get("date") ||
formatDate(new Date(), {
type: "date-time",
dateFormat: "YYYY-MM-DD",
timeFormat: "24-hour"
});
return new Response(
await makeImage({
date,
description,
title
}),
{
status: 200,
headers: {
"Content-Type": "image/png"
}
}
);
url.pathname = url.pathname.replace(/.png/, ".jpg");
return Response.redirect(url, 308);
}

View File

@@ -17,12 +17,15 @@ 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 { json, LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { COMPATIBILITY_VERSION, INSTANCE_NAME } from "../utils/env";
export async function loader() {
return Response.json({
export async function loader({ request }: LoaderFunctionArgs) {
const response = json({
version: COMPATIBILITY_VERSION,
id: "monograph",
instance: INSTANCE_NAME
});
return cors(request, response);
}

View File

@@ -17,13 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const p = "process" in globalThis ? globalThis.process : ({ env: {} } as any);
export const API_HOST =
import.meta.env.API_HOST ||
process.env.API_HOST ||
`https://api.notesnook.com`;
import.meta.env.API_HOST || p.env.API_HOST || `https://api.notesnook.com`;
export const PUBLIC_URL =
import.meta.env.PUBLIC_URL ||
process.env.PUBLIC_URL ||
`http://localhost:${import.meta.env.PORT || process.env.PORT || 5017}`;
p.env.PUBLIC_URL ||
`http://localhost:${import.meta.env.PORT || p.env.PORT || 5173}`;
export const COMPATIBILITY_VERSION = 1;
export const INSTANCE_NAME = process.env.INSTANCE_NAME || "default";
export const INSTANCE_NAME = p.env.INSTANCE_NAME || "default";

View File

@@ -0,0 +1,174 @@
/*
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 { createCanvas, GlobalFonts, loadImage } from "@napi-rs/canvas";
import { LRUCache } from "lru-cache";
import { ThemeDark } from "@notesnook/theme";
import path from "path";
import { fileURLToPath } from "url";
import { split } from "canvas-hypertxt";
import { readFile } from "fs/promises";
export type OGMetadata = { title: string; description: string; date: string };
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT = path.join(__dirname, "../../");
const fontMap = JSON.parse(
await readFile(path.join(ROOT, "fonts", "fonts.json"), "utf-8")
);
// Register fonts
const OpenSans = path.join(
__dirname,
import.meta.env.DEV ? "../assets/fonts/" : "../../assets/fonts/",
"open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf"
);
const OpenSansBold = path.join(
__dirname,
import.meta.env.DEV ? "../assets/fonts/" : "../../assets/fonts/",
"open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf"
);
console.log("OpenSans", GlobalFonts.registerFromPath(OpenSans, "OpenSans"));
console.log(
"registering",
"OpenSansBold",
GlobalFonts.registerFromPath(OpenSansBold, "OpenSansBold")
);
const fontFamilies = {
regular: ["OpenSans"],
bold: ["OpenSansBold"]
};
for (const font of fontMap) {
const id = (font.name + font.weight).replace(/ /g, "");
const result = GlobalFonts.registerFromPath(
path.resolve(ROOT, font.path),
id
);
if (!result)
throw new Error(
`Failed to register font: ${id} at ${path.resolve(ROOT, font.path)}`
);
if (font.weight === "600") fontFamilies.bold.push(id);
else fontFamilies.regular.push(id);
console.log("registering", id, result);
}
const cache = new LRUCache<string, Buffer>({
ttl: 1000 * 60 * 60 * 24,
ttlAutopurge: true
});
const WIDTH = 1200;
const HEIGHT = 630;
const PADDING = 50;
const QUALITY = 80;
const logo = loadImage(
import.meta.env.DEV
? path.resolve(__dirname, "../../public/logo.svg")
: path.resolve(__dirname, "../../client/logo.svg")
);
const boldFontFamily = fontFamilies.bold.join(",");
const regularFontFamily = fontFamilies.regular.join(",");
export async function makeImage(metadata: OGMetadata, cacheKey: string) {
if (cache.has(cacheKey)) {
return cache.get(cacheKey)!;
}
console.time("canvas");
const theme = ThemeDark.scopes.base;
const canvas = createCanvas(WIDTH, HEIGHT);
const ctx = canvas.getContext("2d");
// Background
ctx.fillStyle = theme.primary.background;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Bottom border
ctx.fillStyle = "#008837";
ctx.fillRect(0, HEIGHT - 10, WIDTH, 10);
// Draw logo
ctx.drawImage(await logo, PADDING, HEIGHT - PADDING - 85, 80, 80);
// Draw bottom text
ctx.fillStyle = theme.primary.heading;
ctx.font = "600 32px OpenSansBold";
ctx.fillText("Notesnook Monograph", PADDING + 95, HEIGHT - PADDING - 55);
ctx.fillStyle = theme.secondary.paragraph;
ctx.font = "25px OpenSans";
ctx.fillText(
"Anonymous, secure, and encrypted note sharing with password protection.",
PADDING + 95,
HEIGHT - PADDING - 19
);
// Draw date
ctx.fillStyle = theme.secondary.paragraph;
ctx.font = "25px OpenSans";
ctx.fillText(metadata.date, PADDING, PADDING + 25);
// Draw title
ctx.fillStyle = theme.primary.heading;
ctx.font = `600 64px ${boldFontFamily}`;
let y = PADDING + 105;
const titleLines = split(
ctx as any,
metadata.title,
`600 64px ${boldFontFamily}`,
WIDTH - PADDING * 2,
true
);
for (const line of titleLines) {
ctx.fillText(line, PADDING, y);
y += 60;
}
// Draw description
ctx.fillStyle = theme.primary.paragraph;
ctx.font = `30px ${regularFontFamily}`;
const description = Buffer.from(
metadata.description || "",
"base64"
).toString("utf-8");
const descLines = split(
ctx as any,
description,
`30px ${regularFontFamily}`,
WIDTH - PADDING * 2,
true
).slice(0, 4);
for (const line of descLines) {
ctx.fillText(line, PADDING, y);
y += 40;
}
const buffer = canvas.toBuffer("image/jpeg", QUALITY);
console.timeEnd("canvas");
cache.set(cacheKey, buffer);
return buffer;
}

View File

@@ -1,148 +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 satori from "satori";
import { ThemeDark } from "@notesnook/theme";
import { svg2png, initialize } from "svg2png-wasm";
import svg2pngWasm from "svg2png-wasm/svg2png_wasm_bg.wasm?arraybuffer";
import fontRegular from "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf?arraybuffer";
import fontBold from "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf?arraybuffer";
export type OGMetadata = { title: string; description: string; date: string };
await initialize(svg2pngWasm);
export async function makeImage(metadata: OGMetadata) {
const theme = ThemeDark.scopes.base;
const svg = await satori(
<div
style={{
flexDirection: "column",
display: "flex",
justifyContent: "space-between",
height: "100%",
borderBottom: "10px solid #008837",
width: "100%",
padding: 50,
margin: 0
}}
>
<div style={{ display: "flex", flexDirection: "column" }}>
<p
style={{ fontSize: 25, margin: 0, color: theme.secondary.paragraph }}
>
{metadata.date}
</p>
<h1
style={{
margin: 0,
marginTop: 5,
fontSize: 64,
fontWeight: 600,
color: theme.primary.heading
}}
>
{metadata.title}
</h1>
<p
style={{
margin: 0,
marginTop: 5,
fontSize: 30,
color: theme.primary.paragraph
}}
>
{Buffer.from(metadata.description || "", "base64").toString("utf-8")}
</p>
</div>
<div style={{ display: "flex", flexDirection: "row", flexShrink: 0 }}>
<svg
width="80"
height="80"
viewBox="0 0 1024 1024"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_25_12)">
<path d="M1024 0H0V1024H1024V0Z" fill="white" />
<path
d="M736.652 699.667C719.397 750.078 684.817 792.732 639.064 820.039C593.311 847.346 539.354 857.535 486.794 848.792C434.234 840.049 386.481 812.942 352.032 772.294C317.583 731.646 298.673 680.095 298.667 626.812V516.562L377.788 549.615V626.767C377.78 647.546 382.221 668.085 390.812 687.005C399.402 705.924 411.943 722.785 427.592 736.455C430.562 739.042 433.644 741.562 436.828 743.914C460.184 761.302 488.23 771.266 517.322 772.511C518.312 772.511 519.268 772.59 520.247 772.612C521.225 772.635 522.497 772.612 523.622 772.612C524.747 772.612 525.872 772.612 526.997 772.612C528.122 772.612 528.932 772.612 529.922 772.511C559.002 771.263 587.038 761.308 610.393 743.936C613.565 741.585 616.648 739.076 619.629 736.489C640.186 718.51 655.286 695.123 663.212 668.989L736.652 699.667Z"
fill="black"
/>
<path
d="M748.667 430.748V626.813C748.667 629.344 748.667 631.887 748.509 634.418L669.545 601.399V430.748C669.533 393.064 654.939 356.847 628.82 329.682C602.702 302.518 567.086 286.514 529.431 285.022C491.777 283.53 455.006 296.666 426.82 321.679C398.634 346.692 381.221 381.641 378.227 419.206C377.945 423.008 377.788 426.867 377.788 430.748V479.461L298.667 446.386V205.748H523.667C583.34 205.748 640.57 229.453 682.766 271.649C724.961 313.845 748.667 371.074 748.667 430.748Z"
fill="black"
/>
</g>
<defs>
<clipPath id="clip0_25_12">
<rect width="1024" height="1024" rx="200" fill="white" />
</clipPath>
</defs>
</svg>
<div
style={{ display: "flex", flexDirection: "column", marginLeft: 15 }}
>
<p
style={{
margin: 0,
fontSize: 32,
fontWeight: 600,
color: theme.primary.heading
}}
>
Notesnook Monograph
</p>
<p
style={{
fontSize: 25,
margin: 0,
color: theme.secondary.paragraph
}}
>
Anonymous, secure, and encrypted note sharing with password
protection.
</p>
</div>
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Open Sans",
data: fontRegular!,
weight: 400,
style: "normal"
},
{
name: "Open Sans",
data: fontBold!,
weight: 600,
style: "normal"
}
]
}
);
return await svg2png(svg, {
backgroundColor: "black"
});
}

View File

@@ -74,7 +74,7 @@ export async function isSpam(monograph: Monograph) {
}
return isSpam;
} catch (e) {
console.error(e);
// console.error(e);
return false;
}
}

View File

@@ -23,7 +23,7 @@ export async function read<T>(key: string, fallback: T): Promise<T> {
try {
return (JSON.parse(await readFile(key, "utf-8")) as T) || fallback;
} catch (e) {
console.error(e);
// console.error(e);
return fallback;
}
}

View File

@@ -28,11 +28,11 @@ const cache: Record<
export async function read<T>(key: string, fallback: T) {
const cached = cache[key];
if (cached && cached.ttl > Date.now() - cached.cachedAt) {
return cached.value;
return cached.value as T;
}
const value = (await provider).read<T>(key, fallback);
cache[key] = {
ttl: 5 * 60000,
ttl: 60 * 60 * 1000,
value,
cachedAt: Date.now()
};
@@ -40,6 +40,10 @@ export async function read<T>(key: string, fallback: T) {
}
export async function write<T>(key: string, data: T) {
if (cache[key]) {
cache[key].value = data;
cache[key].cachedAt = Date.now();
}
return (await provider).write<T>(key, data);
}

View File

@@ -40,7 +40,7 @@ export async function read<T>(key: string, fallback: T): Promise<T> {
key
});
if (typeof response === "object" && !response.success) {
console.error("failed:", response.errors);
// console.error("failed:", response.errors);
return fallback;
}
return (
@@ -48,7 +48,7 @@ export async function read<T>(key: string, fallback: T): Promise<T> {
fallback
);
} catch (e) {
console.error(e);
// console.error(e);
return fallback;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,24 @@
{
"name": "@notesnook/monograph",
"version": "1.1.0",
"version": "1.2.2",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"build": "npm run fonts && remix vite:build",
"dev": "remix vite:dev",
"typecheck": "tsc",
"start": "remix-serve ./build/server/index.js"
"fonts": "node scripts/font-loader.mjs",
"start": "bun server.ts"
},
"dependencies": {
"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.1",
"@emotion/server": "11.11.0",
"@mdi/js": "^7.4.47",
"@lingui/core": "4.11.4",
"@lingui/react": "4.11.4",
"@mdi/js": "^7.4.47",
"@napi-rs/canvas": "0.1.59",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
"@notesnook/editor": "file:../../packages/editor",
@@ -28,11 +30,12 @@
"@sagi.io/workers-kv": "^0.0.14",
"@theme-ui/components": "^0.16.2",
"@theme-ui/core": "^0.16.2",
"buffer": "^6.0.3",
"canvas-hypertxt": "^1.0.3",
"comlink": "^4.4.1",
"date-fns": "^4.1.0",
"html-to-text": "^9.0.5",
"isbot": "^5.1.17",
"lru-cache": "^11.0.2",
"mac-scrollbar": "^0.13.6",
"nanoid": "^5.0.7",
"react": "18.3.1",
@@ -41,24 +44,24 @@
"react-turnstile": "^1.1.4",
"refractor": "^4.8.1",
"remix-utils": "^7.7.0",
"satori": "^0.11.1",
"slugify": "^1.6.6",
"svg2png-wasm": "^1.4.1",
"zustand": "^4.5.5"
},
"devDependencies": {
"@remix-run/dev": "^2.12.1",
"@remix-run/serve": "^2.12.1",
"@types/bun": "^1.1.10",
"@remix-run/server-runtime": "^2.15.0",
"@types/bun": "^1.1.13",
"@types/html-to-text": "^9.0.4",
"@types/react": "^18.3.9",
"@types/react-dom": "^18.3.0",
"@types/react-modal": "^3.16.3",
"autoprefixer": "^10.4.20",
"google-fonts-helper": "^3.6.0",
"postcss": "^8.4.47",
"vite": "^5.4.8",
"vite-plugin-arraybuffer": "^0.0.8",
"vite-plugin-wasm": "^3.3.0",
"vite-plugin-static-copy": "^2.1.0",
"vite-tsconfig-paths": "^5.0.1",
"wrangler": "3.78.11"
},

View File

@@ -0,0 +1,32 @@
<svg
width="80"
height="80"
viewBox="0 0 1024 1024"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_25_12)">
<rect
x="0"
y="0"
width="1024"
height="1024"
rx="130"
ry="130"
fill="white"
/>
<path
d="M736.652 699.667C719.397 750.078 684.817 792.732 639.064 820.039C593.311 847.346 539.354 857.535 486.794 848.792C434.234 840.049 386.481 812.942 352.032 772.294C317.583 731.646 298.673 680.095 298.667 626.812V516.562L377.788 549.615V626.767C377.78 647.546 382.221 668.085 390.812 687.005C399.402 705.924 411.943 722.785 427.592 736.455C430.562 739.042 433.644 741.562 436.828 743.914C460.184 761.302 488.23 771.266 517.322 772.511C518.312 772.511 519.268 772.59 520.247 772.612C521.225 772.635 522.497 772.612 523.622 772.612C524.747 772.612 525.872 772.612 526.997 772.612C528.122 772.612 528.932 772.612 529.922 772.511C559.002 771.263 587.038 761.308 610.393 743.936C613.565 741.585 616.648 739.076 619.629 736.489C640.186 718.51 655.286 695.123 663.212 668.989L736.652 699.667Z"
fill="black"
/>
<path
d="M748.667 430.748V626.813C748.667 629.344 748.667 631.887 748.509 634.418L669.545 601.399V430.748C669.533 393.064 654.939 356.847 628.82 329.682C602.702 302.518 567.086 286.514 529.431 285.022C491.777 283.53 455.006 296.666 426.82 321.679C398.634 346.692 381.221 381.641 378.227 419.206C377.945 423.008 377.788 426.867 377.788 430.748V479.461L298.667 446.386V205.748H523.667C583.34 205.748 640.57 229.453 682.766 271.649C724.961 313.845 748.667 371.074 748.667 430.748Z"
fill="black"
/>
</g>
<defs>
<clipPath id="clip0_25_12">
<rect width="1024" height="1024" rx="200" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,121 @@
/*
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 { createWriteStream, existsSync, writeFileSync } from "fs";
import { constructURL } from "google-fonts-helper";
import { Writable } from "stream";
import { mkdir } from "fs/promises";
const FONTS = [
{ locale: "ja-JP", slug: "Noto+Sans+JP", name: "Noto Sans JP" },
{ locale: "ko-KR", slug: "Noto+Sans+KR", name: "Noto Sans KR" },
{ locale: "zh-CN", slug: "Noto+Sans+SC", name: "Noto Sans SC" },
{ locale: "zh-TW", slug: "Noto+Sans+TC", name: "Noto Sans TC" },
{ locale: "zh-HK", slug: "Noto+Sans+HK", name: "Noto Sans HK" },
{ locale: "th-TH", slug: "Noto+Sans+Thai", name: "Noto Sans Thai" },
{ locale: "bn-IN", slug: "Noto+Sans+Bengali", name: "Noto Sans Bengali" },
{ locale: "ar-AR", slug: "Noto+Sans+Arabic", name: "Noto Sans Arabic" },
{ locale: "ta-IN", slug: "Noto+Sans+Tamil", name: "Noto Sans Tamil" },
{ locale: "ml-IN", slug: "Noto+Sans+Malayalam", name: "Noto Sans Malayalam" },
{ locale: "he-IL", slug: "Noto+Sans+Hebrew", name: "Noto Sans Hebrew" },
{ locale: "te-IN", slug: "Noto+Sans+Telugu", name: "Noto Sans Telugu" },
{
locale: "devanagari",
slug: "Noto+Sans+Devanagari",
name: "Noto Sans Devanagari"
},
{ locale: "kannada", slug: "Noto+Sans+Kannada", name: "Noto Sans Kannada" }
// { locale: "symbol", slug: "Noto+Sans+Symbols", name: "Noto Sans Symbols" },
// {
// locale: "symbol",
// slug: "Noto+Sans+Symbols+2",
// name: "Noto Sans Symbols 2"
// },
// { locale: "math", slug: "Noto+Sans+Math", name: "Noto Sans Math" }
];
async function loadFonts(dir) {
const families = {};
for (const font of FONTS) {
families[font.name] = [400, 600];
}
const css = await (
await fetch(constructURL({ families }), {
headers: {
// Make sure it returns TTF.
"User-Agent":
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1"
}
})
).text();
const fontFaces = css
.split("@font-face")
.filter(Boolean)
.map((fontFace) => {
const src = fontFace.match(
/src: url\((.+)\) format\('(opentype|truetype)'\)/
);
const family = fontFace.match(/font-family: '(.+)'/);
const weight = fontFace.match(/font-weight: (\d+)/);
const style = fontFace.match(/font-style: (\w+)/);
return {
src: src[1],
family: family[1],
weight: weight[1],
style: style[1]
};
});
await mkdir("fonts", { recursive: true });
const fontMap = [];
for (const face of fontFaces) {
console.log("Downloading", face.family, face.weight);
const font = FONTS.find((font) => font.name === face.family);
if (!font) continue;
const fileName = `${font.slug}+${face.weight}.ttf`;
const path = `fonts/${fileName}`;
if (existsSync(path)) continue;
const fileStream = Writable.toWeb(
createWriteStream(path, {
autoClose: true,
emitClose: true,
flush: true
})
);
await (await fetch(face.src)).body.pipeTo(fileStream);
fontMap.push({
name: font.name,
locale: font.locale,
weight: face.weight,
path
});
}
writeFileSync("fonts/fonts.json", JSON.stringify(fontMap, null, 2));
console.log("Done");
}
loadFonts();

48
apps/monograph/server.ts Normal file
View File

@@ -0,0 +1,48 @@
/*
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/>.
*/
// This server file is used to serve the Remix app in production using Bun.
// run it like so: npm run build; cd output; bun install; bun run start
// Running it directly will give an error.
import type { ServerBuild } from "@remix-run/server-runtime";
import { createRequestHandler } from "@remix-run/server-runtime";
import { resolve } from "node:path";
// @ts-expect-error server is not built yet
import * as build from "./build/server/index";
import { type Serve } from "bun";
const remix = createRequestHandler(
build as unknown as ServerBuild,
Bun.env.NODE_ENV
);
process.env.PORT = process.env.PORT || "3000";
export default {
port: process.env.PORT,
async fetch(request) {
// First we need to send handle static files
const { pathname } = new URL(request.url);
const file = Bun.file(
resolve(__dirname, "./build/client/", `.${pathname}`)
);
if (await file.exists()) return new Response(file);
// Only if a file doesn't exists we send the request to the Remix request handler
return remix(request);
}
} satisfies Serve;

View File

@@ -9,7 +9,7 @@
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["vite/client", "vite-plugin-arraybuffer/types"],
"types": ["vite/client", "vite-plugin-arraybuffer/types", "bun"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",

View File

@@ -20,11 +20,11 @@ import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import arraybuffer from "vite-plugin-arraybuffer";
import wasm from "vite-plugin-wasm";
import { ThemeDark } from "@notesnook/theme";
import ThemeDark from "@notesnook/theme/theme-engine/themes/default-dark.json" with {type:"json"};
import type { Plugin, ResolvedConfig } from "vite";
import { writeFile } from "fs/promises";
import path from "path";
import { viteStaticCopy } from "vite-plugin-static-copy";
import * as pkg from "./package.json";
const DEDUPE = [
@@ -44,17 +44,19 @@ const DEFAULT_THEME_KEY =
export default defineConfig(({ isSsrBuild }) => ({
plugins: [
writePlugin({
"package.json": JSON.stringify({
"../package.json": JSON.stringify({
name: pkg.name,
version: pkg.version,
type: "module",
scripts: { start: pkg.scripts.start },
dependencies: {
"@remix-run/serve": pkg.devDependencies["@remix-run/serve"]
}
"@napi-rs/canvas": pkg.dependencies["@napi-rs/canvas"],
"@remix-run/server-runtime": pkg.devDependencies["@remix-run/server-runtime"],
},
})
}),
remix({
buildDirectory: "output/build",
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
@@ -63,7 +65,13 @@ export default defineConfig(({ isSsrBuild }) => ({
}),
tsconfigPaths(),
arraybuffer(),
wasm()
isSsrBuild ? viteStaticCopy({
targets: [
{ src: "./server.ts", dest: "../../" },
{ src: "./app/assets", dest: "../" },
{ src: "./fonts", dest: "../" }
]
}) : undefined
],
worker: {
format: "es",
@@ -74,13 +82,15 @@ export default defineConfig(({ isSsrBuild }) => ({
}
},
ssr: {
...(process.env.NODE_ENV === "development" ? {} : { noExternal: true }),
...(process.env.NODE_ENV === "development"
? {}
: { noExternal: true, external: ["@napi-rs/canvas"] }),
target: "node"
},
build: {
target: isSsrBuild ? "node20" : undefined,
rollupOptions: {
external: ["svg2png-wasm/svg2png_wasm_bg.wasm"]
external: ["@napi-rs/canvas"]
}
},
define: {

View File

@@ -973,7 +973,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "3.0.20",
"version": "3.0.22",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -1,4 +1,4 @@
Subtotal: ₹300
Sales tax: ₹200
Discount: -₹300
Total: ₹300/mo
Total: ₹300

View File

@@ -1,4 +1,4 @@
Subtotal: ₹300
Sales tax: ₹200
Discount: -₹300
Total: ₹300/mo
Total: ₹300

View File

@@ -1,4 +1,4 @@
Subtotal: ₹300
Sales tax: ₹200
Discount: -₹300
Total: ₹300/mo
Total: ₹300

View File

@@ -1,4 +1,4 @@
Subtotal: $100
Sales tax: $0
Discount: -$100
Total: $100/mo
Total: $100

View File

@@ -1,4 +1,4 @@
Subtotal: $100
Sales tax: $0
Discount: -$100
Total: $100/mo
Total: $100

View File

@@ -1,4 +1,4 @@
Subtotal: $100
Sales tax: $0
Discount: -$100
Total: $100/mo
Total: $100

View File

@@ -231,6 +231,23 @@ test("add tags to note", async ({ page }) => {
expect(noteTags.every((t, i) => t === tags[i])).toBe(true);
});
test("add tags to locked note", async ({ page }) => {
const tags = ["incognito", "secret-stuff"];
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.lock(PASSWORD);
await note?.openLockedNote(PASSWORD);
await notes.editor.setTags(tags);
await page.waitForTimeout(200);
const noteTags = await notes.editor.getTags();
expect(noteTags).toHaveLength(tags.length);
expect(noteTags.every((t, i) => t === tags[i])).toBe(true);
});
for (const format of ["html", "txt", "md"] as const) {
test(`export note as ${format}`, async ({ page }) => {
const app = new AppModel(page);

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.0.21",
"version": "3.0.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.0.21",
"version": "3.0.22",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -25191,6 +25191,7 @@
"@readme/data-urls": "^3.0.0",
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.9-alpha",
"@types/mime-db": "^1.43.5",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"dom-serializer": "^2.0.0",
@@ -25203,7 +25204,7 @@
"katex": "0.16.2",
"linkedom": "^0.14.17",
"liqe": "^1.13.0",
"mime": "^4.0.4",
"mime-db": "^1.53.0",
"prismjs": "^1.29.0",
"qclone": "^1.2.0",
"rfdc": "^1.3.0",
@@ -35615,7 +35616,7 @@
},
"../desktop": {
"name": "@notesnook/desktop",
"version": "3.0.20",
"version": "3.0.22",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

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

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