Compare commits

..

1 Commits

Author SHA1 Message Date
Ammar Ahmed
6e369e35bb mobile: fix snooze time not rendered properly 2024-12-10 12:29:01 +05:00
134 changed files with 4687 additions and 5706 deletions

View File

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

View File

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

View File

@@ -17,16 +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 Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
import { SerializedKey } from "@notesnook/crypto";
import Sodium from "@ammarahmed/react-native-sodium";
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 { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
import { ToastManager } from "../../services/event-manager";
// 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
@@ -63,7 +62,6 @@ 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(
@@ -80,27 +78,27 @@ function generatePassword() {
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
const key = (await getDatabaseKey()) as string;
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
const key = getDatabaseKey();
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
const databaseKeyCipher = await encrypt(appLockCredentials, key);
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: string) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = (await decrypt(
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
)) as string;
);
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
@@ -112,16 +110,13 @@ export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
return true;
}
export async function setAppLockVerificationCipher(appLockPassword: string) {
export async function setAppLockVerificationCipher(appLockPassword) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = (await encrypt(
appLockCredentials,
generatePassword()
)) as Cipher;
const encrypted = await encrypt(appLockCredentials, generatePassword());
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
@@ -134,9 +129,9 @@ export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword: string) {
export async function validateAppLockPassword(appLockPassword) {
try {
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
@@ -151,18 +146,17 @@ export async function validateAppLockPassword(appLockPassword: string) {
}
}
let DB_KEY: string | undefined;
let DB_KEY;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword?: string) {
export async function getDatabaseKey(appLockPassword) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher: Cipher =
CipherStorage.getMap("databaseKeyCipher");
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
@@ -178,12 +172,13 @@ export async function getDatabaseKey(appLockPassword?: string) {
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
let credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
KEYSTORE_CONFIG
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
DB_KEY = credentials.password;
}
}
@@ -195,7 +190,7 @@ export async function getDatabaseKey(appLockPassword?: string) {
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key as string;
DB_KEY = derivedDatabaseKey.key;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
@@ -207,17 +202,18 @@ export async function getDatabaseKey(appLockPassword?: string) {
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
"notesnook",
KEYSTORE_CONFIG
);
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
const userKeyCipher = 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");
@@ -227,84 +223,45 @@ export async function getDatabaseKey(appLockPassword?: string) {
return DB_KEY;
} catch (e) {
ToastManager.error(e as Error, "Error getting database key");
ToastManager.error(e, "Error getting database key");
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKeyFallback(data: SerializedKey) {
if (Platform.OS !== "ios") return;
export async function deriveCryptoKey(data) {
try {
if (!data.password || !data.salt)
throw new Error(
"Invalid password and salt provided to deriveCryptoKeyFallback"
);
const credentials = await Sodium.deriveKeyFallback?.(
data.password,
data.salt
let credentials = await Sodium.deriveKey(data.password, data.salt);
const userKeyCipher = await encrypt(
{
key: await getDatabaseKey(),
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key
);
if (!credentials) return;
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 fallback stored: ", {
userKeyCipher: !!userKeyCipher
});
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function deriveCryptoKey(data: SerializedKey) {
export async function getCryptoKey(_name) {
try {
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
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 undefined;
return null;
}
const key = await decrypt(
{
key: (await getDatabaseKey()) as string,
key: await getDatabaseKey(),
salt: keyCipher.salt
},
keyCipher
@@ -317,7 +274,7 @@ export async function getCryptoKey() {
}
}
export async function removeCryptoKey() {
export async function removeCryptoKey(_name) {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
@@ -327,79 +284,44 @@ export async function removeCryptoKey() {
}
}
export async function getRandomBytes(length: number) {
export async function getRandomBytes(length) {
return await generateSecureRandom(length);
}
export async function hash(
password: string,
email: string,
options?: { usesFallback?: boolean }
) {
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
export async function hash(password, email) {
let result = await Sodium.hashPassword(password, email);
return result;
}
if (options?.usesFallback && Platform.OS !== "ios") {
return "";
export async function generateCryptoKey(password, salt) {
try {
let credentials = await Sodium.deriveKey(password, salt || null);
return credentials;
} catch (e) {
DatabaseLogger.error(e);
}
return (
options?.usesFallback
? await Sodium.hashPasswordFallback?.(password, email)
: await Sodium.hashPassword(password, email)
) as string;
}
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) {
export function getAlgorithm(base64Variant) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
const _data = { ...data };
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 };
_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: Password,
data: Cipher<"base64">[]
) {
export async function decryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
data = data.map((d) => {
d.output = "plain";
return d;
@@ -409,30 +331,10 @@ export async function decryptMulti(
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: string) {
export function parseAlgorithm(alg) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
@@ -444,11 +346,16 @@ export function parseAlgorithm(alg: string) {
};
}
export async function encrypt(password: SerializedKey, plainText: string) {
const result = await Sodium.encrypt<"base64">(password, {
export async function encrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let message = {
type: "plain",
data: plainText
});
data: data
};
let result = await Sodium.encrypt(password, message);
return {
...result,
@@ -456,13 +363,14 @@ export async function encrypt(password: SerializedKey, plainText: string) {
};
}
export async function encryptMulti(
password: SerializedKey,
plainText: string[]
) {
const results = await Sodium.encryptMulti<"base64">(
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(
password,
plainText.map((item) => ({
data.map((item) => ({
type: "plain",
data: item
}))

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ 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";
@@ -24,12 +25,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 { presentSheet, ToastManager } from "../../services/event-manager";
import { ToastManager, presentSheet } 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";
@@ -50,23 +51,23 @@ export async function downloadAllAttachments() {
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachmentIds
* @param {string[]} attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(attachmentIds: string[]) {
export async function downloadAttachments(attachments) {
await createCacheDir();
if (!attachmentIds || !attachmentIds.length) return;
if (!attachments || !attachments.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
const file = await ScopedStorage.openDocumentTree(true);
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
@@ -82,7 +83,7 @@ export async function downloadAttachments(attachmentIds: string[]) {
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,
@@ -96,21 +97,19 @@ export async function downloadAttachments(attachmentIds: string[]) {
}
};
for (let i = 0; i < attachmentIds.length; i++) {
for (let i = 0; i < attachments.length; i++) {
if (isCancelled()) return;
const attachment = await db.attachments.attachment(attachmentIds[i]);
if (!attachment) continue;
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
try {
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: i + 1,
total: attachmentIds.length,
total: attachments.length,
filename: attachment.hash
});
// Download to cache
const uri = await downloadAttachment(hash, false, {
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
@@ -185,7 +184,7 @@ export async function downloadAttachments(attachmentIds: string[]) {
});
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e as Error, "Error zipping attachments");
ToastManager.error(e, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -195,44 +194,38 @@ export async function downloadAttachments(attachmentIds: string[]) {
}
export default async function downloadAttachment(
hashOrId: string,
hash,
global = true,
options?: {
silent?: boolean;
cache?: boolean;
throwError?: boolean;
groupId?: string;
base64?: boolean;
text?: boolean;
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
}
) {
await createCacheDir();
const attachment = await db.attachments.attachment(hashOrId);
let attachment = await db.attachments.attachment(hash);
if (!attachment) {
DatabaseLogger.log("Attachment not found", {
hash: hashOrId
});
DatabaseLogger.log("Attachment not found");
return;
}
let folder: {
uri: string;
} | null = null;
if (!options?.cache) {
let folder = {};
if (!options.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder = {
uri: await filesystem.checkAndCreateDir("/downloads/")
};
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
}
try {
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
groupId: options.groupId || attachment.hash,
current: 0,
total: 1,
filename: attachment.filename
@@ -241,13 +234,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,
@@ -259,7 +252,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,
@@ -267,16 +260,14 @@ export default async function downloadAttachment(
);
}
const filename = await getFileNameWithExtension(
let filename = await getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
const key = await db.attachments.decryptKey(attachment.key);
let key = await db.attachments.decryptKey(attachment.key);
if (!key) return;
const info = {
let info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
@@ -284,18 +275,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),
@@ -303,15 +294,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 ? "global" : attachment.hash,
context: global ? null : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
@@ -328,7 +319,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,
@@ -336,7 +327,7 @@ export default async function downloadAttachment(
});
DatabaseLogger.error(e);
useAttachmentStore.getState().remove(attachment.hash);
if (options?.throwError) {
if (options.throwError) {
throw e;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,15 +68,11 @@ export const ChangePassword = () => {
}
setLoading(true);
try {
const result = await BackupService.run(
false,
"change-password-dialog",
"partial"
);
if (result.error) {
const result = await BackupService.run(false, "change-password-dialog");
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(),
@@ -88,7 +84,6 @@ export const ChangePassword = () => {
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
console.log(e.stack);
setLoading(false);
ToastManager.show({
heading: strings.passwordChangeFailed(),

View File

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

View File

@@ -179,7 +179,6 @@ const ColorPicker = ({
title: title.current,
colorCode: selectedColor
});
if (!id) return;
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
setVisible(false);

View File

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

View File

@@ -128,6 +128,7 @@ 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,
@@ -137,7 +138,7 @@ const Intro = ({ navigation }) => {
marginTop: deviceMode !== "mobile" ? 50 : null,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
minHeight: height * 0.7 - (insets.top + insets.bottom)
minHeight: height * 0.7
}}
>
<SwiperFlatList

View File

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

View File

@@ -297,6 +297,7 @@ function getDate(item: Item, groupType?: GroupingKey): number {
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},

View File

@@ -129,10 +129,7 @@ export const PricingPlans = ({
if (code.startsWith("com.streetwriters.notesnook")) {
skuId = code;
} else {
skuId = await db.offers?.getCode(
code.split(":")[0],
Platform.OS as "ios" | "android"
);
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
}
const products = await PremiumService.getProducts();

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef } from "react";
import {
@@ -47,6 +46,7 @@ import { MoveNotebookSheet } from "../sheets/move-notebook";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import { strings } from "@notesnook/intl";
export const SelectionHeader = React.memo(
({
@@ -103,11 +103,12 @@ export const SelectionHeader = React.memo(
const deleteItem = async () => {
if (!type) return;
presentDialog({
title: strings.doActions.delete.unknown(type, selectedItemsList.length),
paragraph: strings.actionConfirmations.delete.unknown(
type,
selectedItemsList.length
),
title: strings.doActions.delete[
type as keyof typeof strings.doActions.delete
](selectedItemsList.length),
paragraph: strings.actionConfirmations.delete[
type as keyof typeof strings.doActions.delete
](selectedItemsList.length),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: async () => {
@@ -296,35 +297,15 @@ export const SelectionHeader = React.memo(
{
title: strings.moveToTrash(),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
},
visible: type === "note" || type === "notebook",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" && type !== "note" && type !== "notebook",
visible: type !== "trash",
icon: "delete"
},
{

View File

@@ -134,10 +134,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
) : (
<>
<Input
key="code-input"
fwdRef={codeInputRef}
placeholder={strings.code()}
defaultValue=""
placeholder={strings.verifyNewEmail()}
onChangeText={(code) => {
emailChangeData.current.code = code;
}}

View File

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

View File

@@ -150,7 +150,7 @@ const PublishNoteSheet = ({
</View>
) : (
<>
{isPublished && publishUrl ? (
{isPublished && (
<View
style={{
flexDirection: "row",
@@ -204,7 +204,7 @@ const PublishNoteSheet = ({
name="content-copy"
/>
</View>
) : null}
)}
<TouchableOpacity
onPress={() => {

View File

@@ -17,17 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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,
@@ -35,6 +30,8 @@ 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";
@@ -44,6 +41,9 @@ 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 filesystem.checkAndCreateDir("/");
path = await Storage.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 filesystem.checkAndCreateDir("/");
path = await Storage.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, this.state.key, "utf8");
path = path + fileName;
}

View File

@@ -556,9 +556,9 @@ export default function ReminderSheet({
{Object.keys(ReminderNotificationModes).map((mode) => (
<Button
key={mode}
title={strings.reminderNotificationModes(
mode as keyof typeof ReminderNotificationModes
)}
title={strings.reminderNotificationModes[
mode as keyof typeof strings.reminderNotificationModes
]()}
style={{
marginRight: 12,
borderRadius: 100

View File

@@ -0,0 +1,158 @@
/*
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>
);
};

View File

@@ -18,15 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { DimensionValue, View } from "react-native";
import { View } from "react-native";
import { SvgXml } from "./lazy";
export const SvgView = ({
width = 250,
height = 250,
src
}: {
width?: DimensionValue;
height?: DimensionValue;
width?: number | string;
height?: number | number;
src?: string;
}) => {
if (!src) return null;

View File

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

View File

@@ -17,15 +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 {
EV,
EVENTS,
EventManagerSubscription,
SYNC_CHECK_IDS,
SyncStatusEvent,
User
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { User } from "@notesnook/core";
import { EV, EVENTS, SYNC_CHECK_IDS, SyncStatusEvent } from "@notesnook/core";
import { EventManagerSubscription } from "@notesnook/core";
import notifee from "@notifee/react-native";
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
import React, { useCallback, useEffect, useRef } from "react";
@@ -47,7 +41,6 @@ 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";
@@ -59,7 +52,6 @@ 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,
@@ -74,17 +66,14 @@ 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 { clearAllStores, initAfterSync } from "../stores";
import { refreshAllStores } from "../stores/create-db-collection-store";
import { initAfterSync } from "../stores";
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";
@@ -100,8 +89,11 @@ import {
} from "../utils/events";
import { getGithubVersion } from "../utils/github-version";
import { tabBarRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
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";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -148,7 +140,6 @@ const onUploadedAttachmentProgress = (data: any) => {
};
const onUserSessionExpired = async () => {
console.log("LOGGED OUT USER....");
SettingsService.set({
sessionExpired: true
});
@@ -217,19 +208,11 @@ const onRequestPartialSync = async (
};
const onLogout = async (reason: string) => {
DatabaseLogger.log("User Logged Out " + reason);
setLoginMessage();
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
clearAllStores();
setImmediate(() => {
refreshAllStores();
DatabaseLogger.log("User Logged Out" + reason);
Notifications.setupReminders(true);
SettingsService.set({
introCompleted: true
});
Navigation.queueRoutesForUpdate();
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
};
async function checkForShareExtensionLaunchedInBackground() {
@@ -497,7 +480,7 @@ export const useAppEvents = () => {
}
if (fullBackup) {
await BackupService.run(false, undefined, "full");
await BackupService.run(true, undefined, "full");
}
}
@@ -598,8 +581,7 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.migrationStarted, (name) => {
if (
name !== "notesnook" ||
!SettingsService.getProperty("introCompleted") ||
Config.isTesting === "true"
!SettingsService.getProperty("introCompleted")
)
return;
startProgress({
@@ -612,8 +594,7 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.migrationFinished, (name) => {
if (
name !== "notesnook" ||
!SettingsService.getProperty("introCompleted") ||
Config.isTesting === "true"
!SettingsService.getProperty("introCompleted")
)
return;
endProgress();

View File

@@ -32,11 +32,18 @@ export function useAppState() {
const subscription = AppState.addEventListener("change", onChange);
return () => {
subscription.remove();
// @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);
}
};
}, []);
return appState;
}
export type { AppStateStatus };
export { AppStateStatus };

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { Keyboard, KeyboardEventListener, KeyboardMetrics } from "react-native";
import { Keyboard, KeyboardEventListener, ScreenRect } 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 | KeyboardMetrics;
end: KeyboardMetrics;
start: undefined | ScreenRect;
end: ScreenRect;
}>(initialValue);
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);

View File

@@ -97,7 +97,7 @@ const _TabsHolder = () => {
tabBarRef.current?.goToPage(1, false);
return;
}
if (item && item.type === "notesnook.action.newnote") {
if (item.type === "notesnook.action.newnote") {
clearAppState();
if (!tabBarRef.current) {
await sleep(3000);

View File

@@ -39,7 +39,11 @@
"@lingui/react": "4.11.2",
"@lingui/core": "4.11.2",
"react-native-check-version": "^1.3.0",
"react-native-material-menu": "^2.0.0"
"react-native-material-menu": "^2.0.0",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.36.1"
},
"sideEffects": false
}

View File

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

View File

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

View File

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

View File

@@ -59,38 +59,15 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
}
try {
setLoading(true);
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;
}
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();
console.log(
`Found ${results.placeholders?.length} results for ${query}`

View File

@@ -17,9 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { 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,
@@ -29,11 +26,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 filesystem from "../../common/filesystem";
import Storage from "../../common/database/storage";
import DialogHeader from "../../components/dialog/dialog-header";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-button";
@@ -49,10 +46,13 @@ 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 filesystem.checkAndCreateDir("/");
path = await Storage.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(
path + fileName,
codeString,

View File

@@ -17,24 +17,24 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { 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 { LogMessage } from "@notesnook/logger";
import { format, LogLevel, logManager } from "@notesnook/core";
import React, { useEffect, useRef, useState } from "react";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import filesystem from "../../common/filesystem";
import RNFetchBlob from "react-native-blob-util";
import Storage from "../../common/database/storage";
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 filesystem.checkAndCreateDir("/");
path = await Storage.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName + ".txt", data, "utf8");
path = path + fileName;
}

View File

@@ -45,7 +45,7 @@ const Home = ({
});
const renderItem = ({ item }: { item: SettingSection; index: number }) =>
item.id === "account" ? (
item.name === "account" ? (
<SettingsUserSection item={item} />
) : (
<SectionGroup item={item} />

View File

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

View File

@@ -28,7 +28,7 @@ import DocumentPicker from "react-native-document-picker";
import * as ScopedStorage from "react-native-scoped-storage";
import { unzip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import storage from "../../../common/database/storage";
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 filesystem.checkAndCreateDir("/backups/");
const path = await storage.checkAndCreateDir("/backups/");
files = await RNFetchBlob.fs.lstat(path);
}
files = files

View File

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

View File

@@ -18,10 +18,8 @@ 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";
@@ -30,6 +28,7 @@ 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";
@@ -55,11 +54,14 @@ 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";
@@ -75,6 +77,9 @@ 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[] = [
{
@@ -114,10 +119,6 @@ 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
@@ -133,39 +134,6 @@ 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",
@@ -479,6 +447,18 @@ 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);
@@ -1194,12 +1174,7 @@ export const settingsGroups: SettingSection[] = [
{
id: "select-backup-dir",
name: strings.selectBackupDir(),
description: () => {
const desc = strings.selectBackupDirDesc(
SettingsService.get().backupDirectoryAndroid?.path || ""
);
return desc[0] + " " + desc[1];
},
description: strings.selectBackupDirDesc(),
icon: "folder",
hidden: () =>
!!SettingsService.get().backupDirectoryAndroid ||

View File

@@ -126,19 +126,18 @@ 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",
width: "100%",
paddingHorizontal: 0
borderRadius: 100
}}
fontSize={SIZE.xs}
fontSize={SIZE.sm}
height={30}
type="secondaryAccented"
/>

View File

@@ -19,7 +19,6 @@ 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";
@@ -27,14 +26,20 @@ 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 filesystem, { FileStorage } from "../common/filesystem";
import storage from "../common/database/storage";
import filesystem from "../common/filesystem";
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
import { presentDialog } from "../components/dialog/functions";
import { endProgress, updateProgress } from "../components/dialogs/progress";
import {
endProgress,
startProgress,
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;
@@ -85,13 +90,9 @@ async function checkBackupDirExists(reset = false, context = "global") {
resolve(await getDirectoryAndroid());
return;
}
const desc = strings.selectBackupDirDesc(
SettingsService.get().backupDirectoryAndroid?.path || ""
);
presentDialog({
title: strings.selectBackupDir(),
paragraph: desc[0] + " " + desc,
paragraph: strings.selectBackupDirDesc(),
positivePress: async () => {
resolve(await getDirectoryAndroid());
},
@@ -177,7 +178,7 @@ async function run(
let path;
if (Platform.OS === "ios") {
path = await filesystem.checkAndCreateDir("/backups");
path = await storage.checkAndCreateDir("/backups");
}
const backupFileName = sanitizeFilename(
@@ -231,7 +232,7 @@ async function run(
updateProgress({
progress: `Saving attachments in backup... ${file.hash}`
});
if (await FileStorage.exists(file.hash)) {
if (await filesystem.exists(file.hash)) {
await RNFetchBlob.fs.cp(
`${cacheDir}/${file.hash}`,
`${attachmentsDir}/${file.hash}`
@@ -298,7 +299,7 @@ async function run(
path: path
};
} catch (e) {
ToastManager.error(e as Error, strings.backupFailed(), context || "global");
ToastManager.error(e, strings.backupFailed(), context || "global");
if (
(e as Error)?.message?.includes("android.net.Uri") &&

View File

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

View File

@@ -23,6 +23,7 @@ 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,
@@ -30,13 +31,13 @@ import {
ExportableNote,
exportNotes
} from "@notesnook/common";
import { FilteredSelector, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { basename, dirname, extname, join } from "pathe";
import filesystem from "../common/filesystem";
import { Note } from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core";
import { basename, dirname, join, extname } from "pathe";
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",
@@ -48,7 +49,7 @@ const FolderNames: { [name: string]: string } = {
async function getPath(type: string) {
let path =
Platform.OS === "ios" &&
(await filesystem.checkAndCreateDir(`/exported/${type}/`));
(await Storage.checkAndCreateDir(`/exported/${type}/`));
if (Platform.OS === "android") {
const file = await ScopedStorage.openDocumentTree(true);

View File

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

View File

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

View File

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

View File

@@ -70,7 +70,7 @@
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/new_note_widget_info" />
android:resource="@xml/note_widget_info" />
</receiver>
<activity
@@ -129,16 +129,6 @@
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter android:label="Make Note">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="image/*" />
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter android:label="Make Note">
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />

View File

@@ -5,7 +5,6 @@ import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.widget.RemoteViews;
/**
@@ -25,29 +24,21 @@ public class NoteWidget extends AppWidgetProvider {
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
updateAppWidget(context, appWidgetManager, appWidgetId, newOptions);
}
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle options) {
int minWidth = options != null ? options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) : 0;
// int minHeight = options != null ? options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) : 0;
int layoutId = (minWidth < 100) ? R.layout.note_widget_icon : R.layout.note_widget;
RemoteViews views = new RemoteViews(context.getPackageName(), layoutId);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
updateAppWidget(context, appWidgetManager, appWidgetId,null);
}
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
}

View File

@@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="@color/text"
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,16h-3v3h-2v-3L8,16v-2h3v-3h2v3h3v2zM13,9L13,3.5L18.5,9L13,9z"/>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/background"/>
<solid android:color="#CCFFFFFF"/>
<stroke android:width="0dp" android:color="#B1BCBE" />
<corners android:radius="10dp"/>
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />

View File

@@ -6,6 +6,7 @@
android:padding="@dimen/widget_margin"
android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/widget_button"
@@ -17,21 +18,21 @@
android:elevation="5dp"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/add_note"
android:contentDescription="New note icon" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:textSize="16sp"
android:text="Take a quick note." />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:textColor="@color/text"
android:textSize="16sp"
android:text="@string/take_a_quick_note" />
</LinearLayout>
</RelativeLayout>

View File

@@ -1,13 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/layout_bg"
android:padding="@dimen/widget_margin">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="16dp"
android:src="@drawable/add_note" />
</FrameLayout>

View File

@@ -5,6 +5,4 @@
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
<color name="bootsplash_background">#1f1f1f</color>
<color name="background">#1D1D1D</color>
<color name="text">#B4B4B4</color>
</resources>

View File

@@ -5,6 +5,4 @@
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
<color name="bootsplash_background">#FFFFFF</color>
<color name="background">#FFFFFF</color>
<color name="text">#000000</color>
</resources>

View File

@@ -3,5 +3,4 @@
<string name="title_activity_share">NotesnookShare</string>
<string name="appwidget_text">EXAMPLE</string>
<string name="add_widget">Add widget</string>
<string name="take_a_quick_note">Take a quick note.</string>
</resources>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/note_widget"
android:initialLayout="@layout/note_widget"
android:minWidth="50dp"
android:minHeight="50dp"
android:targetCellWidth="5"
android:targetCellHeight="1"
android:previewImage="@drawable/widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"/>

View File

@@ -2,9 +2,9 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/note_widget"
android:initialLayout="@layout/note_widget"
android:minWidth="50dp"
android:minWidth="300dp"
android:minHeight="50dp"
android:previewImage="@drawable/widget_preview"
android:resizeMode="horizontal|vertical"
android:resizeMode="horizontal"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"/>
android:widgetCategory="home_screen"></appwidget-provider>

View File

@@ -1,5 +1,9 @@
- You can now share multiple files to Notesnook
- Fix file and image sharing not working
- Many other bug fixes and small improvements
- 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
Thank you for using Notesnook!

View File

@@ -5,11 +5,11 @@
<key>provisioningProfiles</key>
<dict>
<key>org.streetwriters.notesnook</key>
<string>Notesnook App Distribution 2025</string>
<string>Notesnook App Distribution 2024</string>
<key>org.streetwriters.notesnook.notewidget</key>
<string>Notesnook Widget Distribution 2025</string>
<string>Notesnook Widget Distribution 2024</string>
<key>org.streetwriters.notesnook.share</key>
<string>Notesnook Share Distribution 2025</string>
<string>Notesnook Share Distribution 2024</string>
</dict>
</dict>
</plist>

View File

@@ -1063,7 +1063,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2122;
CURRENT_PROJECT_VERSION = 2120;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1137,7 +1137,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.25;
MARKETING_VERSION = 3.0.23;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1168,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2122;
CURRENT_PROJECT_VERSION = 2120;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1242,7 +1242,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.25;
MARKETING_VERSION = 3.0.23;
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 2025";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook App Distribution 2024";
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 = 2122;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1413,7 +1413,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.25;
MARKETING_VERSION = 3.0.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1444,7 +1444,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2122;
CURRENT_PROJECT_VERSION = 2120;
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.25;
MARKETING_VERSION = 3.0.23;
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 2025";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Widget Distribution 2024";
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 = 2122;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1561,7 +1561,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.25;
MARKETING_VERSION = 3.0.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1592,7 +1592,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2122;
CURRENT_PROJECT_VERSION = 2120;
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.25;
MARKETING_VERSION = 3.0.23;
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 2025";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Share Distribution 2024";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Make Note/Make Note-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -1744,7 +1744,10 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
@@ -1808,7 +1811,10 @@
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;

View File

@@ -1019,9 +1019,9 @@ PODS:
- react-native-screenguard (1.0.0):
- React-Core
- SDWebImage (~> 5.11.1)
- react-native-share-extension (2.7.0):
- react-native-share-extension (2.6.0):
- React
- react-native-sodium (1.6.1):
- react-native-sodium (1.5.6):
- 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: 17e42444d0d9fbfeb0a7392899def70f9534c9c4
react-native-sodium: 4cb76086943a7f60c42b40ebca866695b360a196
react-native-share-extension: 25437eb1039f7409be6e80a7edf8d02b42e1dc99
react-native-sodium: 605c1523ec8ff5fbff5e9e7769bbacceb571a3c6
react-native-theme-switch-animation: d3eb50365a3829ce5572628888fa514752703f61
react-native-webview: 553abd09f58e340fdc7746c9e2ae096839e99911
React-nativeconfig: ba9a2e54e2f0882cf7882698825052793ed4c851

View File

@@ -65,15 +65,15 @@
"react-native-screenguard": "^1.0.0",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-pluralrules": "5.2.14",
"@ammarahmed/react-native-sodium": "^1.6.1",
"@ammarahmed/react-native-share-extension": "^2.6.0",
"@ammarahmed/react-native-sodium": "1.5.6",
"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",
"react-native-orientation": "github:yamill/react-native-orientation",
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
"@ammarahmed/react-native-share-extension": "^2.8.0"
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0"
},
"devDependencies": {
"detox": "^20.27.6",
@@ -88,6 +88,7 @@
"@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",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.25",
"version": "3.0.23",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -24,7 +24,6 @@
"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",
@@ -38,18 +37,12 @@
"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",
"@tanstack/react-query": "^4.36.1",
"@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"

View File

@@ -81,8 +81,7 @@ const EXTRA_ICON_NAMES = [
"notebook-plus",
"arrow-right-bold-box-outline",
"arrow-up-bold",
"login",
"gift"
"login"
];
const __filename = fileURLToPath(import.meta.url);

View File

@@ -496,7 +496,6 @@ export const Search = ({
const tagId = await db.tags.add({
title: searchKeyword
});
if (!tagId) return;
SearchSetters.selectTags(tagId);
onSearch();
checkQueryExists(searchKeyword);

View File

@@ -43,7 +43,7 @@ import { SafeAreaProvider } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import isURL from "validator/lib/isURL";
import { DatabaseLogger, db } from "../app/common/database";
import { Storage } from "../app/common/database/storage";
import Storage from "../app/common/database/storage";
import { Button } from "../app/components/ui/button";
import Heading from "../app/components/ui/typography/heading";
import Paragraph from "../app/components/ui/typography/paragraph";
@@ -620,45 +620,41 @@ const ShareView = () => {
>
Tap to remove an attachment.
</Paragraph>
{rawFiles.some((item) => isImage(item.type)) ? (
<TouchableOpacity
activeOpacity={1}
<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
style={{
flexDirection: "row",
alignSelf: "center",
alignItems: "center",
width: "100%",
marginTop: 6
}}
onPress={() => {
setCompress(!compress);
flexShrink: 1,
marginLeft: 3,
fontSize: 12
}}
>
<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}
Compress image (recommended)
</Text>
</TouchableOpacity>
</View>
) : null}
<View

View File

@@ -20,5 +20,5 @@
"maxNodeModuleJsDepth": 5,
"downlevelIteration": true
},
"exclude": ["native", "e2e"]
"exclude": ["native"]
}

View File

@@ -2928,6 +2928,22 @@
"@styled-system/css": "^5.1.5"
}
},
"node_modules/@theme-ui/color-modes": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@theme-ui/color-modes/-/color-modes-0.16.2.tgz",
"integrity": "sha512-jWEWx53lxNgWCT38i/kwLV2rsvJz8lVZgi5oImnVwYba9VejXD23q1ckbNFJHosQ8KKXY87ht0KPC6BQFIiHtQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@theme-ui/core": "^0.16.2",
"@theme-ui/css": "^0.16.2",
"deepmerge": "^4.2.2"
},
"peerDependencies": {
"@emotion/react": "^11.11.1",
"react": ">=18"
}
},
"node_modules/@theme-ui/components": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@theme-ui/components/-/components-0.16.2.tgz",
@@ -2973,6 +2989,22 @@
"@emotion/react": "^11.11.1"
}
},
"node_modules/@theme-ui/theme-provider": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@theme-ui/theme-provider/-/theme-provider-0.16.2.tgz",
"integrity": "sha512-LRnVevODcGqO0JyLJ3wht+PV3ZoZcJ7XXLJAJWDoGeII4vZcPQKwVy4Lpz/juHsZppQxKcB3U+sQDGBnP25irQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@theme-ui/color-modes": "^0.16.2",
"@theme-ui/core": "^0.16.2",
"@theme-ui/css": "^0.16.2"
},
"peerDependencies": {
"@emotion/react": "^11.11.1",
"react": ">=18"
}
},
"node_modules/@types/acorn": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz",
@@ -3102,14 +3134,14 @@
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz",
"integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -12197,7 +12229,7 @@
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

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

View File

@@ -1,4 +1,4 @@
Subtotal: ₹400
Sales tax: ₹300
Discount: -₹400
Total: ₹400
Total: ₹400/yr

View File

@@ -1,4 +1,4 @@
Subtotal: ₹400
Sales tax: ₹300
Discount: -₹400
Total: ₹400
Total: ₹400/yr

View File

@@ -1,4 +1,4 @@
Subtotal: ₹400
Sales tax: ₹300
Discount: -₹400
Total: ₹400
Total: ₹400/yr

View File

@@ -1,4 +1,4 @@
Subtotal: $200
Sales tax: $0
Discount: -$200
Total: $200
Total: $200/yr

View File

@@ -1,4 +1,4 @@
Subtotal: $200
Sales tax: $0
Discount: -$200
Total: $200
Total: $200/yr

View File

@@ -1,4 +1,4 @@
Subtotal: $200
Sales tax: $0
Discount: -$200
Total: $200
Total: $200/yr

View File

@@ -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 { getTestId, NOTE, TITLE_ONLY_NOTE } from "./utils";
import { NOTE, TITLE_ONLY_NOTE } from "./utils";
test("focus mode", async ({ page }) => {
const app = new AppModel(page);
@@ -270,103 +270,3 @@ 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());
});

View File

@@ -46,7 +46,7 @@ export class BaseItemModel {
}
async getId() {
return (await this.locator.getAttribute("id"))?.replace("id_", "");
return await this.locator.getAttribute("id");
}
async getTitle() {

View File

@@ -19,8 +19,6 @@ 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;
@@ -35,9 +33,6 @@ 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;
@@ -56,9 +51,6 @@ 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) {
@@ -235,11 +227,4 @@ 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;
}
}
}

View File

@@ -1,36 +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 { 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();
}
}

View File

@@ -20,7 +20,6 @@ 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,

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ class Vault {
subtitle: strings.deleteVaultDesc(),
inputs: {
password: {
label: strings.accountPassword(),
label: strings.password(),
autoComplete: "current-password"
}
},

View File

@@ -206,8 +206,7 @@ function AuthContainer(props) {
>
<Text variant={"subBody"}>
{version.status === "fulfilled" &&
!!version.value &&
version.value.instance !== "default" ? (
version.value?.instance !== "default" ? (
<>
{strings.usingInstance(
version.value.instance,

View File

@@ -30,7 +30,6 @@ import {
Lock,
NormalMode,
Note,
NoteRemove,
Pin,
Properties,
Publish,
@@ -45,7 +44,6 @@ import {
} from "../icons";
import { ScrollContainer } from "@notesnook/ui";
import {
SaveState,
SessionType,
isLockedSession,
useEditorStore
@@ -262,7 +260,7 @@ function TabStrip() {
style={{ flex: 1 }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
pointerEvents: "none"
})}
thumbStyle={() => ({ height: 3 })}
onWheel={(e) => {
@@ -283,7 +281,6 @@ function TabStrip() {
e.stopPropagation();
useEditorStore.getState().newSession();
}}
data-test-id="tabs"
>
<ReorderableList
items={sessions}
@@ -305,95 +302,89 @@ function TabStrip() {
sessions.splice(to, 0, fromTab);
useEditorStore.setState({ sessions });
}}
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")
}
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
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);
}
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
);
});
}}
/>
);
}}
}}
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
)
}
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>
@@ -407,7 +398,6 @@ type TabProps = {
isTemporary: boolean;
isPinned: boolean;
isLocked: boolean;
isUnsaved: boolean;
type: SessionType;
onKeepOpen: () => void;
onFocus: () => void;
@@ -427,7 +417,6 @@ function Tab(props: TabProps) {
isTemporary,
isPinned,
isLocked,
isUnsaved,
type,
onKeepOpen,
onFocus,
@@ -447,8 +436,6 @@ function Tab(props: TabProps) {
? Readonly
: type === "deleted"
? Trash
: isUnsaved
? NoteRemove
: Note;
const { attributes, listeners, setNodeRef, transform, transition, active } =
useSortable({ id });
@@ -457,7 +444,6 @@ function Tab(props: TabProps) {
<Flex
ref={setNodeRef}
className="tab"
data-test-id={`tab-${id}`}
sx={{
borderRadius: "default",
cursor: "pointer",
@@ -558,13 +544,7 @@ function Tab(props: TabProps) {
if (e.button == 0) onFocus();
}}
>
<Icon
data-test-id={`tab-icon${isUnsaved ? "-unsaved" : ""}`}
size={16}
color={
isUnsaved ? "accent-error" : isActive ? "accent-selected" : "icon"
}
/>
<Icon size={16} color={isActive ? "accent-selected" : "icon"} />
<Text
variant="body"
sx={{
@@ -610,7 +590,6 @@ function Tab(props: TabProps) {
}
}}
className="closeTabButton"
data-test-id={"tab-close-button"}
size={16}
/>
)}

View File

@@ -121,13 +121,6 @@ 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

View File

@@ -71,12 +71,9 @@ 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,
@@ -442,17 +439,9 @@ 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) => {
@@ -469,15 +458,6 @@ export function Editor(props: EditorProps) {
};
}, [id]);
useEffect(() => {
const unsub = onPageVisibilityChanged((_, hidden) => {
if (hidden) {
saveSessionContentIfNotSaved(id);
}
});
return () => unsub();
}, []);
return (
<EditorChrome {...props}>
<Tiptap
@@ -579,25 +559,6 @@ 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 : (
<>

View File

@@ -63,7 +63,6 @@ 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,
@@ -90,7 +89,6 @@ type TipTapProps = {
) => Promise<LinkAttributes | undefined>;
onAttachFile?: (file: File) => void;
onFocus?: () => void;
onAutoSaveDisabled: () => void;
content?: () => string | undefined;
readonly?: boolean;
nonce?: number;
@@ -134,11 +132,12 @@ function TipTap(props: TipTapProps) {
onInsertInternalLink,
onContentChange,
onFocus = () => {},
onAutoSaveDisabled,
content,
editorContainer,
readonly,
nonce,
isMobile,
isTablet,
downloadOptions,
fontSize,
fontFamily,
@@ -348,9 +347,6 @@ 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 () => {
@@ -370,36 +366,18 @@ function TipTap(props: TipTapProps) {
zIndex: 2
}}
>
<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>
<Toolbar
editor={editor}
location={"top"}
sx={
isTablet || isMobile
? { overflowX: "scroll", flexWrap: "nowrap" }
: {}
}
tools={toolbarConfig}
defaultFontFamily={fontFamily}
defaultFontSize={fontSize}
/>
</ScopedThemeProvider>
</>
);
@@ -602,9 +580,7 @@ function toIEditor(editor: Editor): IEditor {
},
{ query: (a) => a.hash === hash, preventUpdate: true }
),
startSearch: () => editor.commands.startSearch(),
getContent: () =>
getHTMLFromFragment(editor.state.doc.content, editor.schema)
startSearch: () => editor.commands.startSearch()
};
}

View File

@@ -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 = IS_TESTING ? 100 : 100_000;
export const MAX_AUTO_SAVEABLE_WORDS = 100_000;
export type NoteStatistics = {
words: {
@@ -39,5 +39,4 @@ export interface IEditor {
attachFile: (file: Attachment) => void;
sendAttachmentProgress: (hash: string, progress: number) => void;
startSearch: () => void;
getContent: () => string;
}

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