Compare commits

...

118 Commits

Author SHA1 Message Date
Ammar Ahmed
b296f5dd5c mobile: update export options 2024-12-24 15:59:33 +05:00
Abdullah Atta
2dc5e4fef5 docs(help): add notice on gift card page 2024-12-24 15:54:44 +05:00
Abdullah Atta
f836de3854 docs(help): clarify about expiry date on gift cards 2024-12-24 15:54:44 +05:00
Ammar Ahmed
631a591477 mobile: update ios provisioning 2024-12-24 15:31:06 +05:00
Ammar Ahmed
1bb27145ce mobile: release v3.0.24 2024-12-24 14:14:16 +05:00
Ammar Ahmed
79b89c2e85 mobile: skip migration progress during tests 2024-12-24 14:10:11 +05:00
Ammar Ahmed
ceb2a8c747 intl: fix locales data 2024-12-24 12:06:52 +05:00
Ammar Ahmed
bee06ecf18 mobile: fix multi file sharing 2024-12-24 12:06:52 +05:00
Ammar Ahmed
33c7eb5a74 mobile: fix imports 2024-12-24 12:06:52 +05:00
Abdullah Atta
2cb788712a web: fix editor toolbar scroll ui 2024-12-24 11:13:57 +05:00
Abdullah Atta
90d2338412 Revert "web: fix editor toolbar scroll styling (#7113)"
This reverts commit 5229408156.
2024-12-24 10:31:14 +05:00
01zulfi
4bebf5a7b5 editor: fix task list state & readonly bugs (#7117)
* fix task list complete state not showing on first load
* fix task list allowing paste when readonly

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2024-12-24 10:18:23 +05:00
Ammar Ahmed
68eb5023f8 mobile: update deps 2024-12-24 10:10:56 +05:00
Abdullah Atta
8692c87ed7 intl: fix resend code string 2024-12-24 10:10:56 +05:00
Abdullah Atta
ea0623c6bc web: create backup on submit when changing password 2024-12-24 10:10:56 +05:00
Abdullah Atta
b526dc4d39 core: hash old password before deriving new crypto key 2024-12-24 10:10:56 +05:00
Ammar Ahmed
76a1025f59 mobile: do not call clearSessions when changing password 2024-12-24 10:10:56 +05:00
Ammar Ahmed
85e046a379 mobile: fix error in shortcut manager 2024-12-24 10:10:56 +05:00
Ammar Ahmed
58a6bbd262 mobile: fix logout handling 2024-12-24 10:10:56 +05:00
Ammar Ahmed
07aabf9a2a core: return false if no fallback key 2024-12-24 10:10:56 +05:00
Ammar Ahmed
8c7ba6b963 mobile: fix profile not rendered on home 2024-12-24 10:10:56 +05:00
Ammar Ahmed
d125e0f6d9 mobile: fix encryption 2024-12-24 10:10:56 +05:00
Ammar Ahmed
43d017d64a mobile: fix code input when changing email 2024-12-24 10:10:56 +05:00
Abdullah Atta
bc3e7c6a53 web: add noop fallback methods in storage 2024-12-24 10:10:56 +05:00
Abdullah Atta
6e8610c358 core: compute usesFallbackPWHash instead of storing it
this fixes the fallback logic for already logged in users
2024-12-24 10:10:56 +05:00
Abdullah Atta
ab00645c23 web: fix recovery view button text not wrapping 2024-12-23 18:29:10 +05:00
Abdullah Atta
e041976ae0 web: fix gift cards link 2024-12-23 18:06:09 +05:00
Abdullah Atta
9d7e6b40a0 web: bump version to 3.0.23 2024-12-23 18:05:43 +05:00
Abdullah Atta
db40a648ab web: fix Cannot read properties of undefined (reading 'instance') 2024-12-23 17:53:14 +05:00
Abdullah Atta
805e60a808 web: handle NetworkError when attempting to fetch resource. 2024-12-23 17:46:53 +05:00
Abdullah Atta
8c7093189b core: add support for sorting search results
search results are sorted by date created for now until
we add proper support for sorting search results
in the ui
2024-12-23 14:30:45 +05:00
Abdullah Atta
3092696c83 docs: add docs for gift cards 2024-12-23 14:30:25 +05:00
Ammar Ahmed
c2e7f7576e mobile: redeem gift code 2024-12-23 14:30:25 +05:00
Abdullah Atta
6b4f7900ec web: show subscription provider info in subscription settings 2024-12-23 14:30:25 +05:00
Abdullah Atta
523ae5b07b intl: add other subscription providers 2024-12-23 14:30:25 +05:00
Abdullah Atta
a628b3fd92 intl: add strings for gift redeeming 2024-12-23 14:30:25 +05:00
Abdullah Atta
e9f3037129 web: add gift redeem support 2024-12-23 14:30:25 +05:00
Abdullah Atta
46d4b9dece core: add api for redeeming gift codes 2024-12-23 14:30:25 +05:00
01zulfi
c701baaa2c web: better ux when autosave is off (#6610) (#6849)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-12-23 14:30:03 +05:00
Ammar Ahmed
a798f8e280 mobile: fix intro button position 2024-12-23 13:43:57 +05:00
Abdullah Atta
baebf79046 web: fix type errors 2024-12-23 12:29:07 +05:00
Abdullah Atta
5727aa6c4d web: fix checkout discounted snapshots 2024-12-21 16:41:16 +05:00
Abdullah Atta
36cf1feff9 web: fix crash on opening sort menu 2024-12-21 16:41:04 +05:00
Abdullah Atta
91a3b0f5ed web: fix imports with nested notebooks 2024-12-21 16:15:29 +05:00
Ammar Ahmed
293219d781 mobile: fix encryption on ios 2024-12-21 14:15:56 +05:00
Ammar Ahmed
2cd4f44a92 core: fix fallback key encryption 2024-12-21 14:15:56 +05:00
Abdullah Atta
4b9a95ded7 core: add support for fallback hashing
on ios some users using passwords with multi byte characters
generates the wrong hash. This commit adds support for fallback
to allow those users to continue to login once the fix has been applied.
2024-12-21 14:15:56 +05:00
Ammar Ahmed
565b0382f9 mobile: fix encryption on ios 2024-12-21 14:15:56 +05:00
Ammar Ahmed
1e837f2a9c mobile: fix undo/redo buttons in editor 2024-12-21 14:13:07 +05:00
Ammar Ahmed
765ebbe10c mobile: allow saving empty string input 2024-12-21 14:12:24 +05:00
Ammar Ahmed
e95d1601a6 mobile: fix sharing files on android 2024-12-21 14:10:55 +05:00
Ammar Ahmed
c6dd1d1615 mobile: fix snooze time not rendered properly 2024-12-21 14:10:34 +05:00
Ammar Ahmed
bb50850995 Merge pull request #7008 from streetwriters/fix-localization-error 2024-12-21 14:10:05 +05:00
Ammar Ahmed
83083134e9 Merge branch 'master' into fix-localization-error
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-12-21 14:09:22 +05:00
01zulfi
61b8960aa5 web: fix mfa fallback string (#7123)
* also add selected style to mfa type selector button

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

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

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

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

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.0.21",
"version": "3.0.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": {

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { readFile, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}
if (process.argv[1] === __filename) {
console.log("Patching better-sqlite3");
patchBetterSQLite3();
}

View File

@@ -17,15 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
import { SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { MMKV } from "./mmkv";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
@@ -62,6 +63,7 @@ const KEYSTORE_CONFIG = Platform.select({
function generatePassword() {
const length = 80;
//@ts-ignore
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
@@ -78,27 +80,27 @@ function generatePassword() {
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
const key = getDatabaseKey();
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
const key = (await getDatabaseKey()) as string;
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = await encrypt(appLockCredentials, key);
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = await decrypt(
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = (await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
);
)) as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
@@ -110,13 +112,16 @@ export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
return true;
}
export async function setAppLockVerificationCipher(appLockPassword) {
export async function setAppLockVerificationCipher(appLockPassword: string) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = await encrypt(appLockCredentials, generatePassword());
const encrypted = (await encrypt(
appLockCredentials,
generatePassword()
)) as Cipher;
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
@@ -129,9 +134,9 @@ export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword) {
export async function validateAppLockPassword(appLockPassword: string) {
try {
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
@@ -146,17 +151,18 @@ export async function validateAppLockPassword(appLockPassword) {
}
}
let DB_KEY;
let DB_KEY: string | undefined;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword) {
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKeyCipher: Cipher =
CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
@@ -172,13 +178,12 @@ export async function getDatabaseKey(appLockPassword) {
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
let credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
KEYSTORE_CONFIG
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = credentials.password;
DB_KEY = (credentials as Keychain.UserCredentials).password;
}
}
@@ -190,7 +195,7 @@ export async function getDatabaseKey(appLockPassword) {
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key;
DB_KEY = derivedDatabaseKey.key as string;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
@@ -202,18 +207,17 @@ export async function getDatabaseKey(appLockPassword) {
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook",
KEYSTORE_CONFIG
"notesnook"
);
if (userKeyCredentials) {
const userKeyCipher = await encrypt(
const userKeyCipher: Cipher = (await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
);
)) as Cipher;
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
@@ -223,45 +227,84 @@ export async function getDatabaseKey(appLockPassword) {
return DB_KEY;
} catch (e) {
ToastManager.error(e, "Error getting database key");
ToastManager.error(e as Error, "Error getting database key");
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKey(data) {
export async function deriveCryptoKeyFallback(data: SerializedKey) {
if (Platform.OS !== "ios") return;
try {
let credentials = await Sodium.deriveKey(data.password, data.salt);
const userKeyCipher = await encrypt(
if (!data.password || !data.salt)
throw new Error(
"Invalid password and salt provided to deriveCryptoKeyFallback"
);
const credentials = await Sodium.deriveKeyFallback?.(
data.password,
data.salt
);
if (!credentials) return;
const userKeyCipher = (await encrypt(
{
key: await getDatabaseKey(),
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key
);
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key fallback stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey(_name) {
export async function deriveCryptoKey(data: SerializedKey) {
try {
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
const credentials = (await Sodium.deriveKey(
data.password,
data.salt
)) as Password;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey() {
try {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return null;
return undefined;
}
const key = await decrypt(
{
key: await getDatabaseKey(),
key: (await getDatabaseKey()) as string,
salt: keyCipher.salt
},
keyCipher
@@ -274,7 +317,7 @@ export async function getCryptoKey(_name) {
}
}
export async function removeCryptoKey(_name) {
export async function removeCryptoKey() {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
@@ -284,44 +327,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
}))

View File

@@ -16,26 +16,26 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger } from "@notesnook/core";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { logger as dbLogger, ICompressor } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import SettingsService from "../../services/settings";
import { strings } from "@notesnook/intl";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { FileStorage } from "../filesystem";
import { getDatabaseKey } from "./encryption";
import "./logger";
import { RNSqliteDriver } from "./sqlite.kysely";
import { Storage } from "./storage";
export async function setupDatabase(password) {
export async function setupDatabase(password?: string) {
const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed());
@@ -47,17 +47,21 @@ export async function setupDatabase(password) {
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
...(SettingsService.getProperty("serverUrls") || {})
});
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: () => ({
compress: Gzip.deflate,
decompress: Gzip.inflate
}),
eventsource: (Platform.OS === "ios"
? EventSource
: AndroidEventSource) as any,
fs: FileStorage,
compressor: async () =>
({
compress: Gzip.deflate,
decompress: Gzip.inflate
} as ICompressor),
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({

View File

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

View File

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

View File

@@ -16,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import { getFileNameWithExtension } from "@notesnook/core";
import { strings } from "@notesnook/intl";
@@ -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;
}
}

View File

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

View File

@@ -17,24 +17,40 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { IFileStorage } from "@notesnook/core";
import { checkAttachment, downloadFile } from "./download";
import {
bulkExists,
clearCache,
clearFileStorage,
deleteCacheFileByName,
deleteCacheFileByPath,
deleteFile,
exists,
readEncrypted,
writeEncryptedBase64,
getCacheSize,
hashBase64,
readEncrypted,
writeEncryptedBase64
} from "./io";
import { uploadFile } from "./upload";
import {
cancelable,
checkAndCreateDir,
getUploadedFileSize,
requestPermission
} from "./utils";
export default {
checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
bulkExists,
getCacheSize
} from "./io";
import { uploadFile } from "./upload";
import { cancelable, getUploadedFileSize } from "./utils";
getCacheSize,
requestPermission,
checkAndCreateDir
};
export default {
export const FileStorage: IFileStorage = {
readEncrypted,
writeEncryptedBase64,
hashBase64,
@@ -44,10 +60,5 @@ export default {
exists,
clearFileStorage,
getUploadedFileSize,
checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
bulkExists,
getCacheSize
bulkExists
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,7 +91,6 @@ export const Reminders = ({
/>
<FloatingButton
title={strings.setReminder()}
onPress={() => {
ReminderSheet.present();
}}

View File

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

View File

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

View File

@@ -17,24 +17,24 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Clipboard from "@react-native-clipboard/clipboard";
import { LogMessage } from "@notesnook/logger";
import { sanitizeFilename } from "@notesnook/common";
import { format, LogLevel, logManager } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { LogMessage } from "@notesnook/logger";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "react-native-blob-util";
import Storage from "../../common/database/storage";
import * as ScopedStorage from "react-native-scoped-storage";
import filesystem from "../../common/filesystem";
import { presentDialog } from "../../components/dialog/functions";
import { IconButton } from "../../components/ui/icon-button";
import { Notice } from "../../components/ui/notice";
import Paragraph from "../../components/ui/typography/paragraph";
import useTimer from "../../hooks/use-timer";
import { ToastManager } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { hexToRGBA } from "../../utils/colors";
import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
export default function DebugLogs() {
const { colors } = useThemeColors();
@@ -148,7 +148,7 @@ export default function DebugLogs() {
if (!file) return;
path = file.uri;
} else {
path = await Storage.checkAndCreateDir("/");
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName + ".txt", data, "utf8");
path = path + fileName;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { sanitizeFilename } from "@notesnook/common";
import { formatDate } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
@@ -26,20 +27,14 @@ import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import { zip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../common/database";
import storage from "../common/database/storage";
import filesystem from "../common/filesystem";
import filesystem, { FileStorage } from "../common/filesystem";
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
import { presentDialog } from "../components/dialog/functions";
import {
endProgress,
startProgress,
updateProgress
} from "../components/dialogs/progress";
import { endProgress, updateProgress } from "../components/dialogs/progress";
import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
import { strings } from "@notesnook/intl";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -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") &&

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1063,7 +1063,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2118;
CURRENT_PROJECT_VERSION = 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;

View File

@@ -970,6 +970,8 @@ PODS:
- React
- react-native-background-actions (2.6.7):
- React-Core
- react-native-begin-background-task (0.1.0):
- React
- react-native-blob-util (0.18.3):
- React-Core
- react-native-config (1.5.1):
@@ -981,7 +983,7 @@ PODS:
- react-native-document-picker (7.1.3):
- React-Core
- react-native-fingerprint-scanner (5.0.0):
- React
- React-Core
- react-native-get-random-values (1.9.0):
- React-Core
- react-native-gzip (1.1.0):
@@ -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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.21",
"version": "3.0.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"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { LoaderFunctionArgs } from "@remix-run/node";
import { makeImage } from "../utils/generate-og-image.server";
import { formatDate } from "@notesnook/core";
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const title = url.searchParams.get("title") || "Not found";
const description = url.searchParams.get("description") || "";
const date =
url.searchParams.get("date") ||
formatDate(new Date(), {
type: "date-time",
dateFormat: "YYYY-MM-DD",
timeFormat: "24-hour"
});
return new Response(
await makeImage(
{
date,
description,
title
},
url.search
),
{
status: 200,
headers: {
"Content-Type": "image/jpeg"
}
}
);
}

View File

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

View File

@@ -17,12 +17,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { COMPATIBILITY_VERSION, INSTANCE_NAME } from "../utils/env";
export async function loader() {
return Response.json({
export async function loader({ request }: LoaderFunctionArgs) {
const response = json({
version: COMPATIBILITY_VERSION,
id: "monograph",
instance: INSTANCE_NAME
});
return cors(request, response);
}

View File

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

View File

@@ -0,0 +1,174 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createCanvas, GlobalFonts, loadImage } from "@napi-rs/canvas";
import { LRUCache } from "lru-cache";
import { ThemeDark } from "@notesnook/theme";
import path from "path";
import { fileURLToPath } from "url";
import { split } from "canvas-hypertxt";
import { readFile } from "fs/promises";
export type OGMetadata = { title: string; description: string; date: string };
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT = path.join(__dirname, "../../");
const fontMap = JSON.parse(
await readFile(path.join(ROOT, "fonts", "fonts.json"), "utf-8")
);
// Register fonts
const OpenSans = path.join(
__dirname,
import.meta.env.DEV ? "../assets/fonts/" : "../../assets/fonts/",
"open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf"
);
const OpenSansBold = path.join(
__dirname,
import.meta.env.DEV ? "../assets/fonts/" : "../../assets/fonts/",
"open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf"
);
console.log("OpenSans", GlobalFonts.registerFromPath(OpenSans, "OpenSans"));
console.log(
"registering",
"OpenSansBold",
GlobalFonts.registerFromPath(OpenSansBold, "OpenSansBold")
);
const fontFamilies = {
regular: ["OpenSans"],
bold: ["OpenSansBold"]
};
for (const font of fontMap) {
const id = (font.name + font.weight).replace(/ /g, "");
const result = GlobalFonts.registerFromPath(
path.resolve(ROOT, font.path),
id
);
if (!result)
throw new Error(
`Failed to register font: ${id} at ${path.resolve(ROOT, font.path)}`
);
if (font.weight === "600") fontFamilies.bold.push(id);
else fontFamilies.regular.push(id);
console.log("registering", id, result);
}
const cache = new LRUCache<string, Buffer>({
ttl: 1000 * 60 * 60 * 24,
ttlAutopurge: true
});
const WIDTH = 1200;
const HEIGHT = 630;
const PADDING = 50;
const QUALITY = 80;
const logo = loadImage(
import.meta.env.DEV
? path.resolve(__dirname, "../../public/logo.svg")
: path.resolve(__dirname, "../../client/logo.svg")
);
const boldFontFamily = fontFamilies.bold.join(",");
const regularFontFamily = fontFamilies.regular.join(",");
export async function makeImage(metadata: OGMetadata, cacheKey: string) {
if (cache.has(cacheKey)) {
return cache.get(cacheKey)!;
}
console.time("canvas");
const theme = ThemeDark.scopes.base;
const canvas = createCanvas(WIDTH, HEIGHT);
const ctx = canvas.getContext("2d");
// Background
ctx.fillStyle = theme.primary.background;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Bottom border
ctx.fillStyle = "#008837";
ctx.fillRect(0, HEIGHT - 10, WIDTH, 10);
// Draw logo
ctx.drawImage(await logo, PADDING, HEIGHT - PADDING - 85, 80, 80);
// Draw bottom text
ctx.fillStyle = theme.primary.heading;
ctx.font = "600 32px OpenSansBold";
ctx.fillText("Notesnook Monograph", PADDING + 95, HEIGHT - PADDING - 55);
ctx.fillStyle = theme.secondary.paragraph;
ctx.font = "25px OpenSans";
ctx.fillText(
"Anonymous, secure, and encrypted note sharing with password protection.",
PADDING + 95,
HEIGHT - PADDING - 19
);
// Draw date
ctx.fillStyle = theme.secondary.paragraph;
ctx.font = "25px OpenSans";
ctx.fillText(metadata.date, PADDING, PADDING + 25);
// Draw title
ctx.fillStyle = theme.primary.heading;
ctx.font = `600 64px ${boldFontFamily}`;
let y = PADDING + 105;
const titleLines = split(
ctx as any,
metadata.title,
`600 64px ${boldFontFamily}`,
WIDTH - PADDING * 2,
true
);
for (const line of titleLines) {
ctx.fillText(line, PADDING, y);
y += 60;
}
// Draw description
ctx.fillStyle = theme.primary.paragraph;
ctx.font = `30px ${regularFontFamily}`;
const description = Buffer.from(
metadata.description || "",
"base64"
).toString("utf-8");
const descLines = split(
ctx as any,
description,
`30px ${regularFontFamily}`,
WIDTH - PADDING * 2,
true
).slice(0, 4);
for (const line of descLines) {
ctx.fillText(line, PADDING, y);
y += 40;
}
const buffer = canvas.toBuffer("image/jpeg", QUALITY);
console.timeEnd("canvas");
cache.set(cacheKey, buffer);
return buffer;
}

View File

@@ -1,148 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import satori from "satori";
import { ThemeDark } from "@notesnook/theme";
import { svg2png, initialize } from "svg2png-wasm";
import svg2pngWasm from "svg2png-wasm/svg2png_wasm_bg.wasm?arraybuffer";
import fontRegular from "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf?arraybuffer";
import fontBold from "../assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf?arraybuffer";
export type OGMetadata = { title: string; description: string; date: string };
await initialize(svg2pngWasm);
export async function makeImage(metadata: OGMetadata) {
const theme = ThemeDark.scopes.base;
const svg = await satori(
<div
style={{
flexDirection: "column",
display: "flex",
justifyContent: "space-between",
height: "100%",
borderBottom: "10px solid #008837",
width: "100%",
padding: 50,
margin: 0
}}
>
<div style={{ display: "flex", flexDirection: "column" }}>
<p
style={{ fontSize: 25, margin: 0, color: theme.secondary.paragraph }}
>
{metadata.date}
</p>
<h1
style={{
margin: 0,
marginTop: 5,
fontSize: 64,
fontWeight: 600,
color: theme.primary.heading
}}
>
{metadata.title}
</h1>
<p
style={{
margin: 0,
marginTop: 5,
fontSize: 30,
color: theme.primary.paragraph
}}
>
{Buffer.from(metadata.description || "", "base64").toString("utf-8")}
</p>
</div>
<div style={{ display: "flex", flexDirection: "row", flexShrink: 0 }}>
<svg
width="80"
height="80"
viewBox="0 0 1024 1024"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_25_12)">
<path d="M1024 0H0V1024H1024V0Z" fill="white" />
<path
d="M736.652 699.667C719.397 750.078 684.817 792.732 639.064 820.039C593.311 847.346 539.354 857.535 486.794 848.792C434.234 840.049 386.481 812.942 352.032 772.294C317.583 731.646 298.673 680.095 298.667 626.812V516.562L377.788 549.615V626.767C377.78 647.546 382.221 668.085 390.812 687.005C399.402 705.924 411.943 722.785 427.592 736.455C430.562 739.042 433.644 741.562 436.828 743.914C460.184 761.302 488.23 771.266 517.322 772.511C518.312 772.511 519.268 772.59 520.247 772.612C521.225 772.635 522.497 772.612 523.622 772.612C524.747 772.612 525.872 772.612 526.997 772.612C528.122 772.612 528.932 772.612 529.922 772.511C559.002 771.263 587.038 761.308 610.393 743.936C613.565 741.585 616.648 739.076 619.629 736.489C640.186 718.51 655.286 695.123 663.212 668.989L736.652 699.667Z"
fill="black"
/>
<path
d="M748.667 430.748V626.813C748.667 629.344 748.667 631.887 748.509 634.418L669.545 601.399V430.748C669.533 393.064 654.939 356.847 628.82 329.682C602.702 302.518 567.086 286.514 529.431 285.022C491.777 283.53 455.006 296.666 426.82 321.679C398.634 346.692 381.221 381.641 378.227 419.206C377.945 423.008 377.788 426.867 377.788 430.748V479.461L298.667 446.386V205.748H523.667C583.34 205.748 640.57 229.453 682.766 271.649C724.961 313.845 748.667 371.074 748.667 430.748Z"
fill="black"
/>
</g>
<defs>
<clipPath id="clip0_25_12">
<rect width="1024" height="1024" rx="200" fill="white" />
</clipPath>
</defs>
</svg>
<div
style={{ display: "flex", flexDirection: "column", marginLeft: 15 }}
>
<p
style={{
margin: 0,
fontSize: 32,
fontWeight: 600,
color: theme.primary.heading
}}
>
Notesnook Monograph
</p>
<p
style={{
fontSize: 25,
margin: 0,
color: theme.secondary.paragraph
}}
>
Anonymous, secure, and encrypted note sharing with password
protection.
</p>
</div>
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Open Sans",
data: fontRegular!,
weight: 400,
style: "normal"
},
{
name: "Open Sans",
data: fontBold!,
weight: 600,
style: "normal"
}
]
}
);
return await svg2png(svg, {
backgroundColor: "black"
});
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

After

Width:  |  Height:  |  Size: 1.8 KiB

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