mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
118 Commits
fix-ios-sy
...
3.0.24-and
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b296f5dd5c | ||
|
|
2dc5e4fef5 | ||
|
|
f836de3854 | ||
|
|
631a591477 | ||
|
|
1bb27145ce | ||
|
|
79b89c2e85 | ||
|
|
ceb2a8c747 | ||
|
|
bee06ecf18 | ||
|
|
33c7eb5a74 | ||
|
|
2cb788712a | ||
|
|
90d2338412 | ||
|
|
4bebf5a7b5 | ||
|
|
68eb5023f8 | ||
|
|
8692c87ed7 | ||
|
|
ea0623c6bc | ||
|
|
b526dc4d39 | ||
|
|
76a1025f59 | ||
|
|
85e046a379 | ||
|
|
58a6bbd262 | ||
|
|
07aabf9a2a | ||
|
|
8c7ba6b963 | ||
|
|
d125e0f6d9 | ||
|
|
43d017d64a | ||
|
|
bc3e7c6a53 | ||
|
|
6e8610c358 | ||
|
|
ab00645c23 | ||
|
|
e041976ae0 | ||
|
|
9d7e6b40a0 | ||
|
|
db40a648ab | ||
|
|
805e60a808 | ||
|
|
8c7093189b | ||
|
|
3092696c83 | ||
|
|
c2e7f7576e | ||
|
|
6b4f7900ec | ||
|
|
523ae5b07b | ||
|
|
a628b3fd92 | ||
|
|
e9f3037129 | ||
|
|
46d4b9dece | ||
|
|
c701baaa2c | ||
|
|
a798f8e280 | ||
|
|
baebf79046 | ||
|
|
5727aa6c4d | ||
|
|
36cf1feff9 | ||
|
|
91a3b0f5ed | ||
|
|
293219d781 | ||
|
|
2cd4f44a92 | ||
|
|
4b9a95ded7 | ||
|
|
565b0382f9 | ||
|
|
1e837f2a9c | ||
|
|
765ebbe10c | ||
|
|
e95d1601a6 | ||
|
|
c6dd1d1615 | ||
|
|
bb50850995 | ||
|
|
83083134e9 | ||
|
|
61b8960aa5 | ||
|
|
5229408156 | ||
|
|
0921fef267 | ||
|
|
60989359e2 | ||
|
|
8fe381afa9 | ||
|
|
fea9c4e42b | ||
|
|
1d78402d5d | ||
|
|
aa72c33aea | ||
|
|
69504a150e | ||
|
|
a78a2e4ae2 | ||
|
|
0ffbf83141 | ||
|
|
75c346a460 | ||
|
|
93af79926d | ||
|
|
1b354e4bca | ||
|
|
6d53b78f6a | ||
|
|
8e90adccab | ||
|
|
c19635b16a | ||
|
|
94e4b00294 | ||
|
|
76908b1fbd | ||
|
|
7409c5382d | ||
|
|
2462eb1631 | ||
|
|
986ff32293 | ||
|
|
8f7fd9d8ee | ||
|
|
54d6a5c307 | ||
|
|
d672a43a83 | ||
|
|
48343e5d5c | ||
|
|
0d3a8c88c8 | ||
|
|
31367066f7 | ||
|
|
f7c097988a | ||
|
|
b7f1f36090 | ||
|
|
8756299d80 | ||
|
|
3525d60f8c | ||
|
|
d08a485111 | ||
|
|
c203b035c4 | ||
|
|
0f4b2bdae7 | ||
|
|
cb0bc39cd1 | ||
|
|
84abfa3478 | ||
|
|
a40a48e2c1 | ||
|
|
124499cabb | ||
|
|
e874d1efd4 | ||
|
|
fb1a17566f | ||
|
|
8dd8447212 | ||
|
|
eba7341d45 | ||
|
|
08bd36d518 | ||
|
|
7a45522ff3 | ||
|
|
4ee2923181 | ||
|
|
48459efdb1 | ||
|
|
c848f3c588 | ||
|
|
52a75cfdaf | ||
|
|
9331b5f919 | ||
|
|
2ff53d4650 | ||
|
|
7f2c8ca05a | ||
|
|
9e752e9406 | ||
|
|
b8c3e2098f | ||
|
|
4308e3bf49 | ||
|
|
25956ccadb | ||
|
|
af3fba5176 | ||
|
|
4afaa6a36e | ||
|
|
e98e1a5f36 | ||
|
|
abfdad0fec | ||
|
|
99e50ddad6 | ||
|
|
67a61dd3f3 | ||
|
|
13df01d55a | ||
|
|
af4ea036c8 |
@@ -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 = {
|
||||
|
||||
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.21",
|
||||
"version": "3.0.23",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.21",
|
||||
"version": "3.0.23",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.21",
|
||||
"version": "3.0.23",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
48
apps/desktop/scripts/patch-better-sqlite3.mjs
Normal file
48
apps/desktop/scripts/patch-better-sqlite3.mjs
Normal 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();
|
||||
}
|
||||
@@ -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,79 @@ 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 "";
|
||||
}
|
||||
|
||||
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 Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export async function generateCryptoKeyFallback(
|
||||
password: string,
|
||||
salt?: string
|
||||
): Promise<SerializedKey> {
|
||||
return Sodium.deriveKeyFallback?.(
|
||||
password,
|
||||
salt as string
|
||||
) 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 +409,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 +444,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 +456,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
|
||||
}))
|
||||
@@ -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) => ({
|
||||
@@ -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;
|
||||
|
||||
@@ -16,58 +16,52 @@ 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
|
||||
generateCryptoKeyFallback
|
||||
} 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 +73,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 +107,46 @@ 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
|
||||
generateCryptoKeyFallback,
|
||||
deriveCryptoKeyFallback
|
||||
};
|
||||
@@ -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";
|
||||
@@ -25,12 +24,12 @@ import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import filesystem from ".";
|
||||
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 { 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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"}
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -206,7 +206,7 @@ export const AttachmentDialog = ({
|
||||
errorOnly={currentFilter === "errors"}
|
||||
attachments={attachments}
|
||||
id={index}
|
||||
context="global"
|
||||
context={!isSheet ? "global" : "attachments-list"}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -68,11 +68,15 @@ 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);
|
||||
ToastManager.show({
|
||||
heading: strings.passwordChangedSuccessfully(),
|
||||
@@ -84,6 +88,7 @@ export const ChangePassword = () => {
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
} catch (e) {
|
||||
console.log(e.stack);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.passwordChangeFailed(),
|
||||
|
||||
@@ -188,7 +188,7 @@ const PDFPreview = () => {
|
||||
}}
|
||||
color={colors.static.white}
|
||||
>
|
||||
{strings.loadingWithProgress(progress.percent)}
|
||||
{strings.loadingWithProgress(progress?.percent)}
|
||||
</Paragraph>
|
||||
</Animated.View>
|
||||
) : (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -128,7 +128,6 @@ const Intro = ({ navigation }) => {
|
||||
style={{
|
||||
width: deviceMode !== "mobile" ? width / 2 : "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
@@ -138,7 +137,7 @@ const Intro = ({ navigation }) => {
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
minHeight: height * 0.7
|
||||
minHeight: height * 0.7 - (insets.top + insets.bottom)
|
||||
}}
|
||||
>
|
||||
<SwiperFlatList
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -134,8 +134,10 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
key="code-input"
|
||||
fwdRef={codeInputRef}
|
||||
placeholder={strings.verifyNewEmail()}
|
||||
placeholder={strings.code()}
|
||||
defaultValue=""
|
||||
onChangeText={(code) => {
|
||||
emailChangeData.current.code = code;
|
||||
}}
|
||||
|
||||
@@ -301,8 +301,8 @@ export const NotebookSheet = () => {
|
||||
}}
|
||||
onPress={async () => {
|
||||
await deleteItems(
|
||||
useItemSelectionStore.getState().getSelectedItemIds(),
|
||||
"notebook"
|
||||
"notebook",
|
||||
useItemSelectionStore.getState().getSelectedItemIds()
|
||||
);
|
||||
useSelectionStore.getState().clearSelection();
|
||||
useItemSelectionStore.setState({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -17,9 +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 { User } from "@notesnook/core";
|
||||
import { EV, EVENTS, SYNC_CHECK_IDS, SyncStatusEvent } from "@notesnook/core";
|
||||
import { EventManagerSubscription } from "@notesnook/core";
|
||||
import {
|
||||
EV,
|
||||
EVENTS,
|
||||
EventManagerSubscription,
|
||||
SYNC_CHECK_IDS,
|
||||
SyncStatusEvent,
|
||||
User
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import notifee from "@notifee/react-native";
|
||||
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
@@ -41,6 +47,7 @@ import * as RNIap from "react-native-iap";
|
||||
import { DatabaseLogger, db, setupDatabase } from "../common/database";
|
||||
import { initializeLogger } from "../common/database/logger";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import { endProgress, startProgress } from "../components/dialogs/progress";
|
||||
import Migrate from "../components/sheets/migrate";
|
||||
import NewFeature from "../components/sheets/new-feature";
|
||||
import { Walkthrough } from "../components/walkthroughs";
|
||||
@@ -52,6 +59,7 @@ import {
|
||||
} from "../screens/editor/tiptap/utils";
|
||||
import { useDragState } from "../screens/settings/editor/state";
|
||||
import BackupService from "../services/backup";
|
||||
import BiometricService from "../services/biometrics";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
@@ -66,14 +74,17 @@ import {
|
||||
setRecoveryKeyMessage,
|
||||
setUpdateAvailableMessage
|
||||
} from "../services/message";
|
||||
import Navigation from "../services/navigation";
|
||||
import Notifications from "../services/notifications";
|
||||
import PremiumService from "../services/premium";
|
||||
import SettingsService from "../services/settings";
|
||||
import Sync from "../services/sync";
|
||||
import { initAfterSync } from "../stores";
|
||||
import { clearAllStores, initAfterSync } from "../stores";
|
||||
import { refreshAllStores } from "../stores/create-db-collection-store";
|
||||
import { useAttachmentStore } from "../stores/use-attachment-store";
|
||||
import { useMessageStore } from "../stores/use-message-store";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { changeSystemBarColors } from "../stores/use-theme-store";
|
||||
import { SyncStatus, useUserStore } from "../stores/use-user-store";
|
||||
import { updateStatusBarColor } from "../utils/colors";
|
||||
import { BETA } from "../utils/constants";
|
||||
@@ -89,11 +100,8 @@ import {
|
||||
} from "../utils/events";
|
||||
import { getGithubVersion } from "../utils/github-version";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import { sleep } from "../utils/time";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { changeSystemBarColors } from "../stores/use-theme-store";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { endProgress, startProgress } from "../components/dialogs/progress";
|
||||
import { sleep } from "../utils/time";
|
||||
|
||||
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -140,6 +148,7 @@ const onUploadedAttachmentProgress = (data: any) => {
|
||||
};
|
||||
|
||||
const onUserSessionExpired = async () => {
|
||||
console.log("LOGGED OUT USER....");
|
||||
SettingsService.set({
|
||||
sessionExpired: true
|
||||
});
|
||||
@@ -208,11 +217,19 @@ const onRequestPartialSync = async (
|
||||
};
|
||||
|
||||
const onLogout = async (reason: string) => {
|
||||
DatabaseLogger.log("User Logged Out" + reason);
|
||||
Notifications.setupReminders(true);
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
DatabaseLogger.log("User Logged Out " + reason);
|
||||
setLoginMessage();
|
||||
await PremiumService.setPremiumStatus();
|
||||
await BiometricService.resetCredentials();
|
||||
MMKV.clearStore();
|
||||
clearAllStores();
|
||||
setImmediate(() => {
|
||||
refreshAllStores();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
SettingsService.resetSettings();
|
||||
useUserStore.getState().setUser(null);
|
||||
useUserStore.getState().setSyncing(false);
|
||||
};
|
||||
|
||||
async function checkForShareExtensionLaunchedInBackground() {
|
||||
@@ -581,7 +598,8 @@ export const useAppEvents = () => {
|
||||
EV.subscribe(EVENTS.migrationStarted, (name) => {
|
||||
if (
|
||||
name !== "notesnook" ||
|
||||
!SettingsService.getProperty("introCompleted")
|
||||
!SettingsService.getProperty("introCompleted") ||
|
||||
Config.isTesting === "true"
|
||||
)
|
||||
return;
|
||||
startProgress({
|
||||
@@ -594,7 +612,8 @@ export const useAppEvents = () => {
|
||||
EV.subscribe(EVENTS.migrationFinished, (name) => {
|
||||
if (
|
||||
name !== "notesnook" ||
|
||||
!SettingsService.getProperty("introCompleted")
|
||||
!SettingsService.getProperty("introCompleted") ||
|
||||
Config.isTesting === "true"
|
||||
)
|
||||
return;
|
||||
endProgress();
|
||||
|
||||
@@ -97,7 +97,7 @@ const _TabsHolder = () => {
|
||||
tabBarRef.current?.goToPage(1, false);
|
||||
return;
|
||||
}
|
||||
if (item.type === "notesnook.action.newnote") {
|
||||
if (item && item.type === "notesnook.action.newnote") {
|
||||
clearAppState();
|
||||
if (!tabBarRef.current) {
|
||||
await sleep(3000);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -37,6 +37,7 @@ export type EditorState = {
|
||||
scrollPosition: number;
|
||||
overlay?: boolean;
|
||||
initialLoadCalled?: boolean;
|
||||
editorStateRestored?: boolean;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
|
||||
@@ -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;
|
||||
}, []);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -91,7 +91,6 @@ export const Reminders = ({
|
||||
/>
|
||||
|
||||
<FloatingButton
|
||||
title={strings.setReminder()}
|
||||
onPress={() => {
|
||||
ReminderSheet.present();
|
||||
}}
|
||||
|
||||
@@ -17,6 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors, VariantsWithStaticColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, {
|
||||
Dispatch,
|
||||
@@ -26,11 +29,11 @@ import React, {
|
||||
useState
|
||||
} from "react";
|
||||
import { ActivityIndicator, Linking, Platform, View } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { db } from "../../common/database";
|
||||
import Storage from "../../common/database/storage";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import DialogHeader from "../../components/dialog/dialog-header";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
@@ -46,13 +49,10 @@ import {
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors, VariantsWithStaticColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
const mfaMethods: MFAMethod[] = [
|
||||
{
|
||||
id: "app",
|
||||
@@ -525,7 +525,7 @@ export const MFARecoveryCodes = ({
|
||||
if (!file) return;
|
||||
path = file.uri;
|
||||
} else {
|
||||
path = await Storage.checkAndCreateDir("/");
|
||||
path = await filesystem.checkAndCreateDir("/");
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
path + fileName,
|
||||
codeString,
|
||||
|
||||
@@ -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 : (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const Home = ({
|
||||
});
|
||||
|
||||
const renderItem = ({ item }: { item: SettingSection; index: number }) =>
|
||||
item.name === "account" ? (
|
||||
item.id === "account" ? (
|
||||
<SettingsUserSection item={item} />
|
||||
) : (
|
||||
<SectionGroup item={item} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -18,8 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { User } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import notifee from "@notifee/react-native";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
import { Appearance, Linking, Platform } from "react-native";
|
||||
@@ -28,7 +30,6 @@ import * as RNIap from "react-native-iap";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { ChangePassword } from "../../components/auth/change-password";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
@@ -54,14 +55,11 @@ import {
|
||||
openVault,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { setLoginMessage } from "../../services/message";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Notifications from "../../services/notifications";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import Sync from "../../services/sync";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { refreshAllStores } from "../../stores/create-db-collection-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
|
||||
@@ -77,9 +75,6 @@ import { useDragState } from "./editor/state";
|
||||
import { verifyUser, verifyUserWithApplock } from "./functions";
|
||||
import { SettingSection } from "./types";
|
||||
import { getTimeLeft } from "./user-section";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
|
||||
type User = any;
|
||||
|
||||
export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
@@ -119,6 +114,10 @@ export const settingsGroups: SettingSection[] = [
|
||||
"MMMM D, YYYY"
|
||||
);
|
||||
|
||||
if (user.subscription.provider === 4) {
|
||||
return strings.subEndsOn(expiryDate);
|
||||
}
|
||||
|
||||
return user.subscription?.type === 2
|
||||
? strings.signedUpOn(startDate)
|
||||
: user.subscription?.type === 1
|
||||
@@ -134,6 +133,39 @@ export const settingsGroups: SettingSection[] = [
|
||||
: strings.neverHesitate();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "redeem-gift-code",
|
||||
name: strings.redeemGiftCode(),
|
||||
description: strings.redeemGiftCodeDesc(),
|
||||
hidden: (current) => {
|
||||
return !current as boolean;
|
||||
},
|
||||
useHook: () =>
|
||||
useUserStore(
|
||||
(state) =>
|
||||
state.user?.subscription.type == SUBSCRIPTION_STATUS.TRIAL ||
|
||||
state.user?.subscription.type == SUBSCRIPTION_STATUS.BASIC
|
||||
),
|
||||
icon: "gift",
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: strings.redeemGiftCode(),
|
||||
paragraph: strings.redeemGiftCodeDesc(),
|
||||
input: true,
|
||||
inputPlaceholder: strings.code(),
|
||||
positiveText: strings.redeem(),
|
||||
positivePress: async (value) => {
|
||||
db.subscriptions.redeemCode(value).catch((e) => {
|
||||
ToastManager.show({
|
||||
heading: "Error redeeming code",
|
||||
message: (e as Error).message,
|
||||
type: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "account-settings",
|
||||
type: "screen",
|
||||
@@ -447,18 +479,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
});
|
||||
|
||||
await db.user?.logout();
|
||||
setLoginMessage();
|
||||
await PremiumService.setPremiumStatus();
|
||||
await BiometricService.resetCredentials();
|
||||
MMKV.clearStore();
|
||||
clearAllStores();
|
||||
setImmediate(() => {
|
||||
refreshAllStores();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
SettingsService.resetSettings();
|
||||
useUserStore.getState().setUser(null);
|
||||
useUserStore.getState().setSyncing(false);
|
||||
endProgress();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
@@ -1166,13 +1186,20 @@ export const settingsGroups: SettingSection[] = [
|
||||
type: "component",
|
||||
hidden: () => !useUserStore.getState().user,
|
||||
name: strings.automaticBackupsWithAttachments(),
|
||||
description: strings.automaticBackupsWithAttachmentsDesc(),
|
||||
description: [
|
||||
...strings.automaticBackupsWithAttachmentsDesc()
|
||||
].join("\n"),
|
||||
component: "autobackupsattachments"
|
||||
},
|
||||
{
|
||||
id: "select-backup-dir",
|
||||
name: strings.selectBackupDir(),
|
||||
description: strings.selectBackupDirDesc(),
|
||||
description: () => {
|
||||
const desc = strings.selectBackupDirDesc(
|
||||
SettingsService.get().backupDirectoryAndroid?.path || ""
|
||||
);
|
||||
return desc[0] + " " + desc[1];
|
||||
},
|
||||
icon: "folder",
|
||||
hidden: () =>
|
||||
!!SettingsService.get().backupDirectoryAndroid ||
|
||||
|
||||
@@ -126,18 +126,19 @@ export const Subscription = () => {
|
||||
user.subscription?.type !== SUBSCRIPTION_STATUS.PREMIUM_EXPIRED &&
|
||||
user.subscription?.type !== SUBSCRIPTION_STATUS.BASIC ? (
|
||||
<Button
|
||||
title={subscriptionProviderInfo?.title}
|
||||
title={subscriptionProviderInfo?.title()}
|
||||
onPress={() => {
|
||||
presentSheet({
|
||||
title: subscriptionProviderInfo.title,
|
||||
paragraph: subscriptionProviderInfo.desc
|
||||
title: subscriptionProviderInfo.title(),
|
||||
paragraph: subscriptionProviderInfo.desc()
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
borderRadius: 100
|
||||
width: "100%",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
fontSize={SIZE.sm}
|
||||
fontSize={SIZE.xs}
|
||||
height={30}
|
||||
type="secondaryAccented"
|
||||
/>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -90,9 +85,13 @@ async function checkBackupDirExists(reset = false, context = "global") {
|
||||
resolve(await getDirectoryAndroid());
|
||||
return;
|
||||
}
|
||||
const desc = strings.selectBackupDirDesc(
|
||||
SettingsService.get().backupDirectoryAndroid?.path || ""
|
||||
);
|
||||
|
||||
presentDialog({
|
||||
title: strings.selectBackupDir(),
|
||||
paragraph: strings.selectBackupDirDesc(),
|
||||
paragraph: desc[0] + " " + desc,
|
||||
positivePress: async () => {
|
||||
resolve(await getDirectoryAndroid());
|
||||
},
|
||||
@@ -178,7 +177,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 +231,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 +298,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") &&
|
||||
|
||||
@@ -20,10 +20,10 @@ 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";
|
||||
import { Storage } from "../common/database/storage";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { ToastOptions, ToastManager } from "./event-manager";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
|
||||
@@ -123,6 +123,7 @@ export type PresentSheetOptions = {
|
||||
};
|
||||
|
||||
export function presentSheet(data: Partial<PresentSheetOptions>) {
|
||||
console.log("PRESENTING...");
|
||||
eSendEvent(eOpenSheet, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const STORE_LINK =
|
||||
|
||||
export const GROUP = {
|
||||
default: "default",
|
||||
None: "none",
|
||||
none: "none",
|
||||
abc: "abc",
|
||||
year: "year",
|
||||
week: "week",
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -116,7 +116,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3031
|
||||
versionCode 3034
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -129,6 +129,16 @@
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Make Note">
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Make Note">
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
- Added full support for localization in Notesnook
|
||||
- Improved search experience
|
||||
- Allow user to cancel logging in
|
||||
- Fixed scrolling focused line into view
|
||||
- Support self hosted monograph server
|
||||
- Fixed markdown link pasting in editor
|
||||
- Many other bug fixes and improvements
|
||||
- You can now share multiple files to Notesnook
|
||||
- Fix file and image sharing not working
|
||||
- Many other bug fixes and small improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
Binary file not shown.
@@ -5,11 +5,11 @@
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>org.streetwriters.notesnook</key>
|
||||
<string>Notesnook App Distribution 2024</string>
|
||||
<string>Notesnook App Distribution 2025</string>
|
||||
<key>org.streetwriters.notesnook.notewidget</key>
|
||||
<string>Notesnook Widget Distribution 2024</string>
|
||||
<string>Notesnook Widget Distribution 2025</string>
|
||||
<key>org.streetwriters.notesnook.share</key>
|
||||
<string>Notesnook Share Distribution 2024</string>
|
||||
<string>Notesnook Share Distribution 2025</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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 = 2121;
|
||||
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.24;
|
||||
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 = 2121;
|
||||
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.24;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1252,7 +1252,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook;
|
||||
PRODUCT_NAME = Notesnook;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook App Distribution 2024";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook App Distribution 2025";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Notesnook-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -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 = 2121;
|
||||
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.24;
|
||||
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 = 2121;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1457,12 +1457,12 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.21;
|
||||
MARKETING_VERSION = 3.0.24;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Widget Distribution 2024";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Widget Distribution 2025";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
@@ -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 = 2121;
|
||||
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.24;
|
||||
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 = 2121;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1667,12 +1667,12 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.21;
|
||||
MARKETING_VERSION = 3.0.24;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Share Distribution 2024";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Share Distribution 2025";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Make Note/Make Note-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -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):
|
||||
@@ -1017,9 +1019,9 @@ PODS:
|
||||
- react-native-screenguard (1.0.0):
|
||||
- React-Core
|
||||
- SDWebImage (~> 5.11.1)
|
||||
- react-native-share-extension (2.6.0):
|
||||
- react-native-share-extension (2.7.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
|
||||
@@ -1852,8 +1858,8 @@ SPEC CHECKSUMS:
|
||||
react-native-quick-sqlite: 18e1367c34faac90e37f6eb3e78c196e9b674b5d
|
||||
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
|
||||
react-native-screenguard: 8b36a3df84c76cd2b82c477f71c26fa1c8cc14a0
|
||||
react-native-share-extension: 25437eb1039f7409be6e80a7edf8d02b42e1dc99
|
||||
react-native-sodium: 605c1523ec8ff5fbff5e9e7769bbacceb571a3c6
|
||||
react-native-share-extension: 17e42444d0d9fbfeb0a7392899def70f9534c9c4
|
||||
react-native-sodium: 4cb76086943a7f60c42b40ebca866695b360a196
|
||||
react-native-theme-switch-animation: d3eb50365a3829ce5572628888fa514752703f61
|
||||
react-native-webview: 553abd09f58e340fdc7746c9e2ae096839e99911
|
||||
React-nativeconfig: ba9a2e54e2f0882cf7882698825052793ed4c851
|
||||
|
||||
@@ -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.8.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",
|
||||
|
||||
@@ -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/,
|
||||
|
||||
628
apps/mobile/package-lock.json
generated
628
apps/mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.21",
|
||||
"version": "3.0.24",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
19
apps/mobile/patches/react-native-material-menu+2.0.0.patch
Normal file
19
apps/mobile/patches/react-native-material-menu+2.0.0.patch
Normal 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);
|
||||
@@ -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,
|
||||
@@ -81,7 +81,8 @@ const EXTRA_ICON_NAMES = [
|
||||
"notebook-plus",
|
||||
"arrow-right-bold-box-outline",
|
||||
"arrow-up-bold",
|
||||
"login"
|
||||
"login",
|
||||
"gift"
|
||||
];
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
@@ -43,7 +43,7 @@ import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import isURL from "validator/lib/isURL";
|
||||
import { DatabaseLogger, db } from "../app/common/database";
|
||||
import Storage from "../app/common/database/storage";
|
||||
import { Storage } from "../app/common/database/storage";
|
||||
import { Button } from "../app/components/ui/button";
|
||||
import Heading from "../app/components/ui/typography/heading";
|
||||
import Paragraph from "../app/components/ui/typography/paragraph";
|
||||
@@ -620,41 +620,45 @@ const ShareView = () => {
|
||||
>
|
||||
Tap to remove an attachment.
|
||||
</Paragraph>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginTop: 6
|
||||
}}
|
||||
onPress={() => {
|
||||
setCompress(!compress);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={20}
|
||||
name={
|
||||
compress
|
||||
? "checkbox-marked"
|
||||
: "checkbox-blank-outline"
|
||||
}
|
||||
color={
|
||||
compress ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<Text
|
||||
{rawFiles.some((item) => isImage(item.type)) ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 3,
|
||||
fontSize: 12
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginTop: 6
|
||||
}}
|
||||
onPress={() => {
|
||||
setCompress(!compress);
|
||||
}}
|
||||
>
|
||||
Compress image (recommended)
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Icon
|
||||
size={20}
|
||||
name={
|
||||
compress
|
||||
? "checkbox-marked"
|
||||
: "checkbox-blank-outline"
|
||||
}
|
||||
color={
|
||||
compress
|
||||
? colors.primary.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 3,
|
||||
fontSize: 12
|
||||
}}
|
||||
>
|
||||
Compress image(s) (recommended)
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<View
|
||||
|
||||
2
apps/monograph/.gitignore
vendored
2
apps/monograph/.gitignore
vendored
@@ -6,3 +6,5 @@ node_modules
|
||||
.dev.vars
|
||||
|
||||
.wrangler
|
||||
/output
|
||||
/fonts
|
||||
@@ -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" ]
|
||||
@@ -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" }} />
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
52
apps/monograph/app/routes/api.og[.jpg].ts
Normal file
52
apps/monograph/app/routes/api.og[.jpg].ts
Normal 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"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
174
apps/monograph/app/utils/generate-og-image.server.ts
Normal file
174
apps/monograph/app/utils/generate-og-image.server.ts
Normal 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;
|
||||
}
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export async function isSpam(monograph: Monograph) {
|
||||
}
|
||||
return isSpam;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
836
apps/monograph/package-lock.json
generated
836
apps/monograph/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
},
|
||||
|
||||
32
apps/monograph/public/logo.svg
Normal file
32
apps/monograph/public/logo.svg
Normal 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 |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user