mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
50 Commits
fix-encryp
...
fix-ts-err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a203b05fe | ||
|
|
303d1adc96 | ||
|
|
b296f5dd5c | ||
|
|
2dc5e4fef5 | ||
|
|
f836de3854 | ||
|
|
631a591477 | ||
|
|
1bb27145ce | ||
|
|
79b89c2e85 | ||
|
|
ceb2a8c747 | ||
|
|
bee06ecf18 | ||
|
|
33c7eb5a74 | ||
|
|
2cb788712a | ||
|
|
90d2338412 | ||
|
|
4bebf5a7b5 | ||
|
|
68eb5023f8 | ||
|
|
8692c87ed7 | ||
|
|
ea0623c6bc | ||
|
|
b526dc4d39 | ||
|
|
76a1025f59 | ||
|
|
85e046a379 | ||
|
|
58a6bbd262 | ||
|
|
07aabf9a2a | ||
|
|
8c7ba6b963 | ||
|
|
d125e0f6d9 | ||
|
|
43d017d64a | ||
|
|
bc3e7c6a53 | ||
|
|
6e8610c358 | ||
|
|
ab00645c23 | ||
|
|
e041976ae0 | ||
|
|
9d7e6b40a0 | ||
|
|
db40a648ab | ||
|
|
805e60a808 | ||
|
|
8c7093189b | ||
|
|
3092696c83 | ||
|
|
c2e7f7576e | ||
|
|
6b4f7900ec | ||
|
|
523ae5b07b | ||
|
|
a628b3fd92 | ||
|
|
e9f3037129 | ||
|
|
46d4b9dece | ||
|
|
c701baaa2c | ||
|
|
a798f8e280 | ||
|
|
baebf79046 | ||
|
|
5727aa6c4d | ||
|
|
36cf1feff9 | ||
|
|
91a3b0f5ed | ||
|
|
293219d781 | ||
|
|
2cd4f44a92 | ||
|
|
4b9a95ded7 | ||
|
|
565b0382f9 |
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.22",
|
||||
"version": "3.0.23",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.22",
|
||||
"version": "3.0.23",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.22",
|
||||
"version": "3.0.23",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -17,15 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
|
||||
import { SerializedKey } from "@notesnook/crypto";
|
||||
import { Platform } from "react-native";
|
||||
import "react-native-get-random-values";
|
||||
import * as Keychain from "react-native-keychain";
|
||||
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
@@ -62,6 +63,7 @@ const KEYSTORE_CONFIG = Platform.select({
|
||||
|
||||
function generatePassword() {
|
||||
const length = 80;
|
||||
//@ts-ignore
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
if (typeof crypto === "undefined") {
|
||||
throw new Error(
|
||||
@@ -78,27 +80,27 @@ function generatePassword() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
|
||||
const key = getDatabaseKey();
|
||||
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
|
||||
const key = (await getDatabaseKey()) as string;
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const databaseKeyCipher = await encrypt(appLockCredentials, key);
|
||||
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
|
||||
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
|
||||
// We reset the database key from keychain once app lock password is set.
|
||||
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = await decrypt(
|
||||
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
|
||||
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
|
||||
const databaseKey = (await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
},
|
||||
databaseKeyCipher
|
||||
);
|
||||
)) as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
@@ -110,13 +112,16 @@ export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
export async function setAppLockVerificationCipher(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCredentials = await Sodium.deriveKey(
|
||||
appLockPassword,
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
const encrypted = (await encrypt(
|
||||
appLockCredentials,
|
||||
generatePassword()
|
||||
)) as Cipher;
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
@@ -129,9 +134,9 @@ export async function clearAppLockVerificationCipher() {
|
||||
CipherStorage.removeItem(APPLOCK_CIPHER);
|
||||
}
|
||||
|
||||
export async function validateAppLockPassword(appLockPassword) {
|
||||
export async function validateAppLockPassword(appLockPassword: string) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
@@ -146,17 +151,18 @@ export async function validateAppLockPassword(appLockPassword) {
|
||||
}
|
||||
}
|
||||
|
||||
let DB_KEY;
|
||||
let DB_KEY: string | undefined;
|
||||
export function clearDatabaseKey() {
|
||||
DB_KEY = undefined;
|
||||
DatabaseLogger.info("Cleared database key");
|
||||
}
|
||||
|
||||
export async function getDatabaseKey(appLockPassword) {
|
||||
export async function getDatabaseKey(appLockPassword?: string) {
|
||||
if (DB_KEY) return DB_KEY;
|
||||
try {
|
||||
if (appLockPassword) {
|
||||
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKeyCipher: Cipher =
|
||||
CipherStorage.getMap("databaseKeyCipher");
|
||||
const databaseKey = await decrypt(
|
||||
{
|
||||
password: appLockPassword
|
||||
@@ -172,13 +178,12 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
if (hasKey) {
|
||||
let credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
KEYSTORE_CONFIG
|
||||
const credentials = await Keychain.getInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY
|
||||
);
|
||||
|
||||
DatabaseLogger.info("Getting database key from Keychain");
|
||||
DB_KEY = credentials.password;
|
||||
DB_KEY = (credentials as Keychain.UserCredentials).password;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +195,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
NOTESNOOK_DB_KEY_SALT
|
||||
);
|
||||
|
||||
DB_KEY = derivedDatabaseKey.key;
|
||||
DB_KEY = derivedDatabaseKey.key as string;
|
||||
|
||||
await Keychain.setInternetCredentials(
|
||||
KEYCHAIN_SERVER_DBKEY,
|
||||
@@ -202,18 +207,17 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
|
||||
if (await Keychain.hasInternetCredentials("notesnook")) {
|
||||
const userKeyCredentials = await Keychain.getInternetCredentials(
|
||||
"notesnook",
|
||||
KEYSTORE_CONFIG
|
||||
"notesnook"
|
||||
);
|
||||
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
const userKeyCipher: Cipher = (await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
)) as Cipher;
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
@@ -223,45 +227,84 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
ToastManager.error(e as Error, "Error getting database key");
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deriveCryptoKey(data) {
|
||||
export async function deriveCryptoKeyFallback(data: SerializedKey) {
|
||||
if (Platform.OS !== "ios") return;
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
const userKeyCipher = await encrypt(
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error(
|
||||
"Invalid password and salt provided to deriveCryptoKeyFallback"
|
||||
);
|
||||
|
||||
const credentials = await Sodium.deriveKeyFallback?.(
|
||||
data.password,
|
||||
data.salt
|
||||
);
|
||||
|
||||
if (!credentials) return;
|
||||
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key fallback stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
return credentials.key;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey(_name) {
|
||||
export async function deriveCryptoKey(data: SerializedKey) {
|
||||
try {
|
||||
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
if (!data.password || !data.salt)
|
||||
throw new Error("Invalid password and salt provided to deriveCryptoKey");
|
||||
|
||||
const credentials = (await Sodium.deriveKey(
|
||||
data.password,
|
||||
data.salt
|
||||
)) as Password;
|
||||
const userKeyCipher = (await encrypt(
|
||||
{
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key as string
|
||||
)) as Cipher<"base64">;
|
||||
DatabaseLogger.info("User key stored: ", {
|
||||
userKeyCipher: !!userKeyCipher
|
||||
});
|
||||
|
||||
// Store encrypted user key in MMKV
|
||||
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCryptoKey() {
|
||||
try {
|
||||
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
|
||||
if (!keyCipher) {
|
||||
DatabaseLogger.info("User key cipher is null");
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
key: (await getDatabaseKey()) as string,
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
@@ -274,7 +317,7 @@ export async function getCryptoKey(_name) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCryptoKey(_name) {
|
||||
export async function removeCryptoKey() {
|
||||
try {
|
||||
MMKV.removeItem(USER_KEY_CIPHER);
|
||||
await Keychain.resetInternetCredentials("notesnook");
|
||||
@@ -284,44 +327,79 @@ export async function removeCryptoKey(_name) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRandomBytes(length) {
|
||||
export async function getRandomBytes(length: number) {
|
||||
return await generateSecureRandom(length);
|
||||
}
|
||||
|
||||
export async function hash(password, email) {
|
||||
let result = await Sodium.hashPassword(password, email);
|
||||
return result;
|
||||
}
|
||||
export async function hash(
|
||||
password: string,
|
||||
email: string,
|
||||
options?: { usesFallback?: boolean }
|
||||
) {
|
||||
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
|
||||
|
||||
export async function generateCryptoKey(password, salt) {
|
||||
try {
|
||||
let credentials = await Sodium.deriveKey(password, salt || null);
|
||||
return credentials;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
if (options?.usesFallback && Platform.OS !== "ios") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
options?.usesFallback
|
||||
? await Sodium.hashPasswordFallback?.(password, email)
|
||||
: await Sodium.hashPassword(password, email)
|
||||
) as string;
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant) {
|
||||
export async function generateCryptoKey(password: string, salt?: string) {
|
||||
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export async function generateCryptoKeyFallback(
|
||||
password: string,
|
||||
salt?: string
|
||||
): Promise<SerializedKey> {
|
||||
return Sodium.deriveKeyFallback?.(
|
||||
password,
|
||||
salt as string
|
||||
) as Promise<SerializedKey>;
|
||||
}
|
||||
|
||||
export function getAlgorithm(base64Variant: number) {
|
||||
return `xcha-argon2i13-${base64Variant}`;
|
||||
}
|
||||
|
||||
export async function decrypt(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
let _data = { ...data };
|
||||
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
|
||||
const _data = { ...data };
|
||||
_data.output = "plain";
|
||||
|
||||
if (!password.salt) password.salt = data.salt;
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decrypt(key, _data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decrypt(fallbackKey, _data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
export async function decryptMulti(
|
||||
password: Password,
|
||||
data: Cipher<"base64">[]
|
||||
) {
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
@@ -331,10 +409,30 @@ export async function decryptMulti(password, data) {
|
||||
password.salt = data[0].salt;
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios" && !password.key && password.password) {
|
||||
const key = await Sodium.deriveKey(password.password, password.salt);
|
||||
try {
|
||||
return await Sodium.decryptMulti(key, data);
|
||||
} catch (e) {
|
||||
const fallbackKey = await Sodium.deriveKeyFallback?.(
|
||||
password.password,
|
||||
password.salt as string
|
||||
);
|
||||
if (Platform.OS === "ios" && fallbackKey) {
|
||||
DatabaseLogger.info("Using fallback key for decryption");
|
||||
}
|
||||
if (fallbackKey) {
|
||||
return await Sodium.decryptMulti(fallbackKey, data);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg) {
|
||||
export function parseAlgorithm(alg: string) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
return {
|
||||
@@ -346,16 +444,11 @@ export function parseAlgorithm(alg) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function encrypt(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let message = {
|
||||
export async function encrypt(password: SerializedKey, plainText: string) {
|
||||
const result = await Sodium.encrypt<"base64">(password, {
|
||||
type: "plain",
|
||||
data: data
|
||||
};
|
||||
let result = await Sodium.encrypt(password, message);
|
||||
data: plainText
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
@@ -363,14 +456,13 @@ export async function encrypt(password, data) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let results = await Sodium.encryptMulti(
|
||||
export async function encryptMulti(
|
||||
password: SerializedKey,
|
||||
plainText: string[]
|
||||
) {
|
||||
const results = await Sodium.encryptMulti<"base64">(
|
||||
password,
|
||||
data.map((item) => ({
|
||||
plainText.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
@@ -16,26 +16,26 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { logger as dbLogger, ICompressor } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { FileStorage } from "../filesystem";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import "./logger";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { Storage } from "./storage";
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
export async function setupDatabase(password?: string) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key) throw new Error(strings.databaseSetupFailed());
|
||||
|
||||
@@ -47,17 +47,21 @@ export async function setupDatabase(password) {
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
MONOGRAPH_HOST: "https://monogr.ph",
|
||||
...(SettingsService.getProperty("serverUrls") || {})
|
||||
});
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: () => ({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}),
|
||||
eventsource: (Platform.OS === "ios"
|
||||
? EventSource
|
||||
: AndroidEventSource) as any,
|
||||
fs: FileStorage,
|
||||
compressor: async () =>
|
||||
({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
} as ICompressor),
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
@@ -26,7 +26,7 @@ import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
type Config = { dbName: string; async: boolean; location?: string };
|
||||
|
||||
export class RNSqliteDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
|
||||
@@ -16,58 +16,52 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { IStorage } from "@notesnook/core";
|
||||
import { MMKVInstance } from "react-native-mmkv-storage";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
deriveCryptoKeyFallback,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey
|
||||
generateCryptoKeyFallback
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
export class KV {
|
||||
/**
|
||||
* @type {typeof MMKV}
|
||||
*/
|
||||
storage = null;
|
||||
constructor(storage) {
|
||||
storage: MMKVInstance;
|
||||
constructor(storage: MMKVInstance) {
|
||||
this.storage = storage;
|
||||
}
|
||||
async read(key) {
|
||||
if (!key) return null;
|
||||
let data = this.storage.getString(key);
|
||||
if (!data) return null;
|
||||
|
||||
async read<T>(key: string, isArray?: boolean) {
|
||||
if (!key) return undefined;
|
||||
const data = this.storage.getString(key);
|
||||
if (!data) return undefined;
|
||||
try {
|
||||
let parse = JSON.parse(data);
|
||||
return parse;
|
||||
return JSON.parse(data) as T;
|
||||
} catch (e) {
|
||||
return data;
|
||||
return data as T;
|
||||
}
|
||||
}
|
||||
|
||||
async write(key, data) {
|
||||
async write<T>(key: string, data: T) {
|
||||
this.storage.setString(
|
||||
key,
|
||||
typeof data === "string" ? data : JSON.stringify(data)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async readMulti(keys) {
|
||||
async readMulti<T>(keys: string[]) {
|
||||
if (keys.length <= 0) {
|
||||
return [];
|
||||
} else {
|
||||
try {
|
||||
let data = await this.storage.getMultipleItemsAsync(
|
||||
const data = await this.storage.getMultipleItemsAsync<any>(
|
||||
keys.slice(),
|
||||
"string"
|
||||
);
|
||||
@@ -79,24 +73,24 @@ export class KV {
|
||||
obj = value;
|
||||
}
|
||||
return [key, obj];
|
||||
});
|
||||
}) as [string, T][];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
return this.storage.removeItem(key);
|
||||
async remove(key: string) {
|
||||
this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async removeMulti(keys) {
|
||||
if (!keys) return true;
|
||||
return this.storage.removeItems(keys);
|
||||
async removeMulti(keys: string[]) {
|
||||
if (!keys) return;
|
||||
this.storage.removeItems(keys);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
return this.storage.clearStore();
|
||||
this.storage.clearStore();
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
@@ -113,54 +107,46 @@ export class KV {
|
||||
return keys;
|
||||
}
|
||||
|
||||
async writeMulti(items) {
|
||||
return this.storage.setMultipleItemsAsync(items, "object");
|
||||
async writeMulti(items: [string, any][]) {
|
||||
await this.storage.setMultipleItemsAsync(items, "object");
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultStorage = new KV(MMKV);
|
||||
|
||||
async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
async function checkAndCreateDir(path) {
|
||||
let dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
let exists = await RNFetchBlob.fs.exists(dir);
|
||||
let isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
export default {
|
||||
read: (key) => DefaultStorage.read(key),
|
||||
write: (key, value) => DefaultStorage.write(key, value),
|
||||
readMulti: (keys) => DefaultStorage.readMulti(keys),
|
||||
remove: (key) => DefaultStorage.remove(key),
|
||||
clear: () => DefaultStorage.clear(),
|
||||
getAllKeys: () => DefaultStorage.getAllKeys(),
|
||||
writeMulti: (items) => DefaultStorage.writeMulti(items),
|
||||
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
|
||||
export const Storage: IStorage = {
|
||||
write<T>(key: string, data: T): Promise<void> {
|
||||
return DefaultStorage.write(key, data);
|
||||
},
|
||||
writeMulti<T>(entries: [string, T][]): Promise<void> {
|
||||
return DefaultStorage.writeMulti(entries);
|
||||
},
|
||||
readMulti<T>(keys: string[]): Promise<[string, T][]> {
|
||||
return DefaultStorage.readMulti(keys);
|
||||
},
|
||||
read<T>(key: string, isArray?: boolean): Promise<T | undefined> {
|
||||
return DefaultStorage.read(key, isArray);
|
||||
},
|
||||
remove(key: string): Promise<void> {
|
||||
return DefaultStorage.remove(key);
|
||||
},
|
||||
removeMulti(keys: string[]): Promise<void> {
|
||||
return DefaultStorage.removeMulti(keys);
|
||||
},
|
||||
clear(): Promise<void> {
|
||||
return DefaultStorage.clear();
|
||||
},
|
||||
getAllKeys(): Promise<string[]> {
|
||||
return DefaultStorage.getAllKeys();
|
||||
},
|
||||
hash,
|
||||
getCryptoKey,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
getRandomBytes,
|
||||
checkAndCreateDir,
|
||||
requestPermission,
|
||||
deriveCryptoKey,
|
||||
getCryptoKey,
|
||||
removeCryptoKey,
|
||||
hash,
|
||||
generateCryptoKey,
|
||||
encryptMulti
|
||||
generateCryptoKeyFallback,
|
||||
deriveCryptoKeyFallback
|
||||
};
|
||||
@@ -16,7 +16,6 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { getFileNameWithExtension } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -25,12 +24,12 @@ import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import filesystem from ".";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
@@ -51,23 +50,23 @@ export async function downloadAllAttachments() {
|
||||
/**
|
||||
* Downloads provided attachments to a .zip file
|
||||
* on user's device.
|
||||
* @param {string[]} attachments
|
||||
* @param {string[]} attachmentIds
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(attachments) {
|
||||
export async function downloadAttachments(attachmentIds: string[]) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
if (!attachmentIds || !attachmentIds.length) return;
|
||||
const groupId = `download-all-${Date.now()}`;
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
// Ask the user to select a directory to store the file
|
||||
let file = await ScopedStorage.openDocumentTree(true);
|
||||
const file = await ScopedStorage.openDocumentTree(true);
|
||||
outputFolder = file.uri;
|
||||
if (!outputFolder) return;
|
||||
} else {
|
||||
outputFolder = await Storage.checkAndCreateDir("/downloads/");
|
||||
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
|
||||
}
|
||||
|
||||
// Create the folder to zip;
|
||||
@@ -83,7 +82,7 @@ export async function downloadAttachments(attachments) {
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
const isCancelled = () => {
|
||||
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
|
||||
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
@@ -97,19 +96,21 @@ export async function downloadAttachments(attachments) {
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
for (let i = 0; i < attachmentIds.length; i++) {
|
||||
if (isCancelled()) return;
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const attachment = await db.attachments.attachment(attachmentIds[i]);
|
||||
if (!attachment) continue;
|
||||
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: i + 1,
|
||||
total: attachments.length,
|
||||
total: attachmentIds.length,
|
||||
filename: attachment.hash
|
||||
});
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
@@ -184,7 +185,7 @@ export async function downloadAttachments(attachments) {
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
ToastManager.error(e as Error, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
@@ -194,38 +195,44 @@ 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,
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 0,
|
||||
total: 1,
|
||||
filename: attachment.filename
|
||||
@@ -234,13 +241,13 @@ export default async function downloadAttachment(
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options.groupId || attachment.hash,
|
||||
options?.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options.groupId || attachment.hash,
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 1,
|
||||
total: 1,
|
||||
filename: attachment.filename,
|
||||
@@ -252,7 +259,7 @@ export default async function downloadAttachment(
|
||||
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,
|
||||
@@ -260,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,
|
||||
@@ -275,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),
|
||||
@@ -294,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} />
|
||||
});
|
||||
}
|
||||
@@ -319,7 +328,7 @@ export default async function downloadAttachment(
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: options.groupId || attachment.hash,
|
||||
groupId: options?.groupId || attachment.hash,
|
||||
current: 0,
|
||||
total: 0,
|
||||
filename: attachment.filename,
|
||||
@@ -327,7 +336,7 @@ export default async function downloadAttachment(
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options.throwError) {
|
||||
if (options?.throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RequestOptions } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
@@ -26,7 +27,13 @@ import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
export async function downloadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
@@ -36,9 +43,11 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const { url, headers, chunkSize } = requestOptions;
|
||||
const tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
const originalFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
||||
@@ -46,6 +55,8 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
@@ -71,21 +82,21 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
const resolveUrlResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (!resolveUrlResponse.ok) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
|
||||
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error(
|
||||
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
);
|
||||
}
|
||||
|
||||
const downloadUrl = await res.text();
|
||||
const downloadUrl = await resolveUrlResponse.text();
|
||||
|
||||
if (!downloadUrl) {
|
||||
DatabaseLogger.log(
|
||||
@@ -95,12 +106,12 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
let request = RNFetchBlob.config({
|
||||
const request = RNFetchBlob.config({
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true,
|
||||
overwrite: true
|
||||
})
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.fetch("GET", downloadUrl)
|
||||
.progress(async (recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
@@ -109,15 +120,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
cancelToken.cancel = async (reason) => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
|
||||
};
|
||||
|
||||
let response = await request;
|
||||
console.log(response.info().headers);
|
||||
const response = await request;
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
@@ -128,10 +138,10 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
let status = response.info().status;
|
||||
const status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (exists(originalFilePath)) {
|
||||
if (await exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
}
|
||||
|
||||
@@ -143,11 +153,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
if (
|
||||
(e as Error).message !== "canceled" &&
|
||||
!(e as Error).message.includes("NoSuchKey")
|
||||
) {
|
||||
const toast = {
|
||||
heading: strings.downloadError(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
heading: strings.downloadError((e as Error).message),
|
||||
message: (e as Error).message,
|
||||
type: "error" as const,
|
||||
context: "global"
|
||||
};
|
||||
ToastManager.show(toast);
|
||||
@@ -158,15 +171,14 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
DatabaseLogger.error(e, "Download failed: ", {
|
||||
url
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
export async function checkAttachment(hash: string) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
@@ -184,7 +196,7 @@ export async function checkAttachment(hash) {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
} catch (e) {
|
||||
return { failed: e?.message };
|
||||
return { failed: (e as Error)?.message };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
@@ -17,24 +17,41 @@ 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,
|
||||
getUploadedFileSize
|
||||
};
|
||||
|
||||
export default {
|
||||
export const FileStorage: IFileStorage = {
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
@@ -44,10 +61,5 @@ export default {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
bulkExists
|
||||
};
|
||||
@@ -18,6 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
FileEncryptionMetadataWithHash,
|
||||
FileEncryptionMetadataWithOutputType,
|
||||
Output,
|
||||
RequestOptions
|
||||
} from "@notesnook/core";
|
||||
import { DataFormat, SerializedKey } from "@notesnook/crypto";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
@@ -25,17 +32,21 @@ import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
export async function readEncrypted<TOutputFormat extends DataFormat>(
|
||||
filename: string,
|
||||
key: SerializedKey,
|
||||
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
|
||||
) {
|
||||
await migrateFilesFromCache();
|
||||
DatabaseLogger.log("Read encrypted file...");
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (!(await exists(filename))) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
const output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
...cipherData,
|
||||
@@ -47,15 +58,14 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
|
||||
DatabaseLogger.log("File decrypted...");
|
||||
|
||||
return output;
|
||||
return output as Output<TOutputFormat>;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
DatabaseLogger.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashBase64(data) {
|
||||
export async function hashBase64(data: string) {
|
||||
const hash = await Sodium.hashFile({
|
||||
type: "base64",
|
||||
data,
|
||||
@@ -67,61 +77,70 @@ export async function hashBase64(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64(data, key) {
|
||||
export async function writeEncryptedBase64(
|
||||
data: string,
|
||||
encryptionKey: SerializedKey,
|
||||
mimeType: string
|
||||
): Promise<FileEncryptionMetadataWithHash> {
|
||||
await createCacheDir();
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
const output = await Sodium.encryptFile(encryptionKey, {
|
||||
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
|
||||
type: "url"
|
||||
});
|
||||
|
||||
RNFetchBlob.fs.unlink(filepath).catch(console.log);
|
||||
console.log("encrypted file output: ", output);
|
||||
output.size = output.length;
|
||||
delete output.length;
|
||||
|
||||
return {
|
||||
...output,
|
||||
alg: "xcha-stream"
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteFile(filename, data) {
|
||||
export async function deleteFile(
|
||||
filename: string,
|
||||
requestOptions?: RequestOptions
|
||||
): Promise<boolean> {
|
||||
await createCacheDir();
|
||||
let delFilePath = cacheDir + `/${filename}`;
|
||||
if (!data) {
|
||||
if (!filename) return;
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
const localFilePath = cacheDir + `/${filename}`;
|
||||
if (!requestOptions) {
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
|
||||
return true;
|
||||
}
|
||||
|
||||
let { url, headers } = data;
|
||||
const { url, headers } = requestOptions;
|
||||
|
||||
try {
|
||||
let response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
let status = response.info().status;
|
||||
let ok = status >= 200 && status < 300;
|
||||
const response = await RNFetchBlob.fetch("DELETE", url, headers);
|
||||
const status = response.info().status;
|
||||
const ok = status >= 200 && status < 300;
|
||||
if (ok) {
|
||||
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
|
||||
}
|
||||
return ok;
|
||||
} catch (e) {
|
||||
console.log("delete file: ", e, url, headers);
|
||||
DatabaseLogger.error(e, "Delete file", {
|
||||
url: url
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (let file of files) {
|
||||
for (const file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
|
||||
}
|
||||
for (let file of oldCache) {
|
||||
for (const file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("clearFileStorage", e);
|
||||
DatabaseLogger.error(e, "clearFileStorage");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,11 +165,11 @@ export async function migrateFilesFromCache() {
|
||||
return;
|
||||
}
|
||||
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
console.log("Files to migrate:", files.join(","));
|
||||
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (let file of oldCache) {
|
||||
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (const file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
@@ -159,7 +178,7 @@ export async function migrateFilesFromCache() {
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
console.log("migrateFilesFromCache", e);
|
||||
DatabaseLogger.error(e, "migrateFilesFromCache");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,14 +188,14 @@ export async function clearCache() {
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
export async function deleteCacheFileByPath(path: string) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name) {
|
||||
export async function deleteCacheFileByName(name: string) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
@@ -192,12 +211,12 @@ export async function deleteDCacheFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
export async function exists(filename: string) {
|
||||
const path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
@@ -211,6 +230,7 @@ export async function exists(filename) {
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.size + totalAbytes;
|
||||
@@ -234,7 +254,7 @@ export async function exists(filename) {
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files) {
|
||||
export async function bulkExists(files: string[]) {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
@@ -243,7 +263,7 @@ export async function bulkExists(files) {
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter(
|
||||
@@ -262,8 +282,8 @@ export async function getCacheSize() {
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
stat.forEach((file) => {
|
||||
total += parseInt(file.size as unknown as string);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -24,11 +25,22 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
|
||||
import {
|
||||
cacheDir,
|
||||
checkUpload,
|
||||
FileSizeResult,
|
||||
getUploadedFileSize
|
||||
} from "./utils";
|
||||
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
export async function uploadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
const { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
@@ -39,7 +51,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
filePath = appGroupPath;
|
||||
@@ -54,14 +66,15 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
|
||||
|
||||
let remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === -1) return false;
|
||||
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
|
||||
const remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === FileSizeResult.Error) return false;
|
||||
|
||||
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let uploadUrlResponse = await fetch(url, {
|
||||
const uploadUrlResponse = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
@@ -78,7 +91,8 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
let uploadRequest = RNFetchBlob.config({
|
||||
const uploadRequest = RNFetchBlob.config({
|
||||
//@ts-ignore
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
})
|
||||
.fetch(
|
||||
@@ -98,14 +112,14 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
cancelToken.cancel = async () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
uploadRequest.cancel();
|
||||
};
|
||||
|
||||
let uploadResponse = await uploadRequest;
|
||||
let status = uploadResponse.info().status;
|
||||
let uploaded = status >= 200 && status < 300;
|
||||
const uploadResponse = await uploadRequest;
|
||||
const status = uploadResponse.info().status;
|
||||
const uploaded = status >= 200 && status < 300;
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
@@ -118,12 +132,13 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
);
|
||||
}
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return false;
|
||||
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
|
||||
return uploaded;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
ToastManager.error(e, "File upload failed");
|
||||
ToastManager.error(e as Error, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { hosts, RequestOptions } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { hosts } from "@notesnook/core";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
@@ -31,18 +31,13 @@ export const cacheDir =
|
||||
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
|
||||
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
|
||||
|
||||
export function getRandomId(prefix) {
|
||||
export function getRandomId(prefix: string) {
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
export function parseS3Error(data?: string) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
|
||||
const error = {
|
||||
@@ -70,11 +65,19 @@ export function parseS3Error(data) {
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelable(operation) {
|
||||
export function cancelable(
|
||||
operation: (
|
||||
filename: string,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: {
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
}
|
||||
) => Promise<boolean>
|
||||
) {
|
||||
const cancelToken = {
|
||||
cancel: () => {}
|
||||
cancel: async (reason?: string) => {}
|
||||
};
|
||||
return (filename, requestOptions) => {
|
||||
return (filename: string, requestOptions: RequestOptions) => {
|
||||
return {
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
cancel: async () => {
|
||||
@@ -84,29 +87,35 @@ export function cancelable(operation) {
|
||||
};
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
export function copyFileAsync(source: string, dest: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
//@ts-ignore
|
||||
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function releasePermissions(path) {
|
||||
export async function releasePermissions(path: string) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (let uri of uris) {
|
||||
for (const uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
export const FileSizeResult = {
|
||||
Empty: 0,
|
||||
Error: -1
|
||||
};
|
||||
|
||||
export async function getUploadedFileSize(hash: string) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
@@ -115,16 +124,20 @@ export async function getUploadedFileSize(hash) {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
attachmentInfo.headers?.get("content-length") || "0"
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return -1;
|
||||
return FileSizeResult.Error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUpload(filename, chunkSize, expectedSize) {
|
||||
export async function checkUpload(
|
||||
filename: string,
|
||||
chunkSize: number,
|
||||
expectedSize: number
|
||||
) {
|
||||
const size = await getUploadedFileSize(filename);
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
@@ -138,3 +151,25 @@ export async function checkUpload(filename, chunkSize, expectedSize) {
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
export async function requestPermission() {
|
||||
if (Platform.OS === "ios") return true;
|
||||
return true;
|
||||
}
|
||||
export async function checkAndCreateDir(path: string) {
|
||||
const dir =
|
||||
Platform.OS === "ios"
|
||||
? RNFetchBlob.fs.dirs.DocumentDir + path
|
||||
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
|
||||
|
||||
try {
|
||||
const exists = await RNFetchBlob.fs.exists(dir);
|
||||
const isDir = await RNFetchBlob.fs.isDir(dir);
|
||||
if (!exists || !isDir) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
} catch (e) {
|
||||
await RNFetchBlob.fs.mkdir(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -38,7 +38,7 @@ import { useAppState } from "../../hooks/use-app-state";
|
||||
|
||||
export interface BaseDialogProps extends PropsWithChildren {
|
||||
animation?: "fade" | "none" | "slide" | undefined;
|
||||
visible: boolean;
|
||||
visible?: boolean;
|
||||
onRequestClose?: () => void;
|
||||
onShow?: () => void;
|
||||
premium?: boolean;
|
||||
|
||||
@@ -179,6 +179,7 @@ const ColorPicker = ({
|
||||
title: title.current,
|
||||
colorCode: selectedColor
|
||||
});
|
||||
if (!id) return;
|
||||
useRelationStore.getState().update();
|
||||
useMenuStore.getState().setColorNotes();
|
||||
setVisible(false);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -297,7 +297,6 @@ function getDate(item: Item, groupType?: GroupingKey): number {
|
||||
groupType
|
||||
? db.settings.getGroupOptions(groupType)
|
||||
: {
|
||||
groupBy: "default",
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
},
|
||||
|
||||
@@ -129,7 +129,10 @@ export const PricingPlans = ({
|
||||
if (code.startsWith("com.streetwriters.notesnook")) {
|
||||
skuId = code;
|
||||
} else {
|
||||
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
|
||||
skuId = await db.offers?.getCode(
|
||||
code.split(":")[0],
|
||||
Platform.OS as "ios" | "android"
|
||||
);
|
||||
}
|
||||
|
||||
const products = await PremiumService.getProducts();
|
||||
|
||||
@@ -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;
|
||||
}}
|
||||
|
||||
@@ -150,7 +150,7 @@ const PublishNoteSheet = ({
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{isPublished && (
|
||||
{isPublished && publishUrl ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -204,7 +204,7 @@ const PublishNoteSheet = ({
|
||||
name="content-copy"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -556,9 +556,9 @@ export default function ReminderSheet({
|
||||
{Object.keys(ReminderNotificationModes).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
title={strings.reminderNotificationModes[
|
||||
title={strings.reminderNotificationModes(
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
]()}
|
||||
)}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
borderRadius: 100
|
||||
|
||||
@@ -1,158 +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 React from "react";
|
||||
import { ActivityIndicator, DimensionValue, ViewStyle } from "react-native";
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
Layout,
|
||||
LightSpeedInLeft
|
||||
} from "react-native-reanimated";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import NativeTooltip from "../../../utils/tooltip";
|
||||
import { ButtonProps } from "../button";
|
||||
import { Pressable, useButton } from "../pressable";
|
||||
import Heading from "../typography/heading";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
const AnimatedIcon = Animated.createAnimatedComponent(Icon);
|
||||
|
||||
export const AnimatedButton = ({
|
||||
height = 45,
|
||||
width = null,
|
||||
onPress,
|
||||
loading = false,
|
||||
title = null,
|
||||
icon,
|
||||
fontSize = SIZE.sm,
|
||||
type = "transparent",
|
||||
iconSize = SIZE.md,
|
||||
style = {},
|
||||
accentColor = "accent",
|
||||
accentText = "light",
|
||||
onLongPress,
|
||||
tooltipText,
|
||||
textStyle,
|
||||
iconPosition = "left",
|
||||
buttonType,
|
||||
bold,
|
||||
iconColor,
|
||||
fwdRef,
|
||||
...restProps
|
||||
}: ButtonProps) => {
|
||||
const { text } = useButton({
|
||||
type,
|
||||
accent: accentColor,
|
||||
text: accentText
|
||||
});
|
||||
const textColor = buttonType?.text ? buttonType.text : text;
|
||||
const Component = bold ? Heading : Paragraph;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeIn}
|
||||
exiting={FadeOut}
|
||||
layout={Layout.springify()}
|
||||
>
|
||||
<Pressable
|
||||
{...restProps}
|
||||
fwdRef={fwdRef}
|
||||
onPress={onPress}
|
||||
onLongPress={(event) => {
|
||||
if (onLongPress) {
|
||||
onLongPress(event);
|
||||
return;
|
||||
}
|
||||
if (tooltipText) {
|
||||
NativeTooltip.show(event, tooltipText, NativeTooltip.POSITIONS.TOP);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
type={type}
|
||||
accentColor={accentColor}
|
||||
accentText={accentText}
|
||||
customColor={buttonType?.color}
|
||||
customSelectedColor={buttonType?.selected}
|
||||
customOpacity={buttonType?.opacity}
|
||||
customAlpha={buttonType?.alpha}
|
||||
style={{
|
||||
height: height,
|
||||
width: (width as DimensionValue) || undefined,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 5,
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
...(style as ViewStyle)
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={textColor} size={fontSize + 4} />
|
||||
) : null}
|
||||
{icon && !loading && iconPosition === "left" ? (
|
||||
<AnimatedIcon
|
||||
exiting={FadeOut.duration(100)}
|
||||
entering={LightSpeedInLeft}
|
||||
layout={Layout.springify()}
|
||||
name={icon}
|
||||
style={{
|
||||
marginRight: 0
|
||||
}}
|
||||
color={iconColor || buttonType?.text || textColor}
|
||||
size={iconSize}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!title ? null : (
|
||||
<Component
|
||||
layout={Layout.springify()}
|
||||
animated={true}
|
||||
color={textColor as string}
|
||||
size={fontSize}
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
{
|
||||
marginLeft:
|
||||
icon || (loading && iconPosition === "left") ? 5 : 0,
|
||||
marginRight:
|
||||
icon || (loading && iconPosition === "right") ? 5 : 0
|
||||
},
|
||||
textStyle
|
||||
]}
|
||||
>
|
||||
{title}
|
||||
</Component>
|
||||
)}
|
||||
|
||||
{icon && !loading && iconPosition === "right" ? (
|
||||
<Icon
|
||||
name={icon}
|
||||
style={{
|
||||
marginLeft: 0
|
||||
}}
|
||||
color={iconColor || buttonType?.text || textColor}
|
||||
size={iconSize}
|
||||
/>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
@@ -18,15 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { DimensionValue, View } from "react-native";
|
||||
import { SvgXml } from "./lazy";
|
||||
export const SvgView = ({
|
||||
width = 250,
|
||||
height = 250,
|
||||
src
|
||||
}: {
|
||||
width?: number | string;
|
||||
height?: number | number;
|
||||
width?: DimensionValue;
|
||||
height?: DimensionValue;
|
||||
src?: string;
|
||||
}) => {
|
||||
if (!src) return null;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -32,18 +32,11 @@ export function useAppState() {
|
||||
const subscription = AppState.addEventListener("change", onChange);
|
||||
|
||||
return () => {
|
||||
// @ts-expect-error - React Native >= 0.65
|
||||
if (typeof subscription?.remove === "function") {
|
||||
// @ts-expect-error - need update @types/react-native@0.65.x
|
||||
subscription.remove();
|
||||
} else {
|
||||
// React Native < 0.65
|
||||
AppState.removeEventListener("change", onChange);
|
||||
}
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return appState;
|
||||
}
|
||||
|
||||
export { AppStateStatus };
|
||||
export type { AppStateStatus };
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Keyboard, KeyboardEventListener, ScreenRect } from "react-native";
|
||||
import { Keyboard, KeyboardEventListener, KeyboardMetrics } from "react-native";
|
||||
|
||||
const emptyCoordinates = Object.freeze({
|
||||
screenX: 0,
|
||||
@@ -34,8 +34,8 @@ const initialValue = {
|
||||
export default function useKeyboard() {
|
||||
const [shown, setShown] = useState(false);
|
||||
const [coordinates, setCoordinates] = useState<{
|
||||
start: undefined | ScreenRect;
|
||||
end: ScreenRect;
|
||||
start: undefined | KeyboardMetrics;
|
||||
end: KeyboardMetrics;
|
||||
}>(initialValue);
|
||||
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -91,7 +91,6 @@ export const Reminders = ({
|
||||
/>
|
||||
|
||||
<FloatingButton
|
||||
title={strings.setReminder()}
|
||||
onPress={() => {
|
||||
ReminderSheet.present();
|
||||
}}
|
||||
|
||||
@@ -59,15 +59,38 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
const type =
|
||||
route.params.type === "trash"
|
||||
? "trash"
|
||||
: ((route.params?.type + "s") as keyof typeof db.lookup);
|
||||
console.log(`Searching in ${type} for ${query}`);
|
||||
const results = await db.lookup[type](
|
||||
query,
|
||||
route.params.items as FilteredSelector<Note>
|
||||
).sorted();
|
||||
let results: VirtualizedGrouping<Item> | undefined;
|
||||
|
||||
switch (route.params.type) {
|
||||
case "note":
|
||||
results = await db.lookup
|
||||
.notes(query, route.params.items as FilteredSelector<Note>)
|
||||
.sorted();
|
||||
break;
|
||||
case "notebook":
|
||||
results = await db.lookup.notebooks(query).sorted();
|
||||
break;
|
||||
case "tag":
|
||||
results = await db.lookup.tags(query).sorted();
|
||||
break;
|
||||
case "reminder":
|
||||
results = await db.lookup.reminders(query).sorted();
|
||||
break;
|
||||
case "trash":
|
||||
results = await db.lookup.trash(query).sorted();
|
||||
break;
|
||||
case "attachment":
|
||||
results = await db.lookup.attachments(query).sorted();
|
||||
break;
|
||||
default:
|
||||
results = undefined;
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
setSearchStatus(strings.noResultsFound(query));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Found ${results.placeholders?.length} results for ${query}`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,24 +17,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { LogMessage } from "@notesnook/logger";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { format, LogLevel, logManager } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { LogMessage } from "@notesnook/logger";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import Storage from "../../common/database/storage";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import useTimer from "../../hooks/use-timer";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { hexToRGBA } from "../../utils/colors";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export default function DebugLogs() {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -148,7 +148,7 @@ export default function DebugLogs() {
|
||||
if (!file) return;
|
||||
path = file.uri;
|
||||
} else {
|
||||
path = await Storage.checkAndCreateDir("/");
|
||||
path = await filesystem.checkAndCreateDir("/");
|
||||
await RNFetchBlob.fs.writeFile(path + fileName + ".txt", data, "utf8");
|
||||
path = path + fileName;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const Home = ({
|
||||
});
|
||||
|
||||
const renderItem = ({ item }: { item: SettingSection; index: number }) =>
|
||||
item.name === "account" ? (
|
||||
item.id === "account" ? (
|
||||
<SettingsUserSection item={item} />
|
||||
) : (
|
||||
<SectionGroup item={item} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -1174,7 +1194,12 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "select-backup-dir",
|
||||
name: strings.selectBackupDir(),
|
||||
description: strings.selectBackupDirDesc(),
|
||||
description: () => {
|
||||
const desc = strings.selectBackupDirDesc(
|
||||
SettingsService.get().backupDirectoryAndroid?.path || ""
|
||||
);
|
||||
return desc[0] + " " + desc[1];
|
||||
},
|
||||
icon: "folder",
|
||||
hidden: () =>
|
||||
!!SettingsService.get().backupDirectoryAndroid ||
|
||||
|
||||
@@ -126,18 +126,19 @@ export const Subscription = () => {
|
||||
user.subscription?.type !== SUBSCRIPTION_STATUS.PREMIUM_EXPIRED &&
|
||||
user.subscription?.type !== SUBSCRIPTION_STATUS.BASIC ? (
|
||||
<Button
|
||||
title={subscriptionProviderInfo?.title}
|
||||
title={subscriptionProviderInfo?.title()}
|
||||
onPress={() => {
|
||||
presentSheet({
|
||||
title: subscriptionProviderInfo.title,
|
||||
paragraph: subscriptionProviderInfo.desc
|
||||
title: subscriptionProviderInfo.title(),
|
||||
paragraph: subscriptionProviderInfo.desc()
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
borderRadius: 100
|
||||
width: "100%",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
fontSize={SIZE.sm}
|
||||
fontSize={SIZE.xs}
|
||||
height={30}
|
||||
type="secondaryAccented"
|
||||
/>
|
||||
|
||||
@@ -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") &&
|
||||
|
||||
@@ -23,7 +23,7 @@ import FingerprintScanner, {
|
||||
} 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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -116,7 +116,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3033
|
||||
versionCode 3035
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -129,6 +129,16 @@
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Make Note">
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Make Note">
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
- Added full support for localization in Notesnook
|
||||
- Improved search experience
|
||||
- Allow user to cancel logging in
|
||||
- Fixed scrolling focused line into view
|
||||
- Support self hosted monograph server
|
||||
- Fixed markdown link pasting in editor
|
||||
- Many other bug fixes and improvements
|
||||
- You can now share multiple files to Notesnook
|
||||
- Fix file and image sharing not working
|
||||
- Many other bug fixes and small improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
Binary file not shown.
@@ -5,11 +5,11 @@
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>org.streetwriters.notesnook</key>
|
||||
<string>Notesnook App Distribution 2024</string>
|
||||
<string>Notesnook App Distribution 2025</string>
|
||||
<key>org.streetwriters.notesnook.notewidget</key>
|
||||
<string>Notesnook Widget Distribution 2024</string>
|
||||
<string>Notesnook Widget Distribution 2025</string>
|
||||
<key>org.streetwriters.notesnook.share</key>
|
||||
<string>Notesnook Share Distribution 2024</string>
|
||||
<string>Notesnook Share Distribution 2025</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1063,7 +1063,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2120;
|
||||
CURRENT_PROJECT_VERSION = 2122;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1137,7 +1137,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.23;
|
||||
MARKETING_VERSION = 3.0.25;
|
||||
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 = 2120;
|
||||
CURRENT_PROJECT_VERSION = 2122;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1242,7 +1242,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.23;
|
||||
MARKETING_VERSION = 3.0.25;
|
||||
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 = 2120;
|
||||
CURRENT_PROJECT_VERSION = 2122;
|
||||
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.23;
|
||||
MARKETING_VERSION = 3.0.25;
|
||||
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 = 2120;
|
||||
CURRENT_PROJECT_VERSION = 2122;
|
||||
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.23;
|
||||
MARKETING_VERSION = 3.0.25;
|
||||
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 = 2120;
|
||||
CURRENT_PROJECT_VERSION = 2122;
|
||||
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.23;
|
||||
MARKETING_VERSION = 3.0.25;
|
||||
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 = 2120;
|
||||
CURRENT_PROJECT_VERSION = 2122;
|
||||
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.23;
|
||||
MARKETING_VERSION = 3.0.25;
|
||||
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;
|
||||
@@ -1744,10 +1744,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
@@ -1811,10 +1808,7 @@
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
||||
@@ -1019,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
|
||||
@@ -1858,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
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"react-native-screenguard": "^1.0.0",
|
||||
"@formatjs/intl-locale": "4.0.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.14",
|
||||
"@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-exit-app": "github:ammarahm-ed/react-native-exit-app",
|
||||
@@ -73,7 +73,7 @@
|
||||
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",
|
||||
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
|
||||
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
|
||||
"@ammarahmed/react-native-share-extension": "^2.7.0"
|
||||
"@ammarahmed/react-native-share-extension": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"detox": "^20.27.6",
|
||||
@@ -88,7 +88,6 @@
|
||||
"@tsconfig/react-native": "^3.0.2",
|
||||
"@types/html-to-text": "^8.0.1",
|
||||
"@types/metro-config": "^0.76.3",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-native": "^0.69.1",
|
||||
"@types/react-native-vector-icons": "^6.4.10",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
|
||||
195
apps/mobile/package-lock.json
generated
195
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.23",
|
||||
"version": "3.0.24",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.23",
|
||||
"version": "3.0.24",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -14,9 +14,9 @@
|
||||
"app/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ammarahmed/react-native-share-extension": "^2.7.0",
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@notesnook/editor": "file:../../packages/editor",
|
||||
"@notesnook/editor-mobile": "file:../../packages/editor-mobile",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
@@ -27,11 +27,13 @@
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/react-query": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@types/validator": "^13.12.2",
|
||||
"diffblazer": "^1.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.74.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.39",
|
||||
"fonteditor-core": "^2.1.11",
|
||||
"listr": "^0.14.3",
|
||||
"otplib": "12.0.1",
|
||||
@@ -24352,7 +24354,6 @@
|
||||
"../../packages/sodium": {
|
||||
"name": "@notesnook/sodium",
|
||||
"version": "2.1.3",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -24971,12 +24972,10 @@
|
||||
},
|
||||
"../../packages/sodium/node_modules/libsodium-sumo": {
|
||||
"version": "0.7.15",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"../../packages/sodium/node_modules/libsodium-wrappers-sumo": {
|
||||
"version": "0.7.15",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"libsodium-sumo": "^0.7.15"
|
||||
@@ -28934,7 +28933,8 @@
|
||||
"@ammarahmed/react-native-background-fetch": "^4.2.2",
|
||||
"@ammarahmed/react-native-eventsource": "1.1.0",
|
||||
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
|
||||
"@ammarahmed/react-native-sodium": "1.5.6",
|
||||
"@ammarahmed/react-native-share-extension": "^2.8.0",
|
||||
"@ammarahmed/react-native-sodium": "^1.6.1",
|
||||
"@bam.tech/react-native-image-resizer": "3.0.5",
|
||||
"@callstack/repack": "^4.1.1",
|
||||
"@formatjs/intl-locale": "4.0.0",
|
||||
@@ -29009,7 +29009,6 @@
|
||||
"@types/html-to-text": "^8.0.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/metro-config": "^0.76.3",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-native": "^0.69.1",
|
||||
"@types/react-native-vector-icons": "^6.4.10",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
@@ -29072,17 +29071,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ammarahmed/react-native-share-extension": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.7.0.tgz",
|
||||
"integrity": "sha512-M7mNKv8k+j3SkD0NcRAOItoF7lMa61OYnTD7JOAZiBRHWkZDysc/2u6dbQGDcd6VMRif+xWRrhLILH+zFHkgPg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.8.0.tgz",
|
||||
"integrity": "sha512-eDFVKiJJxjnIORveMUKVJGIhKfRNjqJWIoKS+JM88q8I8e3jHys9moyJAr9QM5oW/tZx4yyItl08yRXGnr7Grg==",
|
||||
"dependencies": {
|
||||
"react-native": "^0.63.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ammarahmed/react-native-sodium": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-sodium/-/react-native-sodium-1.5.6.tgz",
|
||||
"integrity": "sha512-DASF/A/cDViTMRnCxvoM35F0v/l/cD8bpecfI+oNOCUmySqWqR37h1L4tdFfEzwnJxLcm/SAyyDSxaa+qgw7BQ=="
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-sodium/-/react-native-sodium-1.6.1.tgz",
|
||||
"integrity": "sha512-3qSTIPCEYN8DChHqYuv94ekgtyLF6dinH13JZXdxsj6DHRcZxu9syPrFNb8osItPNx3ncME6UGhPFnSL2eDI8g=="
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.2.1",
|
||||
@@ -32854,6 +32853,10 @@
|
||||
"resolved": "../../packages/core",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@notesnook/crypto": {
|
||||
"resolved": "../../packages/crypto",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@notesnook/editor": {
|
||||
"resolved": "../../packages/editor",
|
||||
"link": true
|
||||
@@ -34353,12 +34356,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.13",
|
||||
"version": "18.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
@@ -34395,11 +34399,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.3",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.0",
|
||||
"dev": true,
|
||||
@@ -34409,6 +34408,12 @@
|
||||
"version": "2.0.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
|
||||
"integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.24",
|
||||
"license": "MIT",
|
||||
@@ -35718,15 +35723,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.0",
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
"set-function-length": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -35735,6 +35739,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
|
||||
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/caller-callsite": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
@@ -36771,6 +36787,19 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer": {
|
||||
"version": "0.1.2",
|
||||
"dev": true,
|
||||
@@ -36885,29 +36914,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
@@ -37013,12 +37019,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -37036,6 +37039,17 @@
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw=="
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
||||
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
@@ -38217,15 +38231,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
|
||||
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"dunder-proto": "^1.0.0",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.0.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -38379,10 +38398,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -38473,6 +38493,7 @@
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -38482,8 +38503,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"license": "MIT",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -41243,6 +41265,14 @@
|
||||
"version": "1.2.5",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"license": "CC0-1.0"
|
||||
@@ -43118,6 +43148,28 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom/node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-freeze": {
|
||||
"version": "1.0.3",
|
||||
"license": "MIT",
|
||||
@@ -44271,13 +44323,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/sanitize-filename": {
|
||||
"version": "1.6.3",
|
||||
"dev": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.23",
|
||||
"version": "3.0.25",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -24,6 +24,7 @@
|
||||
"release-android-bundle": "cd native/android && ./gradlew bundleRelease --no-daemon"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.39",
|
||||
"fonteditor-core": "^2.1.11",
|
||||
"listr": "^0.14.3",
|
||||
"otplib": "12.0.1",
|
||||
@@ -37,18 +38,20 @@
|
||||
"dependencies": {
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@notesnook/editor": "file:../../packages/editor",
|
||||
"@notesnook/editor-mobile": "file:../../packages/editor-mobile",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
"@notesnook/logger": "file:../../packages/logger",
|
||||
"@notesnook/theme": "file:../../packages/theme",
|
||||
"@notesnook/themes-server": "file:../../servers/themes",
|
||||
"diffblazer": "^1.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.74.5",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/react-query": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@tanstack/react-query": "^4.36.1"
|
||||
"@types/validator": "^13.12.2",
|
||||
"diffblazer": "^1.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.74.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -496,6 +496,7 @@ export const Search = ({
|
||||
const tagId = await db.tags.add({
|
||||
title: searchKeyword
|
||||
});
|
||||
if (!tagId) return;
|
||||
SearchSetters.selectTags(tagId);
|
||||
onSearch();
|
||||
checkQueryExists(searchKeyword);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"maxNodeModuleJsDepth": 5,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"exclude": ["native"]
|
||||
"exclude": ["native", "e2e"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400/yr
|
||||
Total: ₹400
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400/yr
|
||||
Total: ₹400
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400/yr
|
||||
Total: ₹400
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $200
|
||||
Sales tax: $0
|
||||
Discount: -$200
|
||||
Total: $200/yr
|
||||
Total: $200
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $200
|
||||
Sales tax: $0
|
||||
Discount: -$200
|
||||
Total: $200/yr
|
||||
Total: $200
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $200
|
||||
Sales tax: $0
|
||||
Discount: -$200
|
||||
Total: $200/yr
|
||||
Total: $200
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import { NOTE, TITLE_ONLY_NOTE } from "./utils";
|
||||
import { getTestId, NOTE, TITLE_ONLY_NOTE } from "./utils";
|
||||
|
||||
test("focus mode", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
@@ -270,3 +270,103 @@ test("#1468 count words separated by newlines", async ({ page }) => {
|
||||
|
||||
expect((await notes.editor.getWordCount()) === 10).toBeTruthy();
|
||||
});
|
||||
|
||||
test("disable autosave when note crosses MAX_AUTO_SAVEABLE_WORDS", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const content = "a ".repeat(100);
|
||||
|
||||
await notes.createNote({
|
||||
title: "many words",
|
||||
content
|
||||
});
|
||||
|
||||
expect(
|
||||
await app.toasts.waitForToast(
|
||||
"Auto-save is disabled for large notes. Press Ctrl + S to save."
|
||||
)
|
||||
).toBe(true);
|
||||
await expect(notes.editor.notSavedIcon).toBeVisible();
|
||||
});
|
||||
|
||||
test("when autosave is disabled, pressing ctrl+s should save the note", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const content = "a ".repeat(100);
|
||||
await notes.createNote({
|
||||
title: NOTE.title,
|
||||
content
|
||||
});
|
||||
|
||||
await page.keyboard.press("Control+s");
|
||||
|
||||
await expect(notes.editor.savedIcon).toBeVisible();
|
||||
});
|
||||
|
||||
test("when autosave is disabled, switching to another note should save the note", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const content = "a ".repeat(100);
|
||||
const note1 = await notes.createNote({
|
||||
title: "Test note 1"
|
||||
});
|
||||
const note2 = await notes.createNote({
|
||||
title: "Test note 2"
|
||||
});
|
||||
await note1?.openNote();
|
||||
await notes.editor.setContent(content);
|
||||
|
||||
await note2?.openNote();
|
||||
|
||||
await note1?.openNote();
|
||||
await expect(notes.editor.savedIcon).toBeVisible();
|
||||
expect(await notes.editor.getContent("text")).toBe(content.trim());
|
||||
});
|
||||
|
||||
test("when autosave is disabled, creating a new note should save the note", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const content = "a ".repeat(100);
|
||||
const note = await notes.createNote({
|
||||
title: NOTE.title,
|
||||
content
|
||||
});
|
||||
|
||||
await notes.newNote();
|
||||
|
||||
await note?.openNote();
|
||||
await expect(notes.editor.savedIcon).toBeVisible();
|
||||
expect(await notes.editor.getContent("text")).toBe(content.trim());
|
||||
});
|
||||
|
||||
test("when autosave is disabled, closing the note should save it", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const content = "a ".repeat(100);
|
||||
const note = await notes.createNote({
|
||||
title: "Title",
|
||||
content
|
||||
});
|
||||
|
||||
const noteTab = await notes.editor.findTab((await note!.getId())!);
|
||||
await noteTab?.close();
|
||||
|
||||
await note?.openNote();
|
||||
await expect(notes.editor.savedIcon).toBeVisible();
|
||||
expect(await notes.editor.getContent("text")).toBe(content.trim());
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export class BaseItemModel {
|
||||
}
|
||||
|
||||
async getId() {
|
||||
return await this.locator.getAttribute("id");
|
||||
return (await this.locator.getAttribute("id"))?.replace("id_", "");
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
|
||||
@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
import { TabItemModel } from "./tab-item.model";
|
||||
import { iterateList } from "./utils";
|
||||
|
||||
export class EditorModel {
|
||||
private readonly page: Page;
|
||||
@@ -33,6 +35,9 @@ export class EditorModel {
|
||||
private readonly wordCountText: Locator;
|
||||
private readonly dateEditedText: Locator;
|
||||
private readonly searchButton: Locator;
|
||||
private readonly tabsList: Locator;
|
||||
readonly savedIcon: Locator;
|
||||
readonly notSavedIcon: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
@@ -51,6 +56,9 @@ export class EditorModel {
|
||||
this.wordCountText = page.locator(getTestId("editor-word-count"));
|
||||
this.dateEditedText = page.locator(getTestId("editor-date-edited"));
|
||||
this.searchButton = page.locator(getTestId("Search"));
|
||||
this.savedIcon = page.locator(getTestId("editor-save-state-saved"));
|
||||
this.notSavedIcon = page.locator(getTestId("editor-save-state-notsaved"));
|
||||
this.tabsList = page.locator(getTestId("tabs"));
|
||||
}
|
||||
|
||||
async waitForLoading(title?: string, content?: string) {
|
||||
@@ -227,4 +235,11 @@ export class EditorModel {
|
||||
.replace(" words", "")
|
||||
);
|
||||
}
|
||||
|
||||
async findTab(id: string) {
|
||||
for await (const item of iterateList(this.tabsList.locator(".tab"))) {
|
||||
const tabModel = new TabItemModel(item, this.page);
|
||||
if ((await tabModel.getId()) === id) return tabModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
apps/web/__e2e__/models/tab-item.model.ts
Normal file
36
apps/web/__e2e__/models/tab-item.model.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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 { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
|
||||
export class TabItemModel {
|
||||
private readonly closeButton: Locator;
|
||||
constructor(private readonly locator: Locator, page: Page) {
|
||||
this.closeButton = locator.locator(getTestId("tab-close-button"));
|
||||
}
|
||||
|
||||
async getId() {
|
||||
const testId = await this.locator.getAttribute("data-test-id");
|
||||
return testId?.replace("tab-", "");
|
||||
}
|
||||
close() {
|
||||
return this.closeButton.click();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import {
|
||||
getTestId,
|
||||
groupByOptions,
|
||||
NOTE,
|
||||
orderByOptions,
|
||||
|
||||
4
apps/web/package-lock.json
generated
4
apps/web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.22",
|
||||
"version": "3.0.23",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.22",
|
||||
"version": "3.0.23",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.22",
|
||||
"version": "3.0.23",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -206,7 +206,8 @@ function AuthContainer(props) {
|
||||
>
|
||||
<Text variant={"subBody"}>
|
||||
{version.status === "fulfilled" &&
|
||||
version.value?.instance !== "default" ? (
|
||||
!!version.value &&
|
||||
version.value.instance !== "default" ? (
|
||||
<>
|
||||
{strings.usingInstance(
|
||||
version.value.instance,
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Lock,
|
||||
NormalMode,
|
||||
Note,
|
||||
NoteRemove,
|
||||
Pin,
|
||||
Properties,
|
||||
Publish,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
} from "../icons";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import {
|
||||
SaveState,
|
||||
SessionType,
|
||||
isLockedSession,
|
||||
useEditorStore
|
||||
@@ -260,7 +262,7 @@ function TabStrip() {
|
||||
style={{ flex: 1 }}
|
||||
trackStyle={() => ({
|
||||
backgroundColor: "transparent",
|
||||
pointerEvents: "none"
|
||||
"--ms-track-size": "6px"
|
||||
})}
|
||||
thumbStyle={() => ({ height: 3 })}
|
||||
onWheel={(e) => {
|
||||
@@ -281,6 +283,7 @@ function TabStrip() {
|
||||
e.stopPropagation();
|
||||
useEditorStore.getState().newSession();
|
||||
}}
|
||||
data-test-id="tabs"
|
||||
>
|
||||
<ReorderableList
|
||||
items={sessions}
|
||||
@@ -302,89 +305,95 @@ function TabStrip() {
|
||||
sessions.splice(to, 0, fromTab);
|
||||
useEditorStore.setState({ sessions });
|
||||
}}
|
||||
renderItem={({ item: session, index: i }) => (
|
||||
<Tab
|
||||
id={session.id}
|
||||
key={session.id}
|
||||
title={
|
||||
session.title ||
|
||||
("note" in session ? session.note.title : "Untitled")
|
||||
}
|
||||
isTemporary={!!session.preview}
|
||||
isActive={session.id === activeSessionId}
|
||||
isPinned={!!session.pinned}
|
||||
isLocked={isLockedSession(session)}
|
||||
type={session.type}
|
||||
onKeepOpen={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.updateSession(
|
||||
session.id,
|
||||
[session.type],
|
||||
(s) => (s.preview = false)
|
||||
)
|
||||
}
|
||||
onFocus={() => {
|
||||
if (session.id !== activeSessionId) {
|
||||
useEditorStore.getState().openSession(session.id);
|
||||
renderItem={({ item: session, index: i }) => {
|
||||
const isUnsaved =
|
||||
session.type === "default" &&
|
||||
session.saveState === SaveState.NotSaved;
|
||||
return (
|
||||
<Tab
|
||||
id={session.id}
|
||||
key={session.id}
|
||||
title={
|
||||
session.title ||
|
||||
("note" in session ? session.note.title : "Untitled")
|
||||
}
|
||||
}}
|
||||
onClose={() =>
|
||||
useEditorStore.getState().closeSessions(session.id)
|
||||
}
|
||||
onCloseAll={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions.filter((s) => !s.pinned).map((s) => s.id)
|
||||
isUnsaved={isUnsaved}
|
||||
isTemporary={!!session.preview}
|
||||
isActive={session.id === activeSessionId}
|
||||
isPinned={!!session.pinned}
|
||||
isLocked={isLockedSession(session)}
|
||||
type={session.type}
|
||||
onKeepOpen={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.updateSession(
|
||||
session.id,
|
||||
[session.type],
|
||||
(s) => (s.preview = false)
|
||||
)
|
||||
}
|
||||
onFocus={() => {
|
||||
if (session.id !== activeSessionId) {
|
||||
useEditorStore.getState().openSession(session.id);
|
||||
}
|
||||
}}
|
||||
onClose={() =>
|
||||
useEditorStore.getState().closeSessions(session.id)
|
||||
}
|
||||
onCloseAll={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions.filter((s) => !s.pinned).map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onCloseOthers={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s) => s.id !== session.id && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onCloseToTheRight={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s, index) => index > i && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onCloseToTheLeft={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s, index) => index < i && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onRevealInList={() =>
|
||||
AppEventManager.publish(
|
||||
AppEvents.revealItemInList,
|
||||
"note" in session ? session.note.id : session.id,
|
||||
true
|
||||
)
|
||||
}
|
||||
onCloseOthers={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s) => s.id !== session.id && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onCloseToTheRight={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s, index) => index > i && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onCloseToTheLeft={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s, index) => index < i && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onRevealInList={() =>
|
||||
AppEventManager.publish(
|
||||
AppEvents.revealItemInList,
|
||||
"note" in session ? session.note.id : session.id,
|
||||
true
|
||||
)
|
||||
}
|
||||
onPin={() => {
|
||||
useEditorStore.setState((state) => {
|
||||
// preview tabs can never be pinned.
|
||||
if (!session.pinned) state.sessions[i].preview = false;
|
||||
state.sessions[i].pinned = !session.pinned;
|
||||
state.sessions.sort((a, b) =>
|
||||
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
onPin={() => {
|
||||
useEditorStore.setState((state) => {
|
||||
// preview tabs can never be pinned.
|
||||
if (!session.pinned) state.sessions[i].preview = false;
|
||||
state.sessions[i].pinned = !session.pinned;
|
||||
state.sessions.sort((a, b) =>
|
||||
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollContainer>
|
||||
@@ -398,6 +407,7 @@ type TabProps = {
|
||||
isTemporary: boolean;
|
||||
isPinned: boolean;
|
||||
isLocked: boolean;
|
||||
isUnsaved: boolean;
|
||||
type: SessionType;
|
||||
onKeepOpen: () => void;
|
||||
onFocus: () => void;
|
||||
@@ -417,6 +427,7 @@ function Tab(props: TabProps) {
|
||||
isTemporary,
|
||||
isPinned,
|
||||
isLocked,
|
||||
isUnsaved,
|
||||
type,
|
||||
onKeepOpen,
|
||||
onFocus,
|
||||
@@ -436,6 +447,8 @@ function Tab(props: TabProps) {
|
||||
? Readonly
|
||||
: type === "deleted"
|
||||
? Trash
|
||||
: isUnsaved
|
||||
? NoteRemove
|
||||
: Note;
|
||||
const { attributes, listeners, setNodeRef, transform, transition, active } =
|
||||
useSortable({ id });
|
||||
@@ -444,6 +457,7 @@ function Tab(props: TabProps) {
|
||||
<Flex
|
||||
ref={setNodeRef}
|
||||
className="tab"
|
||||
data-test-id={`tab-${id}`}
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
cursor: "pointer",
|
||||
@@ -544,7 +558,13 @@ function Tab(props: TabProps) {
|
||||
if (e.button == 0) onFocus();
|
||||
}}
|
||||
>
|
||||
<Icon size={16} color={isActive ? "accent-selected" : "icon"} />
|
||||
<Icon
|
||||
data-test-id={`tab-icon${isUnsaved ? "-unsaved" : ""}`}
|
||||
size={16}
|
||||
color={
|
||||
isUnsaved ? "accent-error" : isActive ? "accent-selected" : "icon"
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
@@ -590,6 +610,7 @@ function Tab(props: TabProps) {
|
||||
}
|
||||
}}
|
||||
className="closeTabButton"
|
||||
data-test-id={"tab-close-button"}
|
||||
size={16}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -121,6 +121,13 @@ function EditorFooter() {
|
||||
) : null}
|
||||
{SaveStateIcon && (
|
||||
<SaveStateIcon
|
||||
data-test-id={`editor-save-state-${
|
||||
saveState === SaveState.Saved
|
||||
? "saved"
|
||||
: saveState === SaveState.NotSaved
|
||||
? "notsaved"
|
||||
: "loading"
|
||||
}`}
|
||||
size={13}
|
||||
color={
|
||||
saveState === SaveState.Saved
|
||||
|
||||
@@ -71,9 +71,12 @@ import { logger } from "../../utils/logger";
|
||||
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
|
||||
import { NoteLinkingDialog } from "../../dialogs/note-linking-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { onPageVisibilityChanged } from "../../utils/page-visibility";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
const autoSaveToast = { show: true, hide: () => {} };
|
||||
|
||||
async function saveContent(
|
||||
noteId: string,
|
||||
ignoreEdit: boolean,
|
||||
@@ -439,9 +442,17 @@ export function Editor(props: EditorProps) {
|
||||
readonly: false,
|
||||
focusMode: false
|
||||
};
|
||||
const saveSessionContentIfNotSaved = useEditorStore(
|
||||
(store) => store.saveSessionContentIfNotSaved
|
||||
);
|
||||
const setEditorSaveState = useEditorStore((store) => store.setSaveState);
|
||||
useScrollToBlock(session);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoSaveToast.show) {
|
||||
autoSaveToast.hide();
|
||||
}
|
||||
|
||||
const event = AppEventManager.subscribe(
|
||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||
({ hash, loaded, total }: AttachmentProgress) => {
|
||||
@@ -458,6 +469,15 @@ export function Editor(props: EditorProps) {
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = onPageVisibilityChanged((_, hidden) => {
|
||||
if (hidden) {
|
||||
saveSessionContentIfNotSaved(id);
|
||||
}
|
||||
});
|
||||
return () => unsub();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditorChrome {...props}>
|
||||
<Tiptap
|
||||
@@ -559,6 +579,25 @@ export function Editor(props: EditorProps) {
|
||||
const link = await NoteLinkingDialog.show({ attributes });
|
||||
return link || undefined;
|
||||
}}
|
||||
onAutoSaveDisabled={() => {
|
||||
setEditorSaveState(id, SaveState.NotSaved);
|
||||
if (autoSaveToast.show === false) return;
|
||||
const { hide } = showToast(
|
||||
"error",
|
||||
"Auto-save is disabled for large notes. Press Ctrl + S to save.",
|
||||
[
|
||||
{
|
||||
text: "Dismiss",
|
||||
onClick: () => {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
],
|
||||
Infinity
|
||||
);
|
||||
autoSaveToast.show = false;
|
||||
autoSaveToast.hide = hide;
|
||||
}}
|
||||
>
|
||||
{headless ? null : (
|
||||
<>
|
||||
|
||||
@@ -63,6 +63,7 @@ import useTablet from "../../hooks/use-tablet";
|
||||
import { TimeFormat } from "@notesnook/core";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog";
|
||||
import { EDITOR_ZOOM } from "./common";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
@@ -89,6 +90,7 @@ type TipTapProps = {
|
||||
) => Promise<LinkAttributes | undefined>;
|
||||
onAttachFile?: (file: File) => void;
|
||||
onFocus?: () => void;
|
||||
onAutoSaveDisabled: () => void;
|
||||
content?: () => string | undefined;
|
||||
readonly?: boolean;
|
||||
nonce?: number;
|
||||
@@ -132,12 +134,11 @@ function TipTap(props: TipTapProps) {
|
||||
onInsertInternalLink,
|
||||
onContentChange,
|
||||
onFocus = () => {},
|
||||
onAutoSaveDisabled,
|
||||
content,
|
||||
editorContainer,
|
||||
readonly,
|
||||
nonce,
|
||||
isMobile,
|
||||
isTablet,
|
||||
downloadOptions,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
@@ -347,6 +348,9 @@ function TipTap(props: TipTapProps) {
|
||||
(s) => s.editors[id]?.statistics?.words.total,
|
||||
(totalWords) => {
|
||||
autoSave.current = !totalWords || totalWords < MAX_AUTO_SAVEABLE_WORDS;
|
||||
if (!autoSave.current) {
|
||||
onAutoSaveDisabled();
|
||||
}
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
@@ -366,14 +370,36 @@ function TipTap(props: TipTapProps) {
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
editor={editor}
|
||||
location={"top"}
|
||||
sx={isTablet || isMobile ? { flexWrap: "nowrap" } : {}}
|
||||
tools={toolbarConfig}
|
||||
defaultFontFamily={fontFamily}
|
||||
defaultFontSize={fontSize}
|
||||
/>
|
||||
<ScrollContainer
|
||||
className="toolbarScroll"
|
||||
suppressScrollY
|
||||
style={{ display: "flex" }}
|
||||
trackStyle={() => ({
|
||||
backgroundColor: "transparent",
|
||||
"--ms-track-size": "6px"
|
||||
})}
|
||||
thumbStyle={() => ({ height: 3 })}
|
||||
onWheel={(e) => {
|
||||
const scrollcontainer = document.querySelector(
|
||||
".active .toolbarScroll"
|
||||
);
|
||||
if (!scrollcontainer) return;
|
||||
if (e.deltaY > 0) scrollcontainer.scrollLeft += 100;
|
||||
else if (e.deltaY < 0) scrollcontainer.scrollLeft -= 100;
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
editor={editor}
|
||||
location={"top"}
|
||||
sx={{
|
||||
flexWrap: "unset",
|
||||
overflowX: "unset"
|
||||
}}
|
||||
tools={toolbarConfig}
|
||||
defaultFontFamily={fontFamily}
|
||||
defaultFontSize={fontSize}
|
||||
/>
|
||||
</ScrollContainer>
|
||||
</ScopedThemeProvider>
|
||||
</>
|
||||
);
|
||||
@@ -576,7 +602,9 @@ function toIEditor(editor: Editor): IEditor {
|
||||
},
|
||||
{ query: (a) => a.hash === hash, preventUpdate: true }
|
||||
),
|
||||
startSearch: () => editor.commands.startSearch()
|
||||
startSearch: () => editor.commands.startSearch(),
|
||||
getContent: () =>
|
||||
getHTMLFromFragment(editor.state.doc.content, editor.schema)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
|
||||
export const MAX_AUTO_SAVEABLE_WORDS = 100_000;
|
||||
export const MAX_AUTO_SAVEABLE_WORDS = IS_TESTING ? 100 : 100_000;
|
||||
|
||||
export type NoteStatistics = {
|
||||
words: {
|
||||
@@ -39,4 +39,5 @@ export interface IEditor {
|
||||
attachFile: (file: Attachment) => void;
|
||||
sendAttachmentProgress: (hash: string, progress: number) => void;
|
||||
startSearch: () => void;
|
||||
getContent: () => string;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ import { db } from "../../common/db";
|
||||
import { createDialect } from "../../common/sqlite";
|
||||
import { getDeviceInfo } from "../../utils/platform";
|
||||
|
||||
const IGNORED_ERRORS = ["Error in input stream", "network error"];
|
||||
const IGNORED_ERRORS = [
|
||||
"Error in input stream",
|
||||
"network error",
|
||||
"NetworkError when attempting to fetch resource."
|
||||
];
|
||||
export function GlobalErrorHandler(props: PropsWithChildren) {
|
||||
const { showBoundary } = useErrorBoundary();
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ const groupByMenu: (options: GroupingMenuOptions) => MenuItem | null = (
|
||||
icon: GroupBy.path,
|
||||
menu: {
|
||||
items: map(options, [
|
||||
{ key: "none", title: strings.groupByStrings.None() },
|
||||
{ key: "none", title: strings.groupByStrings.none() },
|
||||
{ key: "default", title: strings.groupByStrings.default() },
|
||||
{ key: "year", title: strings.groupByStrings.year() },
|
||||
{ key: "month", title: strings.groupByStrings.month() },
|
||||
|
||||
@@ -218,7 +218,8 @@ import {
|
||||
mdiServerSecurity,
|
||||
mdiOpenInNew,
|
||||
mdiTagOutline,
|
||||
mdiChatQuestionOutline
|
||||
mdiChatQuestionOutline,
|
||||
mdiNoteRemoveOutline
|
||||
} from "@mdi/js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
@@ -313,6 +314,7 @@ function createIcon(path: string, rotate = false) {
|
||||
|
||||
export const Plus = createIcon(mdiPlus);
|
||||
export const Note = createIcon(mdiNoteOutline);
|
||||
export const NoteRemove = createIcon(mdiNoteRemoveOutline);
|
||||
export const Notes = createIcon(mdiNoteMultipleOutline);
|
||||
export const Minus = createIcon(mdiMinus);
|
||||
export const Notebook = createIcon(mdiBookOutline);
|
||||
|
||||
@@ -147,7 +147,6 @@ function getDate(item: Item, groupType?: GroupingKey): number {
|
||||
groupType
|
||||
? db.settings.getGroupOptions(groupType)
|
||||
: {
|
||||
groupBy: "default",
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
},
|
||||
|
||||
@@ -93,29 +93,14 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "5x faster startup",
|
||||
title: "Notesnook Gift Cards",
|
||||
subtitle:
|
||||
"We have optimized the app startup time by 5x (3000ms to 600ms). This means you can now start taking notes faster than ever."
|
||||
"You can now gift Notesnook Pro to your friends and family. Gift cards are available in 1, 3, and 5-year plans at https://notesnook.com/giftcards."
|
||||
},
|
||||
{
|
||||
title: "Improved ToC UX",
|
||||
title: "Sorted search results",
|
||||
subtitle:
|
||||
"We have made a bunch of improvements to the ToC (Table of Contents): it no longer covers the note content, and its open/closed state is preserved between app restarts."
|
||||
},
|
||||
{
|
||||
title: "Faster checkout",
|
||||
subtitle:
|
||||
"Checkout is now 1-step. You can now directly enter your payment details and complete the purchase without any intermediate steps."
|
||||
},
|
||||
{
|
||||
title: "Zoom in Editor",
|
||||
subtitle:
|
||||
"You can now zoom in/out of the editor using the Ctrl/Cmd + mouse wheel. This is separate from the default font size."
|
||||
},
|
||||
{
|
||||
title: "Improved search",
|
||||
subtitle:
|
||||
"Search now defaults to AND mode instead of OR mode which better matches the expected behavior. You can still use OR if you prefer the old behavior."
|
||||
"Search results are now sorted by date created by default."
|
||||
}
|
||||
],
|
||||
cta: {
|
||||
|
||||
@@ -45,7 +45,6 @@ export const AuthenticationSettings: SettingsGroup[] = [
|
||||
title: strings.changePassword(),
|
||||
variant: "secondary",
|
||||
action: async () => {
|
||||
if (!(await createBackup())) return;
|
||||
const result = await showPasswordDialog({
|
||||
title: strings.changePassword(),
|
||||
message: strings.changePasswordDesc(),
|
||||
@@ -60,6 +59,7 @@ export const AuthenticationSettings: SettingsGroup[] = [
|
||||
}
|
||||
},
|
||||
validate: async ({ oldPassword, newPassword }) => {
|
||||
if (!(await createBackup())) return false;
|
||||
await db.user.clearSessions();
|
||||
return (
|
||||
(await db.user.changePassword(oldPassword, newPassword)) ||
|
||||
|
||||
@@ -30,13 +30,8 @@ import { Features } from "../../buy-dialog/features";
|
||||
import { ConfirmDialog } from "../../confirm";
|
||||
import { BuyDialog } from "../../buy-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { PromptDialog } from "../../prompt";
|
||||
|
||||
const PROVIDER_MAP = {
|
||||
0: "Streetwriters",
|
||||
1: "iOS",
|
||||
2: "Android",
|
||||
3: "Web"
|
||||
} as const;
|
||||
export function SubscriptionStatus() {
|
||||
const user = useUserStore((store) => store.user);
|
||||
|
||||
@@ -44,7 +39,8 @@ export function SubscriptionStatus() {
|
||||
await db.user.activateTrial();
|
||||
});
|
||||
|
||||
const provider = PROVIDER_MAP[user?.subscription?.provider || 0];
|
||||
const provider =
|
||||
strings.subscriptionProviderInfo[user?.subscription?.provider || 0];
|
||||
const {
|
||||
isTrial,
|
||||
isBeta,
|
||||
@@ -72,7 +68,7 @@ export function SubscriptionStatus() {
|
||||
const expiryDate = dayjs(user?.subscription?.expiry).format("MMMM D, YYYY");
|
||||
const startDate = dayjs(user?.subscription?.start).format("MMMM D, YYYY");
|
||||
return isPro
|
||||
? provider === "Streetwriters"
|
||||
? provider.type === "Streetwriters" || provider.type === "Gift card"
|
||||
? `Ending on ${expiryDate}`
|
||||
: `Next payment on ${expiryDate}.`
|
||||
: isProCancelled
|
||||
@@ -132,10 +128,10 @@ export function SubscriptionStatus() {
|
||||
: "Access only to basic features including unlimited notes & end-to-end encrypted syncing to unlimited devices."}
|
||||
</Text>
|
||||
<Text sx={{ mt: 2 }} variant="subBody">
|
||||
{subtitle}
|
||||
{subtitle}. {provider.desc()}
|
||||
</Text>
|
||||
<Flex sx={{ gap: 1, mt: 2 }}>
|
||||
{provider === "Web" && (isPro || isProCancelled) ? (
|
||||
{provider.type === "Web" && (isPro || isProCancelled) ? (
|
||||
<>
|
||||
{isPro && (
|
||||
<Button
|
||||
@@ -209,6 +205,26 @@ export function SubscriptionStatus() {
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const giftCode = await PromptDialog.show({
|
||||
title: strings.redeemGiftCode(),
|
||||
description: strings.redeemGiftCodeDesc()
|
||||
});
|
||||
if (giftCode) {
|
||||
await TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: strings.redeemingGiftCode(),
|
||||
subtitle: strings.pleaseWait() + "...",
|
||||
action: () => db.subscriptions.redeemCode(giftCode)
|
||||
}).catch((e) => showToast("error", e.message));
|
||||
}
|
||||
}}
|
||||
sx={{ bg: "background" }}
|
||||
>
|
||||
{strings.redeemGiftCode()}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -172,6 +172,17 @@ export class NNStorage implements IStorage {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
// noop
|
||||
generateCryptoKeyFallback(
|
||||
password: string,
|
||||
salt?: string
|
||||
): Promise<SerializedKey> {
|
||||
return this.generateCryptoKey(password, salt);
|
||||
}
|
||||
|
||||
// noop
|
||||
async deriveCryptoKeyFallback(): Promise<void> {}
|
||||
}
|
||||
|
||||
const dec = new TextDecoder();
|
||||
|
||||
@@ -49,6 +49,7 @@ import { hashNavigate } from "../navigation";
|
||||
import { AppEventManager, AppEvents } from "../common/app-events";
|
||||
import Vault from "../common/vault";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { useEditorManager } from "../components/editor/manager";
|
||||
|
||||
export enum SaveState {
|
||||
NotSaved = -1,
|
||||
@@ -501,6 +502,12 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const session = this.get().sessions.find((s) => s.id === id);
|
||||
if (!session) id = undefined;
|
||||
|
||||
const activeSession = this.getActiveSession();
|
||||
|
||||
if (activeSession) {
|
||||
this.saveSessionContentIfNotSaved(activeSession.id);
|
||||
}
|
||||
|
||||
if (
|
||||
id &&
|
||||
!settingStore.get().hideNoteTitle &&
|
||||
@@ -848,7 +855,6 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
|
||||
setDocumentTitle(
|
||||
settingStore.get().hideNoteTitle ? undefined : note.title
|
||||
);
|
||||
@@ -870,6 +876,25 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
});
|
||||
};
|
||||
|
||||
saveSessionContentIfNotSaved = (sessionId: string) => {
|
||||
const sessionSaveState = this.getSession(sessionId, ["default"])?.saveState;
|
||||
if (sessionSaveState === SaveState.NotSaved) {
|
||||
const editor = useEditorManager.getState().getEditor(sessionId);
|
||||
const content = editor?.editor?.getContent();
|
||||
this.saveSession(
|
||||
sessionId,
|
||||
content
|
||||
? {
|
||||
content: {
|
||||
data: content,
|
||||
type: "tiptap"
|
||||
}
|
||||
}
|
||||
: {}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
newSession = () => {
|
||||
const state = useEditorStore.getState();
|
||||
const session = state.sessions.find((session) => session.type === "new");
|
||||
@@ -896,6 +921,8 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.saveSessionContentIfNotSaved(session.id);
|
||||
|
||||
db.fs().cancel(session.id).catch(console.error);
|
||||
if (state.history.includes(session.id))
|
||||
state.history.splice(state.history.indexOf(session.id), 1);
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { path } from "@notesnook-importer/core/dist/src/utils/path";
|
||||
import { type ZipEntry } from "./streams/unzip-stream";
|
||||
import { hashBuffer, writeEncryptedFile } from "../interfaces/fs";
|
||||
import { Notebook as NotebookType } from "@notesnook/core";
|
||||
|
||||
export async function* importFiles(zipFiles: File[]) {
|
||||
const { createUnzipIterator } = await import("./streams/unzip-stream");
|
||||
@@ -219,26 +220,29 @@ async function importLegacyNotebook(
|
||||
|
||||
async function importNotebook(
|
||||
notebook: Notebook,
|
||||
parentId?: string
|
||||
parent?: NotebookType
|
||||
): Promise<string[]> {
|
||||
if (!notebook) return [];
|
||||
|
||||
const id =
|
||||
(await db.notebooks.find(notebook.title))?.id ||
|
||||
(await db.notebooks.add({
|
||||
const selector = parent
|
||||
? db.relations.from(parent, "notebook").selector
|
||||
: db.notebooks.roots;
|
||||
let nb = await selector.find((eb) =>
|
||||
eb("notebooks.title", "==", notebook.title)
|
||||
);
|
||||
if (!nb) {
|
||||
const id = await db.notebooks.add({
|
||||
title: notebook.title
|
||||
}));
|
||||
if (!id) throw new Error(`Failed to import notebook: ${notebook.title}`);
|
||||
|
||||
if (parentId)
|
||||
await db.relations.add(
|
||||
{ type: "notebook", id: parentId },
|
||||
{ type: "notebook", id: id }
|
||||
);
|
||||
|
||||
const assignedNotebooks: string[] = notebook.children.length > 0 ? [] : [id];
|
||||
for (const child of notebook.children || []) {
|
||||
assignedNotebooks.push(...(await importNotebook(child, id)));
|
||||
});
|
||||
if (!id) return [];
|
||||
nb = await db.notebooks.notebook(id);
|
||||
if (parent && nb) await db.relations.add(parent, nb);
|
||||
}
|
||||
if (!nb) return [];
|
||||
if (notebook.children.length === 0) return [nb.id];
|
||||
|
||||
const assignedNotebooks: string[] = [];
|
||||
for (const child of notebook.children || [])
|
||||
assignedNotebooks.push(...(await importNotebook(child, nb)));
|
||||
return assignedNotebooks;
|
||||
}
|
||||
|
||||
@@ -310,7 +310,8 @@ function RecoveryMethods(props: BaseRecoveryComponentProps<"methods">) {
|
||||
sx={{
|
||||
color: method.isDangerous
|
||||
? "var(--paragraph-error)"
|
||||
: "var(--paragraph-secondary)"
|
||||
: "var(--paragraph-secondary)",
|
||||
whiteSpace: "pre-wrap"
|
||||
}}
|
||||
>
|
||||
{method.description()}
|
||||
|
||||
84
docs/help/contents/gift-cards.md
Normal file
84
docs/help/contents/gift-cards.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Gift cards
|
||||
description: Gift your friends and family a Notesnook Pro subscription.
|
||||
---
|
||||
|
||||
# Gift cards
|
||||
|
||||
Gift cards allow you to gift anyone a free Notesnook Pro subscription. It's a simple 3-step process:
|
||||
|
||||
1. You purchase a gift card.
|
||||
2. You send someone the gift code.
|
||||
3. They redeem it and claim the benefits.
|
||||
|
||||
## Purchasing a gift card
|
||||
|
||||
You can purchase a gift card from [https://notesnook.com/giftcards](https://notesnook.com/giftcards). There are 3 gift cards currently:
|
||||
|
||||
- 1 year gift card
|
||||
- 3 years gift card
|
||||
- 5 years gift card
|
||||
|
||||
> info
|
||||
>
|
||||
> Gift cards are not attached to a user account and can be claimed by any Notesnook user.
|
||||
|
||||
## Redeem a gift code
|
||||
|
||||
> info
|
||||
>
|
||||
> You won't be able to redeem a gift code if you already have a Notesnook subscription (even if its cancelled).
|
||||
|
||||
# [Desktop/Web](#/tab/web)
|
||||
|
||||
1. Go to `Settings`
|
||||
2. Go to `Subscription settings`
|
||||
3. Click on `Redeem a gift code` button
|
||||
4. Enter the gift code you received
|
||||
5. Click on `Submit` and wait for the app to verify your gift code.
|
||||
6. Once the process succeeds, you should be upgraded to Pro.
|
||||
|
||||
# [Mobile](#/tab/mobile)
|
||||
|
||||
1. Go to `Settings`
|
||||
2. Go to `Account settings`
|
||||
3. Click on `Redeem a gift code`
|
||||
4. Enter the gift code you received
|
||||
5. Click on `Redeem` and wait for the app to verify your gift code.
|
||||
6. Once the process succeeds, you should be upgraded to Pro.
|
||||
|
||||
---
|
||||
|
||||
> info
|
||||
>
|
||||
> Once you redeem a gift code, the person who purchased it will receive an email informing them that one of their gift code was claimed. The email **does not** contain any information about who claimed the gift code.
|
||||
|
||||
## FAQs
|
||||
|
||||
### Who can redeem a gift code?
|
||||
|
||||
Anyone who doesn't have a Notesnook Pro subscription can redeem a gift code and upgrade their account. One user can only redeem a single gift code at a time.
|
||||
|
||||
### Can I use a gift card to extend my subscription?
|
||||
|
||||
No.
|
||||
|
||||
### Is there an expiry date to a gift code?
|
||||
|
||||
Yes, these gift codes will expire after 1 year from the time of purchase.
|
||||
|
||||
### What can I do with a gift code?
|
||||
|
||||
Whatever you like. Sell it, gift it, use it.
|
||||
|
||||
### Can I refund a gift code I purchased?
|
||||
|
||||
No. Gift codes are non-refundable.
|
||||
|
||||
### Are gift cards auto-renewable?
|
||||
|
||||
No. Gift cards are a one-time purchase.
|
||||
|
||||
### Can I use cryptocurrency to purchase a gift card?
|
||||
|
||||
Currently, no. But we are actively working on a solution to support this so stay tuned.
|
||||
@@ -55,6 +55,7 @@ navigation:
|
||||
- path: recovering-your-account.md
|
||||
- path: deleting-your-account.md
|
||||
- path: app-lock.md
|
||||
- path: gift-cards.md
|
||||
|
||||
- path: privacy-mode.md
|
||||
- path: web-clipper
|
||||
|
||||
5
fastlane/metadata/android/en-US/changelogs/15159.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/15159.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- 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!
|
||||
@@ -189,7 +189,7 @@ class Database {
|
||||
tokenManager = new TokenManager(this.kv);
|
||||
mfa = new MFAManager(this.tokenManager);
|
||||
subscriptions = new Subscriptions(this.tokenManager);
|
||||
offers = new Offers();
|
||||
offers = Offers;
|
||||
debug = new Debug();
|
||||
pricing = Pricing;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { match } from "fuzzyjs";
|
||||
import Database from "./index.js";
|
||||
import { Item, Note, TrashItem } from "../types.js";
|
||||
import { Item, Note, SortOptions, TrashItem } from "../types.js";
|
||||
import { DatabaseSchema, RawDatabaseSchema } from "../database/index.js";
|
||||
import { AnyColumnWithTable, Kysely, sql } from "@streetwriters/kysely";
|
||||
import { FilteredSelector } from "../database/sql-collection.js";
|
||||
@@ -27,6 +27,7 @@ import { VirtualizedGrouping } from "../utils/virtualized-grouping.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { rebuildSearchIndex } from "../database/fts.js";
|
||||
import { transformQuery } from "../utils/query-transformer.js";
|
||||
import { getSortSelectors } from "../utils/grouping.js";
|
||||
|
||||
type SearchResults<T> = {
|
||||
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
|
||||
@@ -43,7 +44,7 @@ export default class Lookup {
|
||||
constructor(private readonly db: Database) {}
|
||||
|
||||
notes(query: string, notes?: FilteredSelector<Note>): SearchResults<Note> {
|
||||
return this.toSearchResults(async (limit) => {
|
||||
return this.toSearchResults(async (limit, sortOptions) => {
|
||||
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
|
||||
const excludedIds = this.db.trash.cache.notes;
|
||||
|
||||
@@ -71,13 +72,16 @@ export default class Lookup {
|
||||
)
|
||||
.where("data", "match", query)
|
||||
.select(["noteId as id", "rank"])
|
||||
.$castTo<{ id: string; rank: number }>()
|
||||
.$castTo<{
|
||||
id: string;
|
||||
rank: number;
|
||||
}>()
|
||||
)
|
||||
.as("results")
|
||||
)
|
||||
.select(["results.id"])
|
||||
.groupBy("results.id")
|
||||
.orderBy(sql`SUM(results.rank)`, "desc")
|
||||
.orderBy(sql`SUM(results.rank)`, sortOptions?.sortDirection || "desc")
|
||||
.$if(!!limit, (eb) => eb.limit(limit!))
|
||||
|
||||
// filter out ids that have no note against them
|
||||
@@ -119,9 +123,17 @@ export default class Lookup {
|
||||
}
|
||||
|
||||
trash(query: string): SearchResults<TrashItem> {
|
||||
const sortOptions: SortOptions = {
|
||||
sortBy: "dateDeleted",
|
||||
sortDirection: "desc"
|
||||
};
|
||||
return {
|
||||
sorted: async (limit?: number) => {
|
||||
const { ids, items } = await this.filterTrash(query, limit);
|
||||
const { ids, items } = await this.filterTrash(
|
||||
query,
|
||||
limit,
|
||||
sortOptions
|
||||
);
|
||||
return new VirtualizedGrouping<TrashItem>(
|
||||
ids.length,
|
||||
this.db.options.batchSize,
|
||||
@@ -135,7 +147,7 @@ export default class Lookup {
|
||||
);
|
||||
},
|
||||
items: async (limit?: number) => {
|
||||
const { items } = await this.filterTrash(query, limit);
|
||||
const { items } = await this.filterTrash(query, limit, sortOptions);
|
||||
return items;
|
||||
},
|
||||
ids: () => this.filterTrash(query).then(({ ids }) => ids)
|
||||
@@ -157,7 +169,8 @@ export default class Lookup {
|
||||
fields: FuzzySearchField<T>[]
|
||||
) {
|
||||
return this.toSearchResults(
|
||||
(limit) => this.filter(selector, query, fields, limit),
|
||||
(limit, sortOptions) =>
|
||||
this.filter(selector, query, fields, limit, sortOptions),
|
||||
selector
|
||||
);
|
||||
}
|
||||
@@ -166,7 +179,8 @@ export default class Lookup {
|
||||
selector: FilteredSelector<T>,
|
||||
query: string,
|
||||
fields: FuzzySearchField<T>[],
|
||||
limit?: number
|
||||
limit?: number,
|
||||
sortOptions?: SortOptions
|
||||
) {
|
||||
const results: Map<string, number> = new Map();
|
||||
const columns = fields.map((f) => f.column);
|
||||
@@ -183,24 +197,46 @@ export default class Lookup {
|
||||
}
|
||||
selector.fields([]);
|
||||
|
||||
return Array.from(results.entries())
|
||||
.sort((a, b) => a[1] - b[1])
|
||||
.map((a) => a[0]);
|
||||
const sorted = Array.from(results.entries());
|
||||
|
||||
if (!sortOptions)
|
||||
// || sortOptions.sortBy === "relevance")
|
||||
sorted.sort(
|
||||
// sortOptions?.sortDirection === "desc"
|
||||
// ? (a, b) => a[1] - b[1]
|
||||
// :
|
||||
(a, b) => b[1] - a[1]
|
||||
);
|
||||
|
||||
return sorted.map((a) => a[0]);
|
||||
}
|
||||
|
||||
private toSearchResults<T extends Item>(
|
||||
ids: (limit?: number) => Promise<string[]>,
|
||||
ids: (limit?: number, sortOptions?: SortOptions) => Promise<string[]>,
|
||||
selector: FilteredSelector<T>
|
||||
): SearchResults<T> {
|
||||
const sortOptions: SortOptions = {
|
||||
sortBy: "dateCreated",
|
||||
sortDirection: "desc"
|
||||
};
|
||||
return {
|
||||
sorted: async (limit?: number) =>
|
||||
this.toVirtualizedGrouping(await ids(limit), selector),
|
||||
items: async (limit?: number) => this.toItems(await ids(limit), selector),
|
||||
this.toVirtualizedGrouping(
|
||||
await ids(limit, sortOptions),
|
||||
selector,
|
||||
sortOptions
|
||||
),
|
||||
items: async (limit?: number) =>
|
||||
this.toItems(await ids(limit, sortOptions), selector, sortOptions),
|
||||
ids
|
||||
};
|
||||
}
|
||||
|
||||
private async filterTrash(query: string, limit?: number) {
|
||||
private async filterTrash(
|
||||
query: string,
|
||||
limit?: number,
|
||||
sortOptions?: SortOptions
|
||||
) {
|
||||
const items = await this.db.trash.all();
|
||||
|
||||
const results: Map<string, { rank: number; item: TrashItem }> = new Map();
|
||||
@@ -212,10 +248,20 @@ export default class Lookup {
|
||||
results.set(item.id, { rank: result.score, item });
|
||||
}
|
||||
}
|
||||
const sorted = Array.from(results.entries());
|
||||
|
||||
const sorted = Array.from(results.entries()).sort(
|
||||
(a, b) => a[1].rank - b[1].rank
|
||||
);
|
||||
if (!sortOptions)
|
||||
// || sortOptions.sortBy === "relevance")
|
||||
sorted.sort(
|
||||
// sortOptions?.sortDirection === "desc"
|
||||
// ? (a, b) => a[1].rank - b[1].rank
|
||||
// :
|
||||
(a, b) => b[1].rank - a[1].rank
|
||||
);
|
||||
else {
|
||||
const selector = getSortSelectors(sortOptions)[sortOptions.sortDirection];
|
||||
sorted.sort((a, b) => selector(a[1].item, b[1].item));
|
||||
}
|
||||
return {
|
||||
ids: sorted.map((a) => a[0]),
|
||||
items: sorted.map((a) => a[1].item)
|
||||
@@ -224,28 +270,33 @@ export default class Lookup {
|
||||
|
||||
private toVirtualizedGrouping<T extends Item>(
|
||||
ids: string[],
|
||||
selector: FilteredSelector<T>
|
||||
selector: FilteredSelector<T>,
|
||||
sortOptions?: SortOptions
|
||||
) {
|
||||
// if (sortOptions?.sortBy === "relevance") sortOptions = undefined;
|
||||
return new VirtualizedGrouping<T>(
|
||||
ids.length,
|
||||
this.db.options.batchSize,
|
||||
() => Promise.resolve(ids),
|
||||
async (start, end) => {
|
||||
const items = await selector.records(ids);
|
||||
const items = await selector.items(ids.slice(start, end), sortOptions);
|
||||
return {
|
||||
ids: ids.slice(start, end),
|
||||
items: Object.values(items).slice(start, end)
|
||||
items
|
||||
};
|
||||
}
|
||||
// (items) => groupArray(items, () => `${items.length} results`)
|
||||
);
|
||||
}
|
||||
|
||||
private toItems<T extends Item>(
|
||||
ids: string[],
|
||||
selector: FilteredSelector<T>
|
||||
selector: FilteredSelector<T>,
|
||||
sortOptions?: SortOptions
|
||||
) {
|
||||
if (!ids.length) return [];
|
||||
return selector.items(ids);
|
||||
// if (sortOptions?.sortBy === "relevance") sortOptions = undefined;
|
||||
return selector.items(ids, sortOptions);
|
||||
}
|
||||
|
||||
async rebuild() {
|
||||
|
||||
@@ -90,4 +90,16 @@ export default class Subscriptions {
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
async redeemCode(code: string) {
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
return http.post.json(
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/redeem`,
|
||||
{
|
||||
code
|
||||
},
|
||||
token
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,17 +136,39 @@ class UserManager {
|
||||
hashedPassword = await this.db.storage().hash(password, email);
|
||||
}
|
||||
try {
|
||||
let usesFallback = false;
|
||||
await this.tokenManager.saveToken(
|
||||
await http.post(
|
||||
`${constants.AUTH_HOST}${ENDPOINTS.token}`,
|
||||
{
|
||||
grant_type: "mfa_password",
|
||||
client_id: "notesnook",
|
||||
scope: "notesnook.sync offline_access IdentityServerApi",
|
||||
password: hashedPassword
|
||||
},
|
||||
token.access_token
|
||||
)
|
||||
await http
|
||||
.post(
|
||||
`${constants.AUTH_HOST}${ENDPOINTS.token}`,
|
||||
{
|
||||
grant_type: "mfa_password",
|
||||
client_id: "notesnook",
|
||||
scope: "notesnook.sync offline_access IdentityServerApi",
|
||||
password: hashedPassword
|
||||
},
|
||||
token.access_token
|
||||
)
|
||||
.catch(async (e) => {
|
||||
if (e instanceof Error && e.message === "Password is incorrect.") {
|
||||
hashedPassword = await this.db
|
||||
.storage()
|
||||
.hash(password, email, { usesFallback: true });
|
||||
if (hashedPassword === null) return Promise.reject(e);
|
||||
usesFallback = true;
|
||||
return await http.post(
|
||||
`${constants.AUTH_HOST}${ENDPOINTS.token}`,
|
||||
{
|
||||
grant_type: "mfa_password",
|
||||
client_id: "notesnook",
|
||||
scope: "notesnook.sync offline_access IdentityServerApi",
|
||||
password: hashedPassword
|
||||
},
|
||||
token.access_token
|
||||
);
|
||||
}
|
||||
return Promise.reject(e);
|
||||
})
|
||||
);
|
||||
|
||||
const user = await this.fetchUser();
|
||||
@@ -157,10 +179,17 @@ class UserManager {
|
||||
await this.db.syncer.devices.register();
|
||||
}
|
||||
|
||||
await this.db.storage().deriveCryptoKey({
|
||||
password,
|
||||
salt: user.salt
|
||||
});
|
||||
if (usesFallback) {
|
||||
await this.db.storage().deriveCryptoKeyFallback({
|
||||
password,
|
||||
salt: user.salt
|
||||
});
|
||||
} else {
|
||||
await this.db.storage().deriveCryptoKey({
|
||||
password,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
EV.publish(EVENTS.userLoggedIn, user);
|
||||
} catch (e) {
|
||||
await this.tokenManager.saveToken(token);
|
||||
@@ -301,7 +330,11 @@ class UserManager {
|
||||
|
||||
await http.post(
|
||||
`${constants.API_HOST}${ENDPOINTS.deleteUser}`,
|
||||
{ password: await this.db.storage().hash(password, user.email) },
|
||||
{
|
||||
password: await this.db.storage().hash(password, user.email, {
|
||||
usesFallback: await this.usesFallbackPWHash(password)
|
||||
})
|
||||
},
|
||||
token
|
||||
);
|
||||
await this.logout(false, "Account deleted.");
|
||||
@@ -450,16 +483,13 @@ class UserManager {
|
||||
{
|
||||
type: "change_email",
|
||||
new_email: newEmail,
|
||||
password: await this.db.storage().hash(password, email),
|
||||
password: await this.db.storage().hash(password, email, {
|
||||
usesFallback: await this.usesFallbackPWHash(password)
|
||||
}),
|
||||
verification_code: code
|
||||
},
|
||||
token
|
||||
);
|
||||
|
||||
await this.db.storage().deriveCryptoKey({
|
||||
password,
|
||||
salt: user.salt
|
||||
});
|
||||
}
|
||||
|
||||
recoverAccount(email: string) {
|
||||
@@ -510,6 +540,11 @@ class UserManager {
|
||||
|
||||
if (data.encryptionKey) await this.db.sync({ type: "fetch", force: true });
|
||||
|
||||
if (old_password)
|
||||
old_password = await this.db.storage().hash(old_password, email, {
|
||||
usesFallback: await this.usesFallbackPWHash(old_password)
|
||||
});
|
||||
|
||||
await this.db.storage().deriveCryptoKey({
|
||||
password: new_password,
|
||||
salt
|
||||
@@ -528,8 +563,6 @@ class UserManager {
|
||||
await this.updateUser({ attachmentsKey: user.attachmentsKey });
|
||||
}
|
||||
|
||||
if (old_password)
|
||||
old_password = await this.db.storage().hash(old_password, email);
|
||||
if (new_password)
|
||||
new_password = await this.db.storage().hash(new_password, email);
|
||||
|
||||
@@ -545,6 +578,30 @@ class UserManager {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async usesFallbackPWHash(password: string) {
|
||||
const user = await this.getUser();
|
||||
const encryptionKey = await this.getEncryptionKey();
|
||||
if (!user || !encryptionKey) return false;
|
||||
const fallbackCryptoKey = await this.db
|
||||
.storage()
|
||||
.generateCryptoKeyFallback(password, user.salt);
|
||||
if (!fallbackCryptoKey) return false;
|
||||
const cryptoKey = await this.db
|
||||
.storage()
|
||||
.generateCryptoKey(password, user.salt);
|
||||
|
||||
if (!encryptionKey.key || !fallbackCryptoKey.key || !cryptoKey.key)
|
||||
throw new Error("Failed to generate crypto keys.");
|
||||
|
||||
if (
|
||||
fallbackCryptoKey.key !== encryptionKey.key &&
|
||||
cryptoKey.key !== encryptionKey.key
|
||||
)
|
||||
throw new Error("Wrong password.");
|
||||
|
||||
return fallbackCryptoKey.key === encryptionKey.key;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserManager;
|
||||
|
||||
@@ -402,7 +402,9 @@ export class FilteredSelector<T extends Item> {
|
||||
return (
|
||||
await this.filter
|
||||
.$if(!!sortOptions, (eb) =>
|
||||
eb.$call(this.buildSortExpression(sortOptions!))
|
||||
eb.$call(
|
||||
this.buildSortExpression({ ...sortOptions!, groupBy: "none" })
|
||||
)
|
||||
)
|
||||
.select("id")
|
||||
.execute()
|
||||
@@ -414,7 +416,7 @@ export class FilteredSelector<T extends Item> {
|
||||
return (await this.filter
|
||||
.$if(!!ids && ids.length > 0, (eb) => eb.where("id", "in", ids!))
|
||||
.$if(!!sortOptions, (eb) =>
|
||||
eb.$call(this.buildSortExpression(sortOptions!))
|
||||
eb.$call(this.buildSortExpression({ ...sortOptions!, groupBy: "none" }))
|
||||
)
|
||||
.$if(this._fields.length === 0, (eb) => eb.selectAll())
|
||||
.$if(this._fields.length > 0, (eb) => eb.select(this._fields))
|
||||
@@ -528,7 +530,9 @@ export class FilteredSelector<T extends Item> {
|
||||
if (options.groupBy === "abc") fields.push("title");
|
||||
else if (options.sortBy === "title" && options.groupBy !== "none")
|
||||
fields.push("dateCreated");
|
||||
else if (options.sortBy !== "dueDate") fields.push(options.sortBy);
|
||||
else if (options.sortBy !== "dueDate")
|
||||
// && options.sortBy !== "relevance")
|
||||
fields.push(options.sortBy);
|
||||
|
||||
return Array.from(
|
||||
groupArray(
|
||||
@@ -550,7 +554,7 @@ export class FilteredSelector<T extends Item> {
|
||||
() => this.ids(options),
|
||||
async (start, end) => {
|
||||
const items = (await this.filter
|
||||
.$call(this.buildSortExpression(options))
|
||||
.$call(this.buildSortExpression({ ...options, groupBy: "none" }))
|
||||
.offset(start)
|
||||
.limit(end - start)
|
||||
.selectAll()
|
||||
@@ -596,18 +600,13 @@ export class FilteredSelector<T extends Item> {
|
||||
}
|
||||
}
|
||||
|
||||
private buildSortExpression(
|
||||
options: GroupOptions | SortOptions,
|
||||
hasDueDate?: boolean
|
||||
) {
|
||||
private buildSortExpression(options: GroupOptions, hasDueDate?: boolean) {
|
||||
sanitizeSortOptions(this.type, options);
|
||||
|
||||
const sortBy: Set<SortOptions["sortBy"]> = new Set();
|
||||
if (isGroupOptions(options)) {
|
||||
if (options.groupBy === "abc") sortBy.add("title");
|
||||
else if (options.sortBy === "title" && options.groupBy !== "none")
|
||||
sortBy.add("dateCreated");
|
||||
}
|
||||
if (options.groupBy === "abc") sortBy.add("title");
|
||||
else if (options.sortBy === "title" && options.groupBy !== "none")
|
||||
sortBy.add("dateCreated");
|
||||
sortBy.add(options.sortBy);
|
||||
|
||||
return <T>(
|
||||
@@ -644,7 +643,8 @@ export class FilteredSelector<T extends Item> {
|
||||
(qb) => qb.parens(createUpcomingReminderTimeQuery()),
|
||||
options.sortDirection
|
||||
);
|
||||
} else qb = qb.orderBy(item, options.sortDirection);
|
||||
} // if (item !== "relevance")
|
||||
else qb = qb.orderBy(item, options.sortDirection);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,10 +58,20 @@ export interface IStorage {
|
||||
items: Cipher<"base64">[]
|
||||
): Promise<string[]>;
|
||||
deriveCryptoKey(credentials: SerializedKey): Promise<void>;
|
||||
hash(password: string, email: string): Promise<string>;
|
||||
hash(
|
||||
password: string,
|
||||
email: string,
|
||||
options?: { usesFallback?: boolean }
|
||||
): Promise<string>;
|
||||
getCryptoKey(): Promise<string | undefined>;
|
||||
generateCryptoKey(password: string, salt?: string): Promise<SerializedKey>;
|
||||
|
||||
generateCryptoKeyFallback(
|
||||
password: string,
|
||||
salt?: string
|
||||
): Promise<SerializedKey>;
|
||||
deriveCryptoKeyFallback(credentials: SerializedKey): Promise<void>;
|
||||
|
||||
// async generateRandomKey() {
|
||||
// const passwordBytes = randomBytes(124);
|
||||
// const password = passwordBytes.toString("base64");
|
||||
|
||||
@@ -536,7 +536,7 @@ export type User = {
|
||||
cancelURL: string | null;
|
||||
expiry: number;
|
||||
productId: string;
|
||||
provider: 0 | 1 | 2 | 3;
|
||||
provider: 0 | 1 | 2 | 3 | 4;
|
||||
start: number;
|
||||
type: 0 | 1 | 2 | 5 | 6 | 7;
|
||||
updateURL: string | null;
|
||||
|
||||
@@ -18,7 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isReminderActive } from "../collections/reminders.js";
|
||||
import { GroupHeader, GroupOptions, ItemType, Reminder } from "../types.js";
|
||||
import {
|
||||
GroupHeader,
|
||||
GroupOptions,
|
||||
ItemType,
|
||||
Reminder,
|
||||
SortOptions
|
||||
} from "../types.js";
|
||||
import { getWeekGroupFromTimestamp, MONTHS_FULL } from "../utils/date.js";
|
||||
|
||||
type PartialGroupableItem = {
|
||||
@@ -33,7 +39,7 @@ type PartialGroupableItem = {
|
||||
export type GroupKeySelectorFunction<T> = (item: T) => string;
|
||||
|
||||
export const getSortValue = (
|
||||
options: GroupOptions | undefined,
|
||||
options: SortOptions | undefined,
|
||||
item: PartialGroupableItem
|
||||
) => {
|
||||
if (
|
||||
@@ -53,7 +59,7 @@ export const getSortValue = (
|
||||
};
|
||||
|
||||
export function getSortSelectors<T extends PartialGroupableItem>(
|
||||
options: GroupOptions
|
||||
options: SortOptions
|
||||
) {
|
||||
if (options.sortBy === "title")
|
||||
return {
|
||||
|
||||
@@ -230,6 +230,9 @@ export function TaskListComponent(
|
||||
ref={forwardRef}
|
||||
dir={textDirection}
|
||||
contentEditable={editor.isEditable && !readonly}
|
||||
onPaste={(e) => {
|
||||
if (readonly) e.preventDefault();
|
||||
}}
|
||||
sx={{
|
||||
ul: {
|
||||
display: "block",
|
||||
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
findParentNodeClosestToPos,
|
||||
getExactChangedNodes
|
||||
} from "../../utils/prosemirror.js";
|
||||
import { countCheckedItems, findRootTaskList, toggleChildren } from "./utils.js";
|
||||
import {
|
||||
countCheckedItems,
|
||||
findRootTaskList,
|
||||
toggleChildren
|
||||
} from "./utils.js";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { TaskItemNode } from "../task-item/index.js";
|
||||
|
||||
@@ -49,7 +53,16 @@ export const TaskListNode = TaskList.extend({
|
||||
return {
|
||||
stats: {
|
||||
default: { checked: 0, total: 0 },
|
||||
rendered: false
|
||||
rendered: false,
|
||||
parseHTML: (element) => {
|
||||
// do not update stats for nested task lists
|
||||
if (!!element.closest("ul")) return { checked: 0, total: 0 };
|
||||
const total = element.querySelectorAll("li.checklist--item").length;
|
||||
const checked = element.querySelectorAll(
|
||||
"li.checklist--item.checked"
|
||||
).length;
|
||||
return { checked, total };
|
||||
}
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
@@ -222,26 +235,6 @@ export const TaskListNode = TaskList.extend({
|
||||
// the task list.
|
||||
new Plugin({
|
||||
key: new PluginKey("task-list-state-management"),
|
||||
view(view) {
|
||||
const { tr } = view.state;
|
||||
tr.doc.descendants((node, pos) => {
|
||||
if (node.type.name === TaskList.name) {
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
stats: countCheckedItems(node)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
tr.setMeta("preventUpdate", true);
|
||||
tr.setMeta("addToHistory", false);
|
||||
try {
|
||||
view.dispatch(tr);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return {};
|
||||
},
|
||||
appendTransaction(transactions, oldState, newState) {
|
||||
if (!transactions[0].docChanged) return;
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
useToolbarStore
|
||||
} from "./stores/toolbar-store.js";
|
||||
import { ToolbarDefinition } from "./types.js";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
|
||||
type ToolbarProps = FlexProps & {
|
||||
editor: Editor;
|
||||
@@ -90,44 +89,34 @@ export function Toolbar(props: ToolbarProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollContainer
|
||||
className="tabsScroll"
|
||||
suppressScrollY
|
||||
style={{ flex: 1 }}
|
||||
trackStyle={() => ({
|
||||
backgroundColor: "transparent",
|
||||
pointerEvents: "none"
|
||||
})}
|
||||
thumbStyle={() => ({ height: 3 })}
|
||||
<Flex
|
||||
className={["editor-toolbar", className].join(" ")}
|
||||
sx={{
|
||||
flexWrap: isMobile ? "nowrap" : "wrap",
|
||||
overflowX: isMobile ? "auto" : "hidden",
|
||||
bg: "background",
|
||||
borderRadius: isMobile ? "0px" : "default",
|
||||
...sx
|
||||
}}
|
||||
{...flexProps}
|
||||
>
|
||||
<Flex
|
||||
className={["editor-toolbar", className].join(" ")}
|
||||
sx={{
|
||||
flexWrap: isMobile ? "nowrap" : "wrap",
|
||||
bg: "background",
|
||||
borderRadius: isMobile ? "0px" : "default",
|
||||
...sx
|
||||
}}
|
||||
{...flexProps}
|
||||
>
|
||||
{toolbarTools.map((tools) => {
|
||||
return (
|
||||
<ToolbarGroup
|
||||
key={tools.join("")}
|
||||
tools={tools}
|
||||
editor={editor}
|
||||
groupId={tools.join("")}
|
||||
sx={{
|
||||
borderRight: "1px solid var(--separator)",
|
||||
":last-of-type": { borderRight: "none" },
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
<EditorFloatingMenus editor={editor} />
|
||||
</ScrollContainer>
|
||||
{toolbarTools.map((tools) => {
|
||||
return (
|
||||
<ToolbarGroup
|
||||
key={tools.join("")}
|
||||
tools={tools}
|
||||
editor={editor}
|
||||
groupId={tools.join("")}
|
||||
sx={{
|
||||
borderRight: "1px solid var(--separator)",
|
||||
":last-of-type": { borderRight: "none" },
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
<EditorFloatingMenus editor={editor} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user