mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 19:19:34 +02:00
Compare commits
108 Commits
3.0.13-and
...
v3.0.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
225b21977e | ||
|
|
026b9212c4 | ||
|
|
5ce59c6125 | ||
|
|
b7c81a956c | ||
|
|
873f976fd3 | ||
|
|
7d8a16c463 | ||
|
|
f5841985fa | ||
|
|
da86cbbe32 | ||
|
|
ed115d3f9e | ||
|
|
e52e74523a | ||
|
|
7e43c3b373 | ||
|
|
e6db186b62 | ||
|
|
998e62ade7 | ||
|
|
ac16bc74fc | ||
|
|
2aee26b082 | ||
|
|
a188920d96 | ||
|
|
1f3defc28c | ||
|
|
7aeeabfb3a | ||
|
|
6641d3e625 | ||
|
|
72031617eb | ||
|
|
78090b700b | ||
|
|
f2f8d81b98 | ||
|
|
84d68ca67b | ||
|
|
6368e47712 | ||
|
|
0f17b01a74 | ||
|
|
025e23356a | ||
|
|
ac85002dff | ||
|
|
3a23e4fbaf | ||
|
|
3547663af8 | ||
|
|
c5dfc02085 | ||
|
|
bb9a6431bf | ||
|
|
624d3d7ce8 | ||
|
|
170b0aa49a | ||
|
|
07cfd74506 | ||
|
|
1d40054ac0 | ||
|
|
5cd37f4b64 | ||
|
|
25675f71e8 | ||
|
|
202534fca9 | ||
|
|
a57558d8ab | ||
|
|
f1c201dca9 | ||
|
|
a73bff83fa | ||
|
|
e4428299bc | ||
|
|
487567ffbc | ||
|
|
efd83e6de3 | ||
|
|
a07230f707 | ||
|
|
35a2e11afd | ||
|
|
cf4a7a3d34 | ||
|
|
86e661ccd6 | ||
|
|
e7be9b35ed | ||
|
|
53a367b0b2 | ||
|
|
b6206e2ca7 | ||
|
|
a910c819f2 | ||
|
|
6dd7a4960f | ||
|
|
c4e15b8f90 | ||
|
|
f458a4b86a | ||
|
|
8a44a7b99a | ||
|
|
25065c01a0 | ||
|
|
87ae9e86e7 | ||
|
|
782c53b622 | ||
|
|
cb52b390ab | ||
|
|
5a49941003 | ||
|
|
6797b9b621 | ||
|
|
031a1b31b4 | ||
|
|
4e5762be45 | ||
|
|
7a6ba5d1d1 | ||
|
|
cc3ccd041d | ||
|
|
31453dc098 | ||
|
|
2e84099c80 | ||
|
|
5bbc0cd452 | ||
|
|
22aae401ba | ||
|
|
97f135cd07 | ||
|
|
976f933233 | ||
|
|
95fe1044d6 | ||
|
|
677201e979 | ||
|
|
6ec9370afb | ||
|
|
bea0f1ab0e | ||
|
|
5357be4741 | ||
|
|
02fdec74ce | ||
|
|
dbc907ceeb | ||
|
|
7bb53a5e0c | ||
|
|
69cb24149d | ||
|
|
438d412ae9 | ||
|
|
1b71edef45 | ||
|
|
a4c0e8412b | ||
|
|
fbcb8675ca | ||
|
|
fbf5c82d04 | ||
|
|
fa40c539ed | ||
|
|
f3218fdc31 | ||
|
|
d12ed967e3 | ||
|
|
b6021dc76b | ||
|
|
f95442a828 | ||
|
|
846aae312e | ||
|
|
68fdbcb7fc | ||
|
|
b938c58b67 | ||
|
|
759fba06ae | ||
|
|
10e24ae632 | ||
|
|
341032c884 | ||
|
|
aa1fa0ca81 | ||
|
|
398432efde | ||
|
|
57ddaafc16 | ||
|
|
e4a755c69f | ||
|
|
053f70cdb4 | ||
|
|
cdcdc43cad | ||
|
|
f1111191b1 | ||
|
|
529e2a4dfd | ||
|
|
d88d07a2b0 | ||
|
|
197ff36c20 | ||
|
|
d94474de79 |
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.13",
|
||||
"version": "3.0.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.13",
|
||||
"version": "3.0.15",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.13",
|
||||
"version": "3.0.15",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,156 +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 { Platform } from "react-native";
|
||||
import { MMKV } from "../database/mmkv";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const WEBSITE_ID = "3c6890ce-8410-49d5-8831-15fb2eb28a21";
|
||||
const baseUrl = "https://analytics.streetwriters.co/api/collect";
|
||||
|
||||
const UA =
|
||||
Platform.OS === "ios"
|
||||
? "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
|
||||
: `
|
||||
Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36`;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Routes} route
|
||||
* @param {Routes[]} conditions
|
||||
* @returns
|
||||
*/
|
||||
async function canUpdateAnalytics(route, conditions = []) {
|
||||
if (!useSettingStore?.getState()?.settings?.telemetry) return false;
|
||||
let eventsList = MMKV.getString("notesnookUserEvents");
|
||||
|
||||
if (eventsList) {
|
||||
eventsList = JSON.parse(eventsList);
|
||||
}
|
||||
|
||||
if (eventsList && eventsList[route]) {
|
||||
console.log("analytics: event already sent", route);
|
||||
return false;
|
||||
}
|
||||
if (route !== "/welcome") {
|
||||
for (let cond of conditions) {
|
||||
if (!eventsList || !eventsList[cond]) {
|
||||
console.log("analytics: conditions not met for event", route, cond);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("analytics: will send event", route);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveAnalytics(route, value = true) {
|
||||
let eventsList = MMKV.getString("notesnookUserEvents");
|
||||
if (eventsList) {
|
||||
eventsList = JSON.parse(eventsList);
|
||||
} else {
|
||||
eventsList = {};
|
||||
}
|
||||
eventsList[route] = value;
|
||||
MMKV.setString("notesnookUserEvents", JSON.stringify(eventsList));
|
||||
}
|
||||
|
||||
/**
|
||||
*@typedef {"/welcome" | "/home" | "/signup" | "/first-note" | "/account-created" | "/pro-sheet" | "/pro-plans" | "/iap-native" | "/pro-screen" | "/editor" | "/editor-toolbar" | "/properties" | "/sidemenu"} Routes
|
||||
* @param {Routes} prevRoute
|
||||
* @param {Routes} route
|
||||
* @param {Routes[]} conditions
|
||||
* @param {boolean} once
|
||||
* @returns
|
||||
*/
|
||||
|
||||
async function pageView(
|
||||
route,
|
||||
prevRoute = "",
|
||||
conditions = ["/welcome"],
|
||||
once = true
|
||||
) {
|
||||
if (__DEV__) return;
|
||||
if (!(await canUpdateAnalytics(route, conditions)) && once) return;
|
||||
let body = {
|
||||
payload: {
|
||||
website: WEBSITE_ID,
|
||||
url: `notesnook-${Platform.OS}${prevRoute}${route}`,
|
||||
referrer: `https://notesnook.com/notesnook-${Platform.OS}${prevRoute}`,
|
||||
hostname: `notesnook-${Platform.OS}`,
|
||||
language: "en-US",
|
||||
screen: "1920x1080"
|
||||
},
|
||||
type: "pageview"
|
||||
};
|
||||
|
||||
try {
|
||||
let response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
console.log("analytics: event sent", route);
|
||||
await saveAnalytics(route);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEvent(type, value, once = true) {
|
||||
if (__DEV__) return;
|
||||
if (!(await canUpdateAnalytics(type)) && once) return;
|
||||
let body = {
|
||||
payload: {
|
||||
website: WEBSITE_ID,
|
||||
url: "/",
|
||||
event_type: type,
|
||||
event_value: value,
|
||||
hostname: "notesnook-android-app",
|
||||
language: "en-US",
|
||||
screen: "1920x1080"
|
||||
},
|
||||
type: "event"
|
||||
};
|
||||
|
||||
try {
|
||||
let response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
console.log(response.status);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
sendEvent,
|
||||
pageView,
|
||||
saveAnalytics
|
||||
};
|
||||
@@ -28,29 +28,7 @@ import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.43.5:5264",
|
||||
// AUTH_HOST: "http://192.168.43.5:8264",
|
||||
// SSE_HOST: "http://192.168.43.5:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
|
||||
// ISSUES_HOST: "http://192.168.43.5:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
@@ -59,6 +37,15 @@ export async function setupDatabase(password) {
|
||||
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
database.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
...(SettingsService.getProperty("serverUrls") || {})
|
||||
});
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
|
||||
@@ -21,15 +21,15 @@ import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey,
|
||||
decryptMulti,
|
||||
encryptMulti
|
||||
removeCryptoKey
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { Dimensions, Image, Platform } from "react-native";
|
||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
console.log("hello world");
|
||||
/**
|
||||
* Scale down & compress images to screen width
|
||||
* for loading in editor.
|
||||
|
||||
@@ -18,24 +18,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
import { getFileNameWithExtension } from "@notesnook/core/dist/utils/filename";
|
||||
import React from "react";
|
||||
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 RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
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 { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -211,7 +207,11 @@ export default async function downloadAttachment(
|
||||
try {
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(options.groupId || attachment.hash, attachment.hash);
|
||||
.downloadFile(
|
||||
options.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
@@ -259,15 +259,6 @@ export default async function downloadAttachment(
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!isImage(attachment.mimeType) &&
|
||||
!isDocument(attachment.mimeType)
|
||||
) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
@@ -287,7 +278,6 @@ export default async function downloadAttachment(
|
||||
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
@@ -296,6 +286,7 @@ export default async function downloadAttachment(
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.catch(console.log);
|
||||
}
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
|
||||
@@ -17,31 +17,54 @@ 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 from "@notesnook/core/dist/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir, fileCheck } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) {
|
||||
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
let { url, headers } = data;
|
||||
let path = `${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}`);
|
||||
return true;
|
||||
}
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (size === 0) {
|
||||
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
|
||||
if (attachment && attachment.size !== decryptedLength) {
|
||||
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: "GET",
|
||||
@@ -63,34 +86,51 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
);
|
||||
throw new Error("Unable to resolve download url");
|
||||
}
|
||||
let totalSize = 0;
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
let request = RNFetchBlob.config({
|
||||
path: path,
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true
|
||||
})
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.progress((recieved, total) => {
|
||||
.progress(async (recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(0, total, filename, recieved, "download");
|
||||
totalSize = total;
|
||||
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
};
|
||||
|
||||
let response = await request;
|
||||
await fileCheck(response, totalSize);
|
||||
console.log(response.info().headers);
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
response.info().headers?.["Content-Type"];
|
||||
|
||||
if (contentType === "application/xml") {
|
||||
const error = parseS3Error(await response.text());
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
|
||||
if (!(await exists(filename))) {
|
||||
throw new Error("File size mismatch");
|
||||
}
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled") {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
@@ -106,7 +146,8 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
@@ -115,37 +156,23 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
if (!isInternetReachable) return { success: true };
|
||||
if (!isInternetReachable) return;
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) return { failed: "Attachment not found." };
|
||||
|
||||
try {
|
||||
const size = await getUploadedFileSize(hash);
|
||||
console.log("File Size", size);
|
||||
if (size === -1) return { success: true };
|
||||
|
||||
if (size === 0) return { failed: "File length is 0." };
|
||||
if (size === 0)
|
||||
return {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
} catch (e) {
|
||||
return { failed: e?.message };
|
||||
}
|
||||
|
||||
@@ -17,17 +17,22 @@ 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 { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
|
||||
import { checkAttachment, downloadFile } from "./download";
|
||||
import {
|
||||
clearFileStorage,
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64
|
||||
hashBase64,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable } from "./utils";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
|
||||
export default {
|
||||
readEncrypted,
|
||||
@@ -39,5 +44,10 @@ export default {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
checkAttachment
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
};
|
||||
|
||||
@@ -20,9 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
@@ -158,7 +159,35 @@ export async function migrateFilesFromCache() {
|
||||
}
|
||||
}
|
||||
|
||||
const ABYTES = 17;
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
@@ -200,3 +229,29 @@ export async function exists(filename) {
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files) {
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter((file) => !appGroupFiles.includes(file));
|
||||
}
|
||||
|
||||
return missingFiles;
|
||||
}
|
||||
|
||||
export async function getCacheSize() {
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -17,32 +17,30 @@ 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 RNFetchBlob from "react-native-blob-util";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { createCacheDir } from "./io";
|
||||
import { getUploadedFileSize } from "./download";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import { cacheDir, getUploadedFileSize } from "./utils";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
const uploadedFileSize = await getUploadedFileSize(filename);
|
||||
|
||||
if (uploadedFileSize === -1) {
|
||||
DatabaseLogger.log("Upload verification failed.");
|
||||
return false;
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const isUploaded = uploadedFileSize !== 0;
|
||||
if (isUploaded) {
|
||||
if (uploadedFileSize !== 0) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
@@ -117,9 +115,6 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
);
|
||||
let attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
} else {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
|
||||
throw new Error(
|
||||
|
||||
@@ -17,10 +17,13 @@ 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 * as ScopedStorage from "react-native-scoped-storage";
|
||||
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/dist/utils/constants";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export const cacheDir =
|
||||
@@ -34,24 +37,36 @@ export function getRandomId(prefix) {
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
export function extractValueFromXmlTag(code, xml) {
|
||||
if (!xml.includes(code)) return `Unknown ${code}`;
|
||||
return xml.slice(
|
||||
xml.indexOf(`<${code}>`) + code.length + 2,
|
||||
xml.indexOf(`</${code}>`)
|
||||
);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
|
||||
export async function fileCheck(response, totalSize) {
|
||||
if (totalSize < 1000) {
|
||||
let text = await response.text();
|
||||
if (text.startsWith("<?xml")) {
|
||||
let errorJson = {
|
||||
Code: extractValueFromXmlTag("Code", text),
|
||||
Message: extractValueFromXmlTag("Message", text)
|
||||
};
|
||||
throw new Error(`${errorJson.Code}: ${errorJson.Message}`);
|
||||
const error = {
|
||||
Code: "UNKNOWN",
|
||||
Message: xml
|
||||
};
|
||||
|
||||
try {
|
||||
if (!xml) return error;
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
|
||||
const ErrorElement = doc.getElementsByTagName("Error")[0];
|
||||
if (!ErrorElement) return error;
|
||||
|
||||
for (const child of ErrorElement.children) {
|
||||
if (
|
||||
child.textContent &&
|
||||
(child.tagName === "Code" || child.tagName === "Message")
|
||||
)
|
||||
error[child.tagName] = child.textContent;
|
||||
}
|
||||
return error;
|
||||
} catch (e) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +74,11 @@ export function cancelable(operation) {
|
||||
const cancelToken = {
|
||||
cancel: () => {}
|
||||
};
|
||||
return (filename, { url, headers }) => {
|
||||
return (filename, requestOptions) => {
|
||||
return {
|
||||
execute: () => operation(filename, { url, headers }, cancelToken),
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
cancel: async () => {
|
||||
await cancelToken.cancel();
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -91,3 +105,21 @@ export async function releasePermissions(path) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,12 +118,14 @@ const Actions = ({
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
});
|
||||
const res = await filesystem.checkAttachment(attachment.hash);
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, result.failed);
|
||||
setFailed(result.failed);
|
||||
ToastManager.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
heading: "File check failed with error: " + result.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
@@ -122,10 +122,11 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
const attachment = (await attachments.item(i))?.item;
|
||||
if (!attachment) continue;
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
if (result.failed) {
|
||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(attachment.id, undefined);
|
||||
await db.attachments.markAsFailed(attachment.id);
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
|
||||
@@ -68,7 +68,8 @@ export const ChangePassword = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (!result) throw new Error("Failed to create backup");
|
||||
if (result.error)
|
||||
throw new Error(`Failed to create backup: ${result.error}`);
|
||||
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
|
||||
@@ -35,6 +35,7 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -77,6 +78,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
hideAuth();
|
||||
SettingsService.setProperty("encryptedBackup", true);
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
|
||||
@@ -83,7 +83,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 1);
|
||||
}, 500);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { AnnouncementDialog } from "../announcements";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
import { Dialog } from "../dialog";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||
import { LoadingDialog } from "../dialogs/loading";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
@@ -35,8 +36,7 @@ import { Expiring } from "../premium/expiring";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
import Progress from "../dialogs/progress";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -52,7 +52,6 @@ const DialogProvider = () => {
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<SheetProvider />
|
||||
<SheetProvider context="sync_progress" />
|
||||
<RestoreDataSheet />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
@@ -62,6 +61,7 @@ const DialogProvider = () => {
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<Progress />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,21 +32,29 @@ const DialogContainer = ({
|
||||
}: ViewProps & {
|
||||
width?: any;
|
||||
height?: any;
|
||||
noBorder?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...restProps}
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12,
|
||||
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
|
||||
}}
|
||||
style={[
|
||||
style,
|
||||
{
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
},
|
||||
restProps?.noBorder
|
||||
? {}
|
||||
: {
|
||||
...getElevationStyle(5),
|
||||
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ type DialogInfo = {
|
||||
paragraph?: string;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positivePress?: (value: any) => void;
|
||||
positivePress?: (...args: any[]) => void;
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
@@ -45,6 +45,15 @@ type DialogInfo = {
|
||||
context: "global" | "local" | (string & {});
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: string;
|
||||
check?: {
|
||||
info: string;
|
||||
type?: string;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
notice: {
|
||||
text: string;
|
||||
type: "alert" | "information";
|
||||
};
|
||||
};
|
||||
|
||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||
|
||||
@@ -37,6 +37,7 @@ import DialogHeader from "./dialog-header";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -62,7 +63,8 @@ export const Dialog = ({ context = "global" }) => {
|
||||
disableBackdropClosing: false,
|
||||
check: {
|
||||
info: "Check",
|
||||
type: "transparent"
|
||||
type: "transparent",
|
||||
defaultValue: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,7 +88,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(false);
|
||||
setChecked(data.check?.defaultValue);
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -180,6 +182,19 @@ export const Dialog = ({ context = "global" }) => {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
type={dialogInfo.notice.type || "information"}
|
||||
text={dialogInfo.notice.text}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -198,7 +213,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "selected" : "plain"}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { deleteCacheFileByPath } from "../../../common/filesystem/io";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
@@ -124,6 +125,7 @@ const PDFPreview = () => {
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
deleteCacheFileByPath(pdfSource);
|
||||
setPDFSource(null);
|
||||
setVisible(false);
|
||||
setPassword("");
|
||||
|
||||
198
apps/mobile/app/components/dialogs/progress/index.tsx
Normal file
198
apps/mobile/app/components/dialogs/progress/index.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogContainer from "../../dialog/dialog-container";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
export type ProgressOptions = {
|
||||
progress?: string;
|
||||
cancelCallback?: () => void;
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
};
|
||||
|
||||
export const PROGRESS_EVENTS = {
|
||||
start: "startProgress",
|
||||
end: "endProgress",
|
||||
update: "updateProgress"
|
||||
};
|
||||
|
||||
export default function Progress() {
|
||||
const { colors } = useThemeColors();
|
||||
const [progress, setProgress] = React.useState<string | undefined>();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const cancelCallback = React.useRef<() => void>();
|
||||
const [data, setData] = React.useState<{
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
console.log("options", options.fillBackground);
|
||||
setData({
|
||||
title: options.title,
|
||||
paragraph: options.paragraph,
|
||||
fillBackground: options.fillBackground,
|
||||
canHideProgress: options.canHideProgress
|
||||
});
|
||||
setVisible(true);
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
|
||||
setProgress(undefined);
|
||||
setVisible(false);
|
||||
setData(undefined);
|
||||
cancelCallback.current?.();
|
||||
cancelCallback.current = undefined;
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
if (options.cancelCallback) {
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
}
|
||||
|
||||
const data: ProgressOptions = {};
|
||||
if (options.title) data.title = options.title;
|
||||
if (options.paragraph) data.paragraph = options.paragraph;
|
||||
if (options.fillBackground)
|
||||
data.fillBackground = options.fillBackground;
|
||||
|
||||
setData((current) => {
|
||||
return {
|
||||
...current,
|
||||
...data
|
||||
};
|
||||
});
|
||||
})
|
||||
];
|
||||
return () => {
|
||||
events.forEach((event) => event?.unsubscribe());
|
||||
cancelCallback.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
background={data?.fillBackground ? colors.primary.background : undefined}
|
||||
visible
|
||||
>
|
||||
<DialogContainer
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
noBorder={data?.fillBackground ? true : false}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
gap: 10,
|
||||
paddingBottom: 20
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{data?.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{progress ? progress : data?.paragraph}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{data?.canHideProgress ? null : (
|
||||
<Button
|
||||
title={cancelCallback.current ? "Cancel" : "Hide"}
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
if (cancelCallback.current) {
|
||||
cancelCallback.current?.();
|
||||
}
|
||||
setVisible(false);
|
||||
setProgress(undefined);
|
||||
setData(undefined);
|
||||
}}
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</DialogContainer>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function startProgress(options: ProgressOptions) {
|
||||
eSendEvent(PROGRESS_EVENTS.start, options);
|
||||
}
|
||||
|
||||
export function endProgress() {
|
||||
eSendEvent(PROGRESS_EVENTS.end);
|
||||
}
|
||||
|
||||
export function updateProgress(options: ProgressOptions) {
|
||||
eSendEvent(PROGRESS_EVENTS.update, options);
|
||||
}
|
||||
@@ -43,9 +43,6 @@ import { eSendEvent } from "../../services/event-manager";
|
||||
|
||||
const Intro = ({ navigation }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const isTelemetryEnabled = useSettingStore(
|
||||
(state) => state.settings.telemetry
|
||||
);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
@@ -217,44 +214,6 @@ const Intro = ({ navigation }) => {
|
||||
type="accent"
|
||||
title="Get started"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
width: "90%",
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "center",
|
||||
padding: 12,
|
||||
maxWidth: 500
|
||||
}}
|
||||
onPress={() => {
|
||||
SettingsService.set({ telemetry: !isTelemetryEnabled });
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={SIZE.md}
|
||||
name={
|
||||
isTelemetryEnabled ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
color={
|
||||
isTelemetryEnabled ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 6
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
Help improve Notesnook by sending completely anonymized{" "}
|
||||
<Heading size={SIZE.xs}>private analytics and bug reports.</Heading>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
@@ -95,9 +95,9 @@ export default function Migrate() {
|
||||
await sleep(1);
|
||||
const { error, report } = await BackupService.run(false, "local");
|
||||
if (error) {
|
||||
ToastManager.error(error, "Backup failed");
|
||||
ToastManager.error(error as Error, "Backup failed");
|
||||
if (report) {
|
||||
reportError(error);
|
||||
reportError(error as Error);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
|
||||
@@ -1,600 +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 { getFormattedDate } from "@notesnook/common";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { unzip } from "react-native-zip-archive";
|
||||
import { db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
|
||||
import {
|
||||
ToastManager,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { refreshAllStores } from "../../../stores/create-db-collection-store";
|
||||
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const sheet = useRef();
|
||||
useEffect(() => {
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
sheet.current?.show();
|
||||
}, 1);
|
||||
};
|
||||
eSubscribeEvent(eOpenRestoreDialog, open);
|
||||
eSubscribeEvent(eCloseRestoreDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenRestoreDialog, open);
|
||||
eUnSubscribeEvent(eCloseRestoreDialog, close);
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (restoring) {
|
||||
showIsWorking();
|
||||
return;
|
||||
}
|
||||
sheet.current?.hide();
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 150);
|
||||
}, [restoring]);
|
||||
|
||||
const showIsWorking = () => {
|
||||
ToastManager.show({
|
||||
heading: "Restoring Backup",
|
||||
message: "Your backup data is being restored. please wait.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
fwdRef={sheet}
|
||||
gestureEnabled={!restoring}
|
||||
closeOnTouchBackdrop={!restoring}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<RestoreDataComponent
|
||||
close={close}
|
||||
restoring={restoring}
|
||||
setRestoring={setRestoring}
|
||||
actionSheetRef={sheet}
|
||||
/>
|
||||
<Toast context="local" />
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreDataSheet;
|
||||
|
||||
const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backupDirectoryAndroid, setBackupDirectoryAndroid] = useState(false);
|
||||
const [progress, setProgress] = useState();
|
||||
useEffect(() => {
|
||||
const subscription = db.eventManager.subscribe(
|
||||
EVENTS.migrationProgress,
|
||||
(progress) => {
|
||||
setProgress(progress);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const restore = async (item) => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = Platform.OS === "ios" ? item.path : item.uri;
|
||||
console.log(file);
|
||||
if (file.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const cacheFile = `file://${RNFetchBlob.fs.dirs.CacheDir}/backup.zip`;
|
||||
if (await RNFetchBlob.fs.exists(cacheFile)) {
|
||||
await RNFetchBlob.fs.unlink(cacheFile);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(cacheFile, "", "utf8");
|
||||
console.log("copying");
|
||||
await copyFileAsync(file, cacheFile);
|
||||
console.log("copied");
|
||||
await restoreFromZip(cacheFile);
|
||||
} else {
|
||||
await restoreFromZip(file, false);
|
||||
}
|
||||
} else if (file.endsWith(".nnbackup")) {
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(file, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(file, "utf8");
|
||||
}
|
||||
await restoreFromNNBackup(JSON.parse(backup));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const withPassword = () => {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: () => {
|
||||
if (resolved) return;
|
||||
resolve(undefined);
|
||||
},
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password, isEncryptionKey) => {
|
||||
resolve({
|
||||
value: password,
|
||||
isEncryptionKey
|
||||
});
|
||||
resolved = true;
|
||||
return true;
|
||||
},
|
||||
check: {
|
||||
info: "Use encryption key",
|
||||
type: "transparent"
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const checkBackups = async () => {
|
||||
try {
|
||||
let files = [];
|
||||
if (Platform.OS === "android") {
|
||||
let backupDirectory = SettingsService.get().backupDirectoryAndroid;
|
||||
if (backupDirectory) {
|
||||
setBackupDirectoryAndroid(backupDirectory);
|
||||
files = await ScopedStorage.listFiles(backupDirectory.uri);
|
||||
} else {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let path = await storage.checkAndCreateDir("/backups/");
|
||||
files = await RNFetchBlob.fs.lstat(path);
|
||||
}
|
||||
files = files
|
||||
.filter((file) => {
|
||||
const name = Platform.OS === "android" ? file.name : file.filename;
|
||||
return name.endsWith(".nnbackup") || name.endsWith(".nnbackupz");
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
setFiles(files);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreBackup = async (backup, password, key) => {
|
||||
await db.transaction(async () => {
|
||||
await db.backup.import(backup, password, key);
|
||||
});
|
||||
await db.initCollections();
|
||||
refreshAllStores();
|
||||
ToastManager.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
return true;
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
console.log(e.stack);
|
||||
ToastManager.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromZip(file, remove) {
|
||||
try {
|
||||
const zipOutputFolder = `${cacheDir}/backup_extracted`;
|
||||
if (await RNFetchBlob.fs.exists(zipOutputFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipOutputFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipOutputFolder);
|
||||
}
|
||||
await unzip(file, zipOutputFolder);
|
||||
console.log("Unzipped files successfully to", zipOutputFolder);
|
||||
|
||||
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
|
||||
|
||||
// if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
|
||||
// throw new Error("Backup file is invalid");
|
||||
// }
|
||||
|
||||
await db.transaction(async () => {
|
||||
let password;
|
||||
let key;
|
||||
console.log(
|
||||
`Found ${backupFiles?.length} files to restore from backup`
|
||||
);
|
||||
for (const path of backupFiles) {
|
||||
if (path === ".nnbackup") continue;
|
||||
const filePath = `${zipOutputFolder}/${path}`;
|
||||
const data = await RNFetchBlob.fs.readFile(filePath, "utf8");
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (parsed.encrypted && !password) {
|
||||
console.log("Backup is encrypted...", "requesting password");
|
||||
const { value, isEncryptionKey } = await withPassword();
|
||||
|
||||
if (isEncryptionKey) {
|
||||
key = value;
|
||||
} else {
|
||||
password = value;
|
||||
}
|
||||
if (!password && !key) throw new Error("Failed to decrypt backup");
|
||||
}
|
||||
await db.backup.import(parsed, password, key);
|
||||
console.log("Imported", path);
|
||||
}
|
||||
});
|
||||
// Remove files from cache
|
||||
RNFetchBlob.fs.unlink(zipOutputFolder).catch(console.log);
|
||||
if (remove) {
|
||||
RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
|
||||
refreshAllStores();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setRestoring(false);
|
||||
close();
|
||||
ToastManager.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromNNBackup(backup) {
|
||||
try {
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
const { value, isEncryptionKey } = await withPassword();
|
||||
|
||||
let key;
|
||||
let password;
|
||||
if (isEncryptionKey) {
|
||||
key = value;
|
||||
} else {
|
||||
password = value;
|
||||
}
|
||||
|
||||
if (key || password) {
|
||||
try {
|
||||
await restoreBackup(backup, password, key);
|
||||
close();
|
||||
setRestoring(false);
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
} else {
|
||||
setRestoring(false);
|
||||
}
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
setRestoring(false);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: async () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
console.log("disabled...");
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
copyTo: "cachesDirectory"
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
if (file.name.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
await restoreFromZip(file.fileCopyUri, true);
|
||||
} else if (file.name.endsWith(".nnbackup")) {
|
||||
RNFetchBlob.fs.unlink(file.fileCopyUri).catch(console.log);
|
||||
setRestoring(true);
|
||||
const data = await fetch(file.uri);
|
||||
await restoreFromNNBackup(await data.json());
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e.stack);
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 50,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
maxWidth: "75%"
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.sm} style={{ width: "100%", maxWidth: "100%" }}>
|
||||
{getFormattedDate(item?.lastModified * 1)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs}>
|
||||
{(item.filename || item.name).replace(".nnbackup", "")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Button
|
||||
title="Restore"
|
||||
height={30}
|
||||
type="accent"
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
fontSize={SIZE.sm - 1}
|
||||
onPress={() => restore(item, index)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 8,
|
||||
paddingRight: 8,
|
||||
alignItems: "center",
|
||||
paddingTop: restoring ? 8 : 0
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title="Backups"
|
||||
paragraph={`All the backups are stored in ${
|
||||
Platform.OS === "ios"
|
||||
? "File Manager/Notesnook/Backups"
|
||||
: "selected backups folder."
|
||||
}`}
|
||||
button={button}
|
||||
/>
|
||||
</View>
|
||||
<Seperator half />
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
!restoring ? (
|
||||
loading ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 100
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 100
|
||||
}}
|
||||
>
|
||||
{Platform.OS === "android" && !backupDirectoryAndroid ? (
|
||||
<>
|
||||
<Button
|
||||
title="Select backups folder"
|
||||
icon="folder"
|
||||
onPress={async () => {
|
||||
let folder = await ScopedStorage.openDocumentTree(
|
||||
true
|
||||
);
|
||||
let subfolder;
|
||||
if (folder.name !== "Notesnook backups") {
|
||||
subfolder = await ScopedStorage.createDirectory(
|
||||
folder.uri,
|
||||
"Notesnook backups"
|
||||
);
|
||||
} else {
|
||||
subfolder = folder;
|
||||
}
|
||||
SettingsService.set({
|
||||
backupDirectoryAndroid: subfolder
|
||||
});
|
||||
setBackupDirectoryAndroid(subfolder);
|
||||
setLoading(true);
|
||||
checkBackups();
|
||||
}}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
height={30}
|
||||
width={null}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 5
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
textBreakStrategy="balanced"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Select the folder that includes your backup files to
|
||||
list them here.
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
No backups found
|
||||
</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 200
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Restoring {progress ? progress?.collection : null}
|
||||
{progress ? `(${progress.current}/${progress.total}) ` : null}
|
||||
...Please wait.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
keyExtractor={(item) => item.name || item.filename}
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
data={restoring || loading ? [] : files}
|
||||
renderItem={renderItem}
|
||||
ListFooterComponent={
|
||||
restoring || loading || files.length === 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -112,7 +112,7 @@ export const Button = ({
|
||||
NativeTooltip.show(event, tooltipText, NativeTooltip.POSITIONS.TOP);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
disabled={loading || restProps.disabled}
|
||||
type={type}
|
||||
accentColor={accentColor}
|
||||
accentText={accentText}
|
||||
@@ -132,6 +132,7 @@ export const Button = ({
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
opacity: restProps?.disabled ? 0.5 : 1,
|
||||
...(style as ViewStyle)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -41,6 +41,7 @@ import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import phone from "phone";
|
||||
import isURL from "validator/lib/isURL";
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
fwdRef?: RefObject<TextInput>;
|
||||
@@ -49,7 +50,8 @@ interface InputProps extends TextInputProps {
|
||||
| "email"
|
||||
| "confirmPassword"
|
||||
| "username"
|
||||
| "phonenumber";
|
||||
| "phonenumber"
|
||||
| "url";
|
||||
loading?: boolean;
|
||||
onSubmit?: (
|
||||
event: NativeSyntheticEvent<TextInputSubmitEditingEventData>
|
||||
@@ -151,6 +153,9 @@ const Input = ({
|
||||
case "confirmPassword":
|
||||
isError = customValidator && value === customValidator();
|
||||
break;
|
||||
case "url":
|
||||
isError = isURL(value);
|
||||
break;
|
||||
case "phonenumber": {
|
||||
const result = phone(value, {
|
||||
strictDetection: true,
|
||||
|
||||
@@ -19,4 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { FeatureType } from "./components/sheets/new-feature";
|
||||
|
||||
export const features: FeatureType[] = [];
|
||||
export const features: FeatureType[] = [
|
||||
{
|
||||
title: "Full offline mode",
|
||||
body: "Enable full offline mode from Settings -> Account -> Sync Settings. This will allow you to keep all your attachments and data offline on the client"
|
||||
},
|
||||
{
|
||||
title: "Backup with attachments",
|
||||
body: "You can now backup your notes alongwith all your attachments - kind of like a full account snapshot."
|
||||
},
|
||||
{
|
||||
title: "Self hosting",
|
||||
body: "This release also adds initial support for changing server URLs i.e. to connect your own instance of Notesnook. Please note that this is still experimental and may not work as expected."
|
||||
}
|
||||
];
|
||||
|
||||
@@ -26,10 +26,7 @@ import {
|
||||
} from "@notesnook/core/dist/common";
|
||||
import { EventManagerSubscription } from "@notesnook/core/dist/utils/event-manager";
|
||||
import notifee from "@notifee/react-native";
|
||||
import NetInfo, {
|
||||
NetInfoState,
|
||||
NetInfoSubscription
|
||||
} from "@react-native-community/netinfo";
|
||||
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
AppState,
|
||||
@@ -47,6 +44,7 @@ import { checkVersion } from "react-native-check-version";
|
||||
import Config from "react-native-config";
|
||||
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 Migrate from "../components/sheets/migrate";
|
||||
import NewFeature from "../components/sheets/new-feature";
|
||||
@@ -97,7 +95,6 @@ import {
|
||||
import { getGithubVersion } from "../utils/github-version";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import { sleep } from "../utils/time";
|
||||
import { initializeLogger } from "../common/database/logger";
|
||||
|
||||
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -362,6 +359,7 @@ const initializeDatabase = async (password?: string) => {
|
||||
try {
|
||||
await setupDatabase(password);
|
||||
await db.init();
|
||||
Sync.run();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
ToastManager.error(e as Error, "Error initializing database", "global");
|
||||
@@ -391,18 +389,15 @@ export const useAppEvents = () => {
|
||||
]);
|
||||
|
||||
const syncedOnLaunch = useRef(false);
|
||||
|
||||
const refValues = useRef<
|
||||
Partial<{
|
||||
subsriptionSuccessListener: EmitterSubscription;
|
||||
subsriptionErrorListener: EmitterSubscription;
|
||||
isUserReady: boolean;
|
||||
prevState: AppStateStatus;
|
||||
showingDialog: boolean;
|
||||
removeInternetStateListener: NetInfoSubscription;
|
||||
isReconnecting: boolean;
|
||||
initialUrl: string;
|
||||
backupDidWait: boolean;
|
||||
isConnectingSSE: boolean;
|
||||
}>
|
||||
>({});
|
||||
|
||||
@@ -465,14 +460,31 @@ export const useAppEvents = () => {
|
||||
const user = await db.user.getUser();
|
||||
if (PremiumService.get() && user) {
|
||||
if (
|
||||
await BackupService.checkBackupRequired(SettingsService.get().reminder)
|
||||
SettingsService.get().backupDirectoryAndroid ||
|
||||
Platform.OS !== "android"
|
||||
) {
|
||||
if (
|
||||
!SettingsService.get().backupDirectoryAndroid &&
|
||||
Platform.OS === "android"
|
||||
)
|
||||
return;
|
||||
sleep(2000).then(() => BackupService.run());
|
||||
const partialBackup = await BackupService.checkBackupRequired(
|
||||
SettingsService.get().reminder,
|
||||
"lastBackupDate"
|
||||
);
|
||||
const fullBackup = await BackupService.checkBackupRequired(
|
||||
SettingsService.get().fullBackupReminder,
|
||||
"lastFullBackupDate"
|
||||
);
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
if (partialBackup) {
|
||||
await BackupService.run();
|
||||
}
|
||||
|
||||
if (fullBackup) {
|
||||
await BackupService.run(true, undefined, "full");
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsService.getProperty("offlineMode")) {
|
||||
db.attachments.cacheAttachments().catch(console.log);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
@@ -533,7 +545,6 @@ export const useAppEvents = () => {
|
||||
setRecoveryKeyMessage();
|
||||
}
|
||||
if (!user?.isEmailConfirmed) setEmailVerifyMessage();
|
||||
refValues.current.isUserReady = true;
|
||||
|
||||
syncedOnLaunch.current = true;
|
||||
if (!isLogin) {
|
||||
@@ -607,18 +618,19 @@ export const useAppEvents = () => {
|
||||
}, [onSyncComplete, onUserUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
const onInternetStateChanged = async (state: NetInfoState) => {
|
||||
const onInternetStateChanged = async () => {
|
||||
if (!syncedOnLaunch.current) return;
|
||||
reconnectSSE(state);
|
||||
Sync.run("global", false, "full");
|
||||
reconnectSSE();
|
||||
};
|
||||
|
||||
const onAppStateChanged = async (state: AppStateStatus) => {
|
||||
if (state === "active") {
|
||||
notifee.setBadgeCount(0);
|
||||
updateStatusBarColor();
|
||||
|
||||
checkAutoBackup();
|
||||
await reconnectSSE();
|
||||
Sync.run("global", false, "full");
|
||||
reconnectSSE();
|
||||
await checkForShareExtensionLaunchedInBackground();
|
||||
MMKV.removeItem("appState");
|
||||
let user = await db.user.getUser();
|
||||
@@ -704,42 +716,15 @@ export const useAppEvents = () => {
|
||||
}
|
||||
}, [appLocked, syncing, checkAutoBackup]);
|
||||
|
||||
async function reconnectSSE(connection?: NetInfoState) {
|
||||
if (refValues.current?.isReconnecting || !refValues.current?.isUserReady)
|
||||
return;
|
||||
|
||||
if (useSettingStore.getState().appDidEnterBackgroundForAction) {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
console.log("AppDidEnterForegroundAfterAction");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SettingsService.get().sessionExpired) {
|
||||
refValues.current.isReconnecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
refValues.current.isReconnecting = true;
|
||||
let connectionState = connection;
|
||||
async function reconnectSSE() {
|
||||
try {
|
||||
if (!connectionState) {
|
||||
connectionState = await NetInfo.fetch();
|
||||
}
|
||||
|
||||
const user = await db.user.getUser();
|
||||
if (
|
||||
user &&
|
||||
connectionState.isConnected &&
|
||||
connectionState.isInternetReachable
|
||||
) {
|
||||
await db.connectSSE();
|
||||
} else {
|
||||
useUserStore.getState().setSyncing(false);
|
||||
await db.syncer.stop();
|
||||
}
|
||||
refValues.current.isReconnecting = false;
|
||||
if (refValues.current?.isConnectingSSE) return;
|
||||
refValues.current.isConnectingSSE = true;
|
||||
await db.connectSSE();
|
||||
refValues.current.isConnectingSSE = false;
|
||||
} catch (e) {
|
||||
refValues.current.isReconnecting = false;
|
||||
refValues.current.isConnectingSSE = false;
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { isImage } from "@notesnook/core/dist/utils/filename";
|
||||
import { basename } from "pathe";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker, {
|
||||
@@ -41,7 +42,6 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { editorController, editorState } from "./utils";
|
||||
import { basename } from "pathe";
|
||||
|
||||
const showEncryptionSheet = (file: DocumentPickerResponse) => {
|
||||
presentSheet({
|
||||
@@ -123,13 +123,13 @@ const file = async (fileOptions: PickerOptions) => {
|
||||
file.name,
|
||||
fileOptions
|
||||
))
|
||||
)
|
||||
return;
|
||||
) {
|
||||
throw new Error("Failed to attach file");
|
||||
}
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
|
||||
if (!fileOptions.tabId) return;
|
||||
if (
|
||||
fileOptions.tabId &&
|
||||
fileOptions.tabId !== undefined &&
|
||||
fileOptions.noteId &&
|
||||
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
|
||||
fileOptions.noteId
|
||||
@@ -158,16 +158,16 @@ const file = async (fileOptions: PickerOptions) => {
|
||||
fileOptions.tabId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to attach file, no tabId is set");
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
eSendEvent(eCloseSheet);
|
||||
}, 1000);
|
||||
eSendEvent(eCloseSheet);
|
||||
} catch (e) {
|
||||
eSendEvent(eCloseSheet);
|
||||
ToastManager.show({
|
||||
heading: (e as Error).message,
|
||||
message: "You need internet access to attach a file",
|
||||
heading: "Failed to attach file",
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
@@ -203,7 +203,6 @@ const camera = async (options: PickerOptions) => {
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
console.log("attachment error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -32,9 +32,12 @@ import {
|
||||
TimeFormatPicker,
|
||||
TrashIntervalPicker,
|
||||
BackupReminderPicker,
|
||||
ApplockTimerPicker
|
||||
ApplockTimerPicker,
|
||||
BackupWithAttachmentsReminderPicker
|
||||
} from "./picker/pickers";
|
||||
import ThemeSelector from "./theme-selector";
|
||||
import { RestoreBackup } from "./restore-backup";
|
||||
import { ServersConfiguration } from "./server-config";
|
||||
|
||||
export const components: { [name: string]: ReactElement } = {
|
||||
colorpicker: <AccentColorPicker />,
|
||||
@@ -51,5 +54,8 @@ export const components: { [name: string]: ReactElement } = {
|
||||
"date-format-selector": <DateFormatPicker />,
|
||||
"time-format-selector": <TimeFormatPicker />,
|
||||
"theme-selector": <ThemeSelector />,
|
||||
"applock-timer": <ApplockTimerPicker />
|
||||
"applock-timer": <ApplockTimerPicker />,
|
||||
autobackupsattachments: <BackupWithAttachmentsReminderPicker />,
|
||||
backuprestore: <RestoreBackup />,
|
||||
"server-config": <ServersConfiguration />
|
||||
};
|
||||
|
||||
@@ -18,35 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import DelayLayout from "../../components/delay-layout";
|
||||
import BaseDialog from "../../components/dialog/base-dialog";
|
||||
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { Header } from "../../components/header";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { SectionGroup } from "./section-group";
|
||||
import { settingsGroups } from "./settings-data";
|
||||
import { RouteParams, SettingSection } from "./types";
|
||||
import SettingsUserSection from "./user-section";
|
||||
import { Header } from "../../components/header";
|
||||
const keyExtractor = (item: SettingSection) => item.id;
|
||||
|
||||
const Home = ({
|
||||
navigation
|
||||
}: NativeStackScreenProps<RouteParams, "SettingsHome">) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useNavigationFocus(navigation, {
|
||||
onFocus: () => {
|
||||
useNavigationStore.getState().setFocusedRouteId("Settings");
|
||||
@@ -62,16 +49,6 @@ const Home = ({
|
||||
<SectionGroup item={item} />
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function settingsLoading(state: boolean) {
|
||||
setLoading(state || true);
|
||||
}
|
||||
eSubscribeEvent("settings-loading", settingsLoading);
|
||||
return () => {
|
||||
eUnSubscribeEvent("settings-loading", settingsLoading);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
@@ -81,57 +58,6 @@ const Home = ({
|
||||
id="Settings"
|
||||
/>
|
||||
<DelayLayout delay={300} type="settings">
|
||||
{loading && (
|
||||
//@ts-ignore // Migrate to typescript required.
|
||||
<BaseDialog animated={false} bounce={false} visible={true}>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.paragraph} size={SIZE.lg}>
|
||||
Logging out
|
||||
</Heading>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Please wait while we log out and clear app data.
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</BaseDialog>
|
||||
)}
|
||||
|
||||
<Animated.FlatList
|
||||
entering={FadeInDown}
|
||||
data={settingsGroups}
|
||||
|
||||
@@ -121,8 +121,8 @@ export const BackupReminderPicker = createSettingsPicker({
|
||||
SettingsService.set({ reminder: item });
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return item === "useroff"
|
||||
? "Never"
|
||||
return item === "useroff" || item === "off" || item === "never"
|
||||
? "Off"
|
||||
: item.slice(0, 1).toUpperCase() + item.slice(1);
|
||||
},
|
||||
getItemKey: (item) => item,
|
||||
@@ -140,6 +140,31 @@ export const BackupReminderPicker = createSettingsPicker({
|
||||
}
|
||||
});
|
||||
|
||||
export const BackupWithAttachmentsReminderPicker = createSettingsPicker({
|
||||
getValue: () => useSettingStore.getState().settings.fullBackupReminder,
|
||||
updateValue: (item) => {
|
||||
SettingsService.set({ fullBackupReminder: item });
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return item === "useroff" || item === "off" || item === "never"
|
||||
? "Off"
|
||||
: item.slice(0, 1).toUpperCase() + item.slice(1);
|
||||
},
|
||||
getItemKey: (item) => item,
|
||||
options: ["never", "weekly", "monthly"],
|
||||
compareValue: (current, item) => current === item,
|
||||
premium: true,
|
||||
requiresVerification: () => {
|
||||
return (
|
||||
!useSettingStore.getState().settings.encryptedBackup &&
|
||||
useUserStore.getState().user
|
||||
);
|
||||
},
|
||||
onCheckOptionIsPremium: (item) => {
|
||||
return item !== "never";
|
||||
}
|
||||
});
|
||||
|
||||
export const ApplockTimerPicker = createSettingsPicker({
|
||||
getValue: () => useSettingStore.getState().settings.appLockTimer,
|
||||
updateValue: (item) => {
|
||||
|
||||
558
apps/mobile/app/screens/settings/restore-backup/index.tsx
Normal file
558
apps/mobile/app/screens/settings/restore-backup/index.tsx
Normal file
@@ -0,0 +1,558 @@
|
||||
/*
|
||||
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 { formatBytes, getFormattedDate } from "@notesnook/common";
|
||||
import { LegacyBackupFile } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, FlatList, Platform, View } from "react-native";
|
||||
import RNFetchBlob, { ReactNativeBlobUtilStat } from "react-native-blob-util";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { unzip } from "react-native-zip-archive";
|
||||
import { DatabaseLogger, db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
import { deleteCacheFileByName } from "../../../common/filesystem/io";
|
||||
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import {
|
||||
endProgress,
|
||||
startProgress,
|
||||
updateProgress
|
||||
} from "../../../components/dialogs/progress";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import Heading from "../../../components/ui/typography/heading";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { SectionItem } from "../../../screens/settings/section-item";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { refreshAllStores } from "../../../stores/create-db-collection-store";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
|
||||
type PasswordOrKey = { password?: string; encryptionKey?: string };
|
||||
|
||||
const withPassword = () => {
|
||||
return new Promise<PasswordOrKey>((resolve) => {
|
||||
let resolved = false;
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: () => {
|
||||
if (resolved) return;
|
||||
resolve({});
|
||||
},
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password, isEncryptionKey) => {
|
||||
resolve({
|
||||
encryptionKey: isEncryptionKey ? password : undefined,
|
||||
password: isEncryptionKey ? undefined : password
|
||||
});
|
||||
resolved = true;
|
||||
return true;
|
||||
},
|
||||
check: {
|
||||
info: "Use encryption key",
|
||||
type: "transparent"
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const restoreBackup = async (options: {
|
||||
uri: string;
|
||||
deleteFile?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const isLegacyBackup = options.uri.endsWith(".nnbackup");
|
||||
|
||||
startProgress({
|
||||
title: "Restoring backup",
|
||||
paragraph: "Preparing to restore backup file...",
|
||||
canHideProgress: false
|
||||
});
|
||||
|
||||
let filePath = options.uri;
|
||||
let deleteBackupFile = options.deleteFile;
|
||||
|
||||
if (!isLegacyBackup) {
|
||||
if (Platform.OS === "android") {
|
||||
updateProgress({
|
||||
progress: `Copying backup file to cache...`
|
||||
});
|
||||
const cacheFile = `file://${RNFetchBlob.fs.dirs.CacheDir}/backup.zip`;
|
||||
if (await RNFetchBlob.fs.exists(cacheFile)) {
|
||||
await RNFetchBlob.fs.unlink(cacheFile);
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.createFile(cacheFile, "", "utf8");
|
||||
if (filePath.startsWith("content://")) {
|
||||
await copyFileAsync(filePath, cacheFile);
|
||||
} else {
|
||||
await RNFetchBlob.fs.cp(filePath, cacheFile);
|
||||
}
|
||||
filePath = cacheFile;
|
||||
deleteBackupFile = true;
|
||||
}
|
||||
|
||||
const zipOutputFolder = `${cacheDir}/backup_extracted`;
|
||||
if (await RNFetchBlob.fs.exists(zipOutputFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipOutputFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipOutputFolder);
|
||||
}
|
||||
updateProgress({
|
||||
progress: `Extracting files from backup...`
|
||||
});
|
||||
await unzip(filePath, zipOutputFolder);
|
||||
|
||||
const extractedBackupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
|
||||
|
||||
const extractedAttachments = extractedBackupFiles.includes("attachments")
|
||||
? await RNFetchBlob.fs.ls(`${zipOutputFolder}/attachments`)
|
||||
: [];
|
||||
|
||||
const attachmentsKeyPath: any = extractedAttachments?.find(
|
||||
(path) => path === ".attachments_key"
|
||||
);
|
||||
const attachmentsKey = attachmentsKeyPath
|
||||
? await JSON.parse(
|
||||
await RNFetchBlob.fs.readFile(
|
||||
`${zipOutputFolder}/attachments/${attachmentsKeyPath}`,
|
||||
"utf8"
|
||||
)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
let count = 0;
|
||||
await db.transaction(async () => {
|
||||
let passwordOrKey: PasswordOrKey;
|
||||
for (const path of extractedBackupFiles) {
|
||||
if (path === ".nnbackup" || path === "attachments") continue;
|
||||
|
||||
updateProgress({
|
||||
progress: `Restoring data (${count++}/${
|
||||
extractedBackupFiles.length
|
||||
})`
|
||||
});
|
||||
|
||||
const filePath = `${zipOutputFolder}/${path}`;
|
||||
const data = await RNFetchBlob.fs.readFile(filePath, "utf8");
|
||||
const backup = JSON.parse(data);
|
||||
|
||||
const isEncryptedBackup = backup.encrypted;
|
||||
|
||||
passwordOrKey = !isEncryptedBackup
|
||||
? ({} as PasswordOrKey)
|
||||
: await withPassword();
|
||||
|
||||
if (
|
||||
isEncryptedBackup &&
|
||||
!passwordOrKey?.encryptionKey &&
|
||||
!passwordOrKey?.password
|
||||
) {
|
||||
endProgress();
|
||||
throw new Error("Failed to decrypt backup");
|
||||
}
|
||||
|
||||
await db.backup.import(backup, {
|
||||
...passwordOrKey,
|
||||
attachmentsKey: attachmentsKey
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await db.initCollections();
|
||||
count = 0;
|
||||
for (const path of extractedAttachments) {
|
||||
if (path === ".attachments_key") continue;
|
||||
updateProgress({
|
||||
progress: `Restoring attachments (${count++}/${
|
||||
extractedAttachments.length
|
||||
})`
|
||||
});
|
||||
const hash = path;
|
||||
const attachment = await db.attachments.attachment(hash as string);
|
||||
if (!attachment) continue;
|
||||
|
||||
console.log("Saving attachment file", hash);
|
||||
await deleteCacheFileByName(hash);
|
||||
await RNFetchBlob.fs.cp(
|
||||
`${zipOutputFolder}/attachments/${hash}`,
|
||||
`${cacheDir}/${hash}`
|
||||
);
|
||||
}
|
||||
updateProgress({
|
||||
progress: `Cleaning up...`
|
||||
});
|
||||
// Remove files from cache
|
||||
RNFetchBlob.fs.unlink(zipOutputFolder).catch(console.log);
|
||||
if (Platform.OS === "android" || deleteBackupFile) {
|
||||
RNFetchBlob.fs.unlink(filePath).catch(console.log);
|
||||
}
|
||||
} else {
|
||||
updateProgress({
|
||||
progress: `Reading backup file...`
|
||||
});
|
||||
const rawData =
|
||||
Platform.OS === "android"
|
||||
? await ScopedStorage.readFile(filePath, "utf8")
|
||||
: await RNFetchBlob.fs.readFile(filePath, "utf8");
|
||||
const backup: LegacyBackupFile = JSON.parse(rawData) as LegacyBackupFile;
|
||||
|
||||
const isEncryptedBackup =
|
||||
typeof backup.data !== "string" && backup.data.cipher;
|
||||
|
||||
updateProgress({
|
||||
progress: isEncryptedBackup
|
||||
? `Backup is encrypted, decrypting...`
|
||||
: "Preparing to restore backup file..."
|
||||
});
|
||||
|
||||
const { encryptionKey, password } = isEncryptedBackup
|
||||
? ({} as PasswordOrKey)
|
||||
: await withPassword();
|
||||
|
||||
if (isEncryptedBackup && !encryptionKey && !password) {
|
||||
endProgress();
|
||||
throw new Error("Failed to decrypt backup");
|
||||
}
|
||||
|
||||
await db.transaction(async () => {
|
||||
updateProgress({
|
||||
progress: "Restoring backup..."
|
||||
});
|
||||
await db.backup.import(backup, {
|
||||
encryptionKey,
|
||||
password
|
||||
});
|
||||
});
|
||||
endProgress();
|
||||
}
|
||||
|
||||
ToastManager.show({
|
||||
heading: "Backup restored successfully",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
await db.initCollections();
|
||||
refreshAllStores();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
endProgress();
|
||||
} catch (e) {
|
||||
endProgress();
|
||||
DatabaseLogger.error(e as Error);
|
||||
ToastManager.error(e as Error, `Failed to restore backup`);
|
||||
}
|
||||
};
|
||||
|
||||
const BACKUP_FILES_CACHE: (ReactNativeBlobUtilStat | ScopedStorage.FileType)[] =
|
||||
[];
|
||||
|
||||
export const RestoreBackup = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [files, setFiles] =
|
||||
useState<(ReactNativeBlobUtilStat | ScopedStorage.FileType)[]>(
|
||||
BACKUP_FILES_CACHE
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backupDirectoryAndroid, setBackupDirectoryAndroid] =
|
||||
useState<ScopedStorage.FileType>();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const checkBackups = async () => {
|
||||
try {
|
||||
let files: (ReactNativeBlobUtilStat | ScopedStorage.FileType)[] = [];
|
||||
if (Platform.OS === "android") {
|
||||
const backupDirectory = SettingsService.get().backupDirectoryAndroid;
|
||||
if (backupDirectory) {
|
||||
setBackupDirectoryAndroid(backupDirectory);
|
||||
files = await ScopedStorage.listFiles(backupDirectory.uri);
|
||||
} else {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const path = await storage.checkAndCreateDir("/backups/");
|
||||
files = await RNFetchBlob.fs.lstat(path);
|
||||
}
|
||||
files = files
|
||||
.filter((file) => {
|
||||
const name =
|
||||
Platform.OS === "android"
|
||||
? (file as ScopedStorage.FileType).name
|
||||
: (file as ReactNativeBlobUtilStat).filename;
|
||||
return name.endsWith(".nnbackup") || name.endsWith(".nnbackupz");
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
const timeA = a.lastModified;
|
||||
const timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
|
||||
setFiles(files);
|
||||
BACKUP_FILES_CACHE.splice(0, BACKUP_FILES_CACHE.length, ...files);
|
||||
} catch (e) {
|
||||
e;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({
|
||||
item,
|
||||
index
|
||||
}: {
|
||||
item: ReactNativeBlobUtilStat | ScopedStorage.FileType;
|
||||
index: number;
|
||||
}) => <BackupItem item={item} index={index} />,
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlatList
|
||||
data={[0]}
|
||||
renderItem={() => {
|
||||
return (
|
||||
<View>
|
||||
<SectionItem
|
||||
item={{
|
||||
id: "restore-from-files",
|
||||
name: "Restore from files",
|
||||
icon: "folder",
|
||||
modifer: async () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
copyTo: "cachesDirectory"
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
restoreBackup({
|
||||
uri:
|
||||
Platform.OS === "android"
|
||||
? (("file://" + file.fileCopyUri) as string)
|
||||
: (file.fileCopyUri as string),
|
||||
deleteFile: true
|
||||
});
|
||||
},
|
||||
description:
|
||||
"Select a backup file from your device to restore backup"
|
||||
}}
|
||||
/>
|
||||
|
||||
{Platform.OS === "android" ? (
|
||||
<SectionItem
|
||||
item={{
|
||||
id: "select-backup-folder",
|
||||
name: "Select folder with backup files",
|
||||
icon: "folder",
|
||||
modifer: async () => {
|
||||
const folder = await ScopedStorage.openDocumentTree(true);
|
||||
let subfolder;
|
||||
if (folder.name !== "Notesnook backups") {
|
||||
subfolder = await ScopedStorage.createDirectory(
|
||||
folder.uri,
|
||||
"Notesnook backups"
|
||||
);
|
||||
} else {
|
||||
subfolder = folder;
|
||||
}
|
||||
SettingsService.set({
|
||||
backupDirectoryAndroid: subfolder
|
||||
});
|
||||
setBackupDirectoryAndroid(subfolder);
|
||||
setLoading(true);
|
||||
checkBackups();
|
||||
},
|
||||
description:
|
||||
"Select folder where Notesnook backup files are stored to view and restore them from the app"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
ListHeaderComponent={
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.accent} size={SIZE.xs}>
|
||||
RECENT BACKUPS
|
||||
</Heading>
|
||||
</View>
|
||||
}
|
||||
stickyHeaderIndices={[0]}
|
||||
ListEmptyComponent={
|
||||
loading ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 300,
|
||||
paddingHorizontal: 50
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
height: 300,
|
||||
paddingHorizontal: 50
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
No backups were found.{" "}
|
||||
{!backupDirectoryAndroid
|
||||
? `Please select a folder with backup files to view them here.`
|
||||
: null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
windowSize={2}
|
||||
keyExtractor={(item) =>
|
||||
(item as ScopedStorage.FileType).name ||
|
||||
(item as ReactNativeBlobUtilStat).filename
|
||||
}
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
}
|
||||
data={files}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BackupItem = ({
|
||||
item,
|
||||
index
|
||||
}: {
|
||||
item: ReactNativeBlobUtilStat | ScopedStorage.FileType;
|
||||
index: number;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const isLegacyBackup =
|
||||
item.path?.endsWith(".nnbackup") ||
|
||||
(item as ScopedStorage.FileType).uri?.endsWith(".nnbackup");
|
||||
const itemName = (
|
||||
(item as ReactNativeBlobUtilStat).filename ||
|
||||
(item as ScopedStorage.FileType).name
|
||||
)
|
||||
.replace(".nnbackupz", "")
|
||||
.replace(".nnbackup", "");
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: colors.primary.border,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Paragraph size={SIZE.sm}>{itemName}</Paragraph>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ width: "100%", maxWidth: "100%" }}
|
||||
>
|
||||
Created on {getFormattedDate(item?.lastModified, "date-time")}
|
||||
{isLegacyBackup ? "(Legacy backup)" : ""} (
|
||||
{formatBytes((item as ReactNativeBlobUtilStat).size)})
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Button
|
||||
title="Restore"
|
||||
type="secondaryAccented"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 35
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
title: `Restore ${itemName}`,
|
||||
paragraph: `Are you sure you want to restore this backup?`,
|
||||
positiveText: "Restore",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
console.log("file path", (item as ScopedStorage.FileType).uri);
|
||||
restoreBackup({
|
||||
uri:
|
||||
Platform.OS === "android"
|
||||
? (item as ScopedStorage.FileType).uri
|
||||
: (item as ReactNativeBlobUtilStat).path
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
213
apps/mobile/app/screens/settings/server-config.tsx
Normal file
213
apps/mobile/app/screens/settings/server-config.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
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 { isServerCompatible } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Input from "../../components/ui/input";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { HostId, HostIds } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
export const ServerIds = ["notesnook-sync", "auth", "sse"] as const;
|
||||
export type ServerId = (typeof ServerIds)[number];
|
||||
type Server = {
|
||||
id: ServerId;
|
||||
host: HostId;
|
||||
title: string;
|
||||
example: string;
|
||||
description: string;
|
||||
};
|
||||
type VersionResponse = {
|
||||
version: number;
|
||||
id: string;
|
||||
instance: string;
|
||||
};
|
||||
const SERVERS: Server[] = [
|
||||
{
|
||||
id: "notesnook-sync",
|
||||
host: "API_HOST",
|
||||
title: "Sync server",
|
||||
example: "http://localhost:4326",
|
||||
description: "Server used to sync your notes & other data between devices."
|
||||
},
|
||||
{
|
||||
id: "auth",
|
||||
host: "AUTH_HOST",
|
||||
title: "Auth server",
|
||||
example: "http://localhost:5326",
|
||||
description: "Server used for login/sign up and authentication."
|
||||
},
|
||||
{
|
||||
id: "sse",
|
||||
host: "SSE_HOST",
|
||||
title: "Events server",
|
||||
example: "http://localhost:7326",
|
||||
description: "Server used to receive important notifications & events."
|
||||
}
|
||||
];
|
||||
export function ServersConfiguration() {
|
||||
const { colors } = useThemeColors();
|
||||
const [error, setError] = useState<string>();
|
||||
const [success, setSuccess] = useState<boolean>();
|
||||
const [urls, setUrls] = useState<Partial<Record<HostId, string>>>(
|
||||
SettingsService.getProperty("serverUrls") || {}
|
||||
);
|
||||
const isLoggedIn = useUserStore((state) => !!state.user);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
gap: 12,
|
||||
marginTop: 12
|
||||
}}
|
||||
>
|
||||
{isLoggedIn ? (
|
||||
<Notice
|
||||
text="You must log out in order to change/reset server URLs."
|
||||
type="alert"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<View style={{ flexDirection: "column" }}>
|
||||
{SERVERS.map((server) => (
|
||||
<Input
|
||||
key={server.id}
|
||||
editable={!isLoggedIn}
|
||||
placeholder={`${server.id} e.g. ${server.example}`}
|
||||
validationType="url"
|
||||
defaultValue={urls[server.host]}
|
||||
errorMessage="Please enter a valid URL."
|
||||
onChangeText={(value) =>
|
||||
setUrls((s) => {
|
||||
s[server.host] = value;
|
||||
return s;
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingVertical: 12
|
||||
}}
|
||||
color={colors.error.paragraph}
|
||||
>
|
||||
{error}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{success === true ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingVertical: 12
|
||||
}}
|
||||
color={colors.success.paragraph}
|
||||
>
|
||||
Connected to all servers sucessfully.
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<View style={{ marginTop: 1, justifyContent: "flex-end", gap: 12 }}>
|
||||
<Button
|
||||
disabled={isLoggedIn}
|
||||
type="secondary"
|
||||
width="100%"
|
||||
onPress={async () => {
|
||||
try {
|
||||
for (const host of HostIds) {
|
||||
const url = urls[host];
|
||||
const server = SERVERS.find((s) => s.host === host)!;
|
||||
if (!url) throw new Error("All server urls are required.");
|
||||
const version = await fetch(`${url}/version`)
|
||||
.then((r) => r.json() as Promise<VersionResponse>)
|
||||
.catch(() => undefined);
|
||||
if (!version)
|
||||
throw new Error(`Could not connect to ${server.title}.`);
|
||||
if (version.id !== server.id)
|
||||
throw new Error(
|
||||
`The URL you have given (${url}) does not point to the ${server.title}.`
|
||||
);
|
||||
if (!isServerCompatible(version.version)) {
|
||||
throw new Error(
|
||||
`The server version is not compatible with the app.`
|
||||
);
|
||||
}
|
||||
}
|
||||
setSuccess(true);
|
||||
setError(undefined);
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
}
|
||||
}}
|
||||
title="Test connection"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="accent"
|
||||
disabled={isLoggedIn}
|
||||
width="100%"
|
||||
onPress={async () => {
|
||||
if (!success) {
|
||||
ToastManager.show({
|
||||
heading: "Test connection before changing server urls"
|
||||
});
|
||||
return;
|
||||
}
|
||||
SettingsService.setProperty(
|
||||
"serverUrls",
|
||||
urls as Record<HostId, string>
|
||||
);
|
||||
|
||||
presentDialog({
|
||||
title: "Server url changed",
|
||||
paragraph: "Restart the app for changes to take effect.",
|
||||
negativeText: "Done"
|
||||
});
|
||||
}}
|
||||
title="Save"
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={isLoggedIn}
|
||||
type="error"
|
||||
width="100%"
|
||||
title="Reset server urls"
|
||||
onPress={async () => {
|
||||
if (isLoggedIn) return;
|
||||
SettingsService.setProperty("serverUrls", undefined);
|
||||
presentDialog({
|
||||
title: "Server urls reset",
|
||||
paragraph: "Restart the app for changes to take effect.",
|
||||
negativeText: "Done"
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import notifee from "@notifee/react-native";
|
||||
import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
@@ -24,12 +25,19 @@ import { Appearance, Linking, Platform } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { db } from "../../common/database";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { AttachmentDialog } from "../../components/attachments";
|
||||
import { ChangePassword } from "../../components/auth/change-password";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { AppLockPassword } from "../../components/dialogs/applock-password";
|
||||
import {
|
||||
endProgress,
|
||||
startProgress,
|
||||
updateProgress
|
||||
} from "../../components/dialogs/progress";
|
||||
import { ChangeEmail } from "../../components/sheets/change-email";
|
||||
import ExportNotesSheet from "../../components/sheets/export-notes";
|
||||
import { Issue } from "../../components/sheets/github/issue";
|
||||
@@ -42,6 +50,7 @@ import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
openVault,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
@@ -60,8 +69,7 @@ import {
|
||||
eCloseSheet,
|
||||
eCloseSimpleDialog,
|
||||
eOpenLoginDialog,
|
||||
eOpenRecoveryKeyDialog,
|
||||
eOpenRestoreDialog
|
||||
eOpenRecoveryKeyDialog
|
||||
} from "../../utils/events";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { sleep } from "../../utils/time";
|
||||
@@ -70,7 +78,6 @@ import { useDragState } from "./editor/state";
|
||||
import { verifyUser, verifyUserWithApplock } from "./functions";
|
||||
import { SettingSection } from "./types";
|
||||
import { getTimeLeft } from "./user-section";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
|
||||
type User = any;
|
||||
|
||||
@@ -331,45 +338,144 @@ export const settingsGroups: SettingSection[] = [
|
||||
},
|
||||
description: "Verify your subscription to Notesnook Pro"
|
||||
},
|
||||
{
|
||||
id: "clear-cache",
|
||||
name: "Clear cache",
|
||||
icon: "delete",
|
||||
modifer: async () => {
|
||||
presentDialog({
|
||||
title: "Clear cache",
|
||||
paragraph: "Are you sure you want to clear the cache?",
|
||||
positiveText: "Clear",
|
||||
positivePress: async () => {
|
||||
filesystem.clearCache();
|
||||
ToastManager.show({
|
||||
heading: "Cache cleared",
|
||||
message: "All cached attachments have been removed",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
description(current) {
|
||||
return `Clear all cached attachments. Current cache size: ${
|
||||
current as number
|
||||
}`;
|
||||
},
|
||||
useHook: () => {
|
||||
const [cacheSize, setCacheSize] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
filesystem.getCacheSize().then(setCacheSize).catch(console.log);
|
||||
const sub = eSubscribeEvent("cache-cleared", () => {
|
||||
setCacheSize(0);
|
||||
});
|
||||
return () => {
|
||||
sub?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
return formatBytes(cacheSize);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "logout",
|
||||
name: "Log out",
|
||||
name: "Logout",
|
||||
description:
|
||||
"Logging out will clear all data stored on THIS DEVICE.",
|
||||
"Logging out will clear all data stored on this device.",
|
||||
icon: "logout",
|
||||
modifer: () => {
|
||||
modifer: async () => {
|
||||
const hasUnsyncedChanges = await db.hasUnsyncedChanges();
|
||||
presentDialog({
|
||||
title: "Logout",
|
||||
paragraph:
|
||||
"Logging out will clear all data stored on THIS DEVICE. Make sure you have synced all your changes before logging out.",
|
||||
"Are you sure you want to logout and clear all data stored on this device?",
|
||||
positiveText: "Logout",
|
||||
positivePress: async () => {
|
||||
try {
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setTimeout(async () => {
|
||||
eSendEvent("settings-loading", true);
|
||||
Navigation.popToTop();
|
||||
check: {
|
||||
info: "Take a backup before logging out",
|
||||
defaultValue: true
|
||||
},
|
||||
notice: hasUnsyncedChanges
|
||||
? {
|
||||
text: "You have unsynced notes. Take a backup or sync your notes to avoid losing your critical data.",
|
||||
type: "alert"
|
||||
}
|
||||
: undefined,
|
||||
positivePress: async (_, takeBackup) => {
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
startProgress({
|
||||
fillBackground: true,
|
||||
title: "Logging out",
|
||||
canHideProgress: true,
|
||||
paragraph:
|
||||
"Please wait while we log out and clear app data."
|
||||
});
|
||||
|
||||
Navigation.navigate("Notes");
|
||||
|
||||
if (takeBackup) {
|
||||
updateProgress({
|
||||
progress: "Taking a backup of your notes"
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await BackupService.run(
|
||||
false,
|
||||
"local",
|
||||
"partial"
|
||||
);
|
||||
if (result.error) throw result.error as Error;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
const error = e;
|
||||
const canLogout = await new Promise((resolve) => {
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Backup failed",
|
||||
paragraph: `${
|
||||
(error as Error).message
|
||||
}. Do you want to continue logging out?`,
|
||||
positiveText: "Continue",
|
||||
positivePress: () => {
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!canLogout) {
|
||||
endProgress();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress({
|
||||
progress: "Logging out... please wait"
|
||||
});
|
||||
|
||||
await db.user?.logout();
|
||||
setLoginMessage();
|
||||
await PremiumService.setPremiumStatus();
|
||||
await BiometricService.resetCredentials();
|
||||
MMKV.clearStore();
|
||||
clearAllStores();
|
||||
refreshAllStores();
|
||||
setImmediate(() => {
|
||||
refreshAllStores();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
SettingsService.resetSettings();
|
||||
useUserStore.getState().setUser(null);
|
||||
useUserStore.getState().setSyncing(false);
|
||||
Navigation.goBack();
|
||||
Navigation.popToTop();
|
||||
setTimeout(() => {
|
||||
eSendEvent("settings-loading", false);
|
||||
}, 3000);
|
||||
}, 300);
|
||||
} catch (e) {
|
||||
ToastManager.error(e as Error, "Error logging out");
|
||||
eSendEvent("settings-loading", false);
|
||||
}
|
||||
endProgress();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
ToastManager.error(e as Error, "Error logging out");
|
||||
endProgress();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -395,12 +501,18 @@ export const settingsGroups: SettingSection[] = [
|
||||
try {
|
||||
const verified = await db.user?.verifyPassword(value);
|
||||
if (verified) {
|
||||
eSendEvent("settings-loading", true);
|
||||
await db.user?.deleteUser(value);
|
||||
await BiometricService.resetCredentials();
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
setTimeout(async () => {
|
||||
startProgress({
|
||||
title: "Deleting account",
|
||||
paragraph: "Please wait while we delete your account"
|
||||
});
|
||||
Navigation.navigate("Notes");
|
||||
await db.user?.deleteUser(value);
|
||||
await BiometricService.resetCredentials();
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
}, 300);
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
@@ -411,9 +523,9 @@ export const settingsGroups: SettingSection[] = [
|
||||
});
|
||||
}
|
||||
|
||||
eSendEvent("settings-loading", false);
|
||||
endProgress();
|
||||
} catch (e) {
|
||||
eSendEvent("settings-loading", false);
|
||||
endProgress();
|
||||
console.log(e);
|
||||
ToastManager.error(
|
||||
e as Error,
|
||||
@@ -434,6 +546,25 @@ export const settingsGroups: SettingSection[] = [
|
||||
type: "screen",
|
||||
icon: "autorenew",
|
||||
sections: [
|
||||
{
|
||||
id: "offline-mode",
|
||||
name: "Full offline mode",
|
||||
description: "Download everything including attachments on sync",
|
||||
type: "switch",
|
||||
property: "offlineMode",
|
||||
modifer: () => {
|
||||
const current = SettingsService.get().offlineMode;
|
||||
if (current) {
|
||||
SettingsService.setProperty("offlineMode", false);
|
||||
db.fs().cancel("offline-mode");
|
||||
return;
|
||||
}
|
||||
PremiumService.verify(() => {
|
||||
SettingsService.setProperty("offlineMode", true);
|
||||
db.attachments.cacheAttachments().catch(console.log);
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "auto-sync",
|
||||
name: "Disable auto sync",
|
||||
@@ -689,6 +820,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
type: "switch"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "servers",
|
||||
type: "screen",
|
||||
name: "Servers",
|
||||
description: "Configure server URLs for Notesnook",
|
||||
icon: "server",
|
||||
component: "server-config"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -696,15 +835,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
id: "privacy-security",
|
||||
name: "Privacy and security",
|
||||
sections: [
|
||||
{
|
||||
id: "temeltery",
|
||||
type: "switch",
|
||||
name: "Telemetry",
|
||||
icon: "radar",
|
||||
description:
|
||||
"Contribute towards a better Notesnook. All tracking information is anonymous.",
|
||||
property: "telemetry"
|
||||
},
|
||||
{
|
||||
id: "marketing-emails",
|
||||
type: "switch",
|
||||
@@ -1041,7 +1171,8 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "backup-now",
|
||||
name: "Backup now",
|
||||
description: "Create a backup of your data",
|
||||
description:
|
||||
"Take a partial backup of your data that does not include attachments",
|
||||
modifer: async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user || SettingsService.getProperty("encryptedBackup")) {
|
||||
@@ -1052,14 +1183,41 @@ export const settingsGroups: SettingSection[] = [
|
||||
verifyUser(null, () => BackupService.run(true));
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "backup-now",
|
||||
name: "Backup now with attachments",
|
||||
hidden: () => !useUserStore.getState().user,
|
||||
description: "Take a full backup of your data with all attachments",
|
||||
modifer: async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user || SettingsService.getProperty("encryptedBackup")) {
|
||||
await BackupService.run(true, undefined, "full");
|
||||
return;
|
||||
}
|
||||
|
||||
verifyUser(null, () =>
|
||||
BackupService.run(true, undefined, "full")
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "auto-backups",
|
||||
type: "component",
|
||||
name: "Automatic backups",
|
||||
description:
|
||||
"Backup your data once every week or daily automatically.",
|
||||
"Set the interval to create a partial backup (without attachments) automatically.",
|
||||
component: "autobackups"
|
||||
},
|
||||
{
|
||||
id: "auto-backups-with-attachments",
|
||||
type: "component",
|
||||
hidden: () => !useUserStore.getState().user,
|
||||
name: "Automatic backups with attachments",
|
||||
description: `Set the interval to create a backup (with attachments) automatically.
|
||||
|
||||
NOTE: Creating a backup with attachments can take a while, and also fail completely. The app will try to resume/restart the backup in case of interruptions.`,
|
||||
component: "autobackupsattachments"
|
||||
},
|
||||
{
|
||||
id: "select-backup-dir",
|
||||
name: "Select backup directory",
|
||||
@@ -1151,9 +1309,8 @@ export const settingsGroups: SettingSection[] = [
|
||||
id: "restore-backup",
|
||||
name: "Restore backup",
|
||||
description: "Restore backup from phone storage.",
|
||||
modifer: () => {
|
||||
eSendEvent(eOpenRestoreDialog);
|
||||
}
|
||||
type: "screen",
|
||||
component: "backuprestore"
|
||||
},
|
||||
{
|
||||
id: "export-notes",
|
||||
|
||||
@@ -16,14 +16,15 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Platform } from "react-native";
|
||||
import BackgroundFetch from "@ammarahmed/react-native-background-fetch";
|
||||
import { AppRegistry, AppState, Platform } from "react-native";
|
||||
import {
|
||||
beginBackgroundTask,
|
||||
endBackgroundTask
|
||||
} from "react-native-begin-background-task";
|
||||
import BackgroundFetch from "@ammarahmed/react-native-background-fetch";
|
||||
import { DatabaseLogger, db, setupDatabase } from "../common/database";
|
||||
import { AppState, AppRegistry } from "react-native";
|
||||
import { deleteDCacheFiles } from "../common/filesystem/io";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import Notifications from "./notifications";
|
||||
import SettingsService from "./settings";
|
||||
|
||||
@@ -114,15 +115,19 @@ async function onBackgroundSyncStarted() {
|
||||
await db.init();
|
||||
}
|
||||
const user = await db.user?.getUser();
|
||||
if (user) {
|
||||
if (user && !useUserStore.getState().syncing) {
|
||||
useUserStore.getState().setSyncing(true);
|
||||
await db.sync({
|
||||
type: "full",
|
||||
force: false
|
||||
});
|
||||
useUserStore.getState().setSyncing(false);
|
||||
}
|
||||
await Notifications.setupReminders();
|
||||
deleteDCacheFiles();
|
||||
DatabaseLogger.info("BACKGROUND SYNC COMPLETE");
|
||||
} catch (e) {
|
||||
useUserStore.getState().setSyncing(false);
|
||||
DatabaseLogger.error(e as Error);
|
||||
console.log("BACKGROUND SYNC ERROR", (e as Error).message);
|
||||
}
|
||||
@@ -137,6 +142,7 @@ const onBoot = async () => {
|
||||
|
||||
DatabaseLogger.info("BOOT TASK STARTED");
|
||||
if (!db.isInitialized) {
|
||||
await setupDatabase();
|
||||
await db.init();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { formatDate } from "@notesnook/core/dist/utils/date";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
@@ -25,8 +27,14 @@ import Share from "react-native-share";
|
||||
import { zip } from "react-native-zip-archive";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import storage from "../common/database/storage";
|
||||
import filesystem from "../common/filesystem";
|
||||
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
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";
|
||||
@@ -37,14 +45,14 @@ const MS_WEEK = MS_DAY * 7;
|
||||
const MONTH = MS_DAY * 30;
|
||||
|
||||
async function getDirectoryAndroid() {
|
||||
let folder = await ScopedStorage.openDocumentTree(true);
|
||||
const folder = await ScopedStorage.openDocumentTree(true);
|
||||
if (!folder) return null;
|
||||
let subfolder;
|
||||
if (!folder.name.includes("Notesnook backups")) {
|
||||
let folderFiles = await ScopedStorage.listFiles(folder.uri);
|
||||
for (let f of folderFiles) {
|
||||
if (f.type === "directory" && f.name === "Notesnook backups") {
|
||||
subfolder = f;
|
||||
const files = await ScopedStorage.listFiles(folder.uri);
|
||||
for (const file of files) {
|
||||
if (file.type === "directory" && file.name === "Notesnook backups") {
|
||||
subfolder = file;
|
||||
}
|
||||
}
|
||||
if (!subfolder) {
|
||||
@@ -67,10 +75,10 @@ async function checkBackupDirExists(reset = false, context = "global") {
|
||||
let dir = SettingsService.get().backupDirectoryAndroid;
|
||||
if (reset) dir = null;
|
||||
if (dir) {
|
||||
let allDirs = await ScopedStorage.getPersistedUriPermissions();
|
||||
const allDirs = await ScopedStorage.getPersistedUriPermissions();
|
||||
let exists = allDirs.findIndex((d) => {
|
||||
return d === dir.uri || dir.uri.includes(d);
|
||||
});
|
||||
return d === dir?.uri || dir?.uri.includes(d);
|
||||
}) as number | boolean;
|
||||
exists = exists !== -1;
|
||||
dir = exists ? dir : null;
|
||||
}
|
||||
@@ -100,7 +108,7 @@ async function checkBackupDirExists(reset = false, context = "global") {
|
||||
return dir;
|
||||
}
|
||||
|
||||
async function presentBackupCompleteSheet(backupFilePath) {
|
||||
async function presentBackupCompleteSheet(backupFilePath: string) {
|
||||
presentSheet({
|
||||
title: "Backup complete",
|
||||
icon: "cloud-upload",
|
||||
@@ -123,7 +131,7 @@ async function presentBackupCompleteSheet(backupFilePath) {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
}).catch(console.log);
|
||||
} as any).catch(console.log);
|
||||
}
|
||||
},
|
||||
actionText: "Share"
|
||||
@@ -142,10 +150,10 @@ async function presentBackupCompleteSheet(backupFilePath) {
|
||||
});
|
||||
}
|
||||
|
||||
async function updateNextBackupTime() {
|
||||
async function updateNextBackupTime(type: "full" | "partial") {
|
||||
SettingsService.set({
|
||||
nextBackupRequestTime: Date.now() + 86400000 * 3,
|
||||
lastBackupDate: Date.now()
|
||||
[type === "full" ? "lastFullBackupDate" : "lastBackupDate"]: Date.now()
|
||||
});
|
||||
}
|
||||
/**
|
||||
@@ -153,77 +161,124 @@ async function updateNextBackupTime() {
|
||||
* @param {string=} context
|
||||
* @returns {Promise<{path?: string, error?: Error, report?: boolean}}>
|
||||
*/
|
||||
async function run(progress = false, context) {
|
||||
let androidBackupDirectory = await checkBackupDirExists(false, context);
|
||||
async function run(
|
||||
progress = false,
|
||||
context?: string,
|
||||
backupType: "full" | "partial" = "partial"
|
||||
) {
|
||||
console.log("Creating backup:", backupType, progress, context);
|
||||
|
||||
const androidBackupDirectory = (await checkBackupDirExists(
|
||||
false,
|
||||
context
|
||||
)) as ScopedStorage.FileType;
|
||||
|
||||
if (!androidBackupDirectory)
|
||||
return {
|
||||
error: new Error("Backup directory not selected"),
|
||||
report: false
|
||||
};
|
||||
|
||||
let path;
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
path = await storage.checkAndCreateDir("/backups");
|
||||
}
|
||||
|
||||
const backupFileName = sanitizeFilename(
|
||||
`${formatDate(Date.now(), {
|
||||
type: "date-time",
|
||||
dateFormat: "YYYY-MM-DD",
|
||||
timeFormat: "24-hour"
|
||||
})}-${new Date().getSeconds()}${backupType === "full" ? "-full" : ""}`,
|
||||
{ replacement: "-" }
|
||||
);
|
||||
|
||||
if (progress) {
|
||||
presentSheet({
|
||||
title: "Backing up your data",
|
||||
paragraph:
|
||||
"All your backups are stored in 'Phone Storage/Notesnook/backups/' folder",
|
||||
progress: true
|
||||
startProgress({
|
||||
title: `Creating ${backupType === "full" ? "a full " : ""}backup`,
|
||||
paragraph: `Please wait while we create a backup of your data. This may take a few minutes.`,
|
||||
progress: undefined
|
||||
});
|
||||
}
|
||||
|
||||
let path;
|
||||
let backupFileName = "notesnook_backup_" + Date.now();
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
path = await storage.checkAndCreateDir("/backups/");
|
||||
}
|
||||
|
||||
const zipSourceFolder = `${cacheDir}/${backupFileName}`;
|
||||
const zipSourceFolder = `${cacheDir}/backup_temp`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${path}/${backupFileName}.nnbackupz`
|
||||
: `${cacheDir}/${backupFileName}.nnbackupz`;
|
||||
|
||||
try {
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
} catch (e) {}
|
||||
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
const attachmentsDir = zipSourceFolder + "/attachments";
|
||||
if (backupType === "full") {
|
||||
await RNFetchBlob.fs.mkdir(attachmentsDir);
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await db.user.getUser();
|
||||
for await (const file of db.backup.export(
|
||||
"mobile",
|
||||
SettingsService.get().encryptedBackup && user
|
||||
)) {
|
||||
console.log("Writing backup chunk of size...", file?.data?.length);
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
`${zipSourceFolder}/${file.path}`,
|
||||
file.data,
|
||||
"utf8"
|
||||
);
|
||||
for await (const file of db.backup.export({
|
||||
type: "mobile",
|
||||
encrypt: SettingsService.get().encryptedBackup && !!user,
|
||||
mode: backupType
|
||||
})) {
|
||||
if (file.type === "file") {
|
||||
updateProgress({
|
||||
progress: `Writing backup chunk of size... ${file?.data?.length}`
|
||||
});
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
`${zipSourceFolder}/${file.path}`,
|
||||
file.data,
|
||||
"utf8"
|
||||
);
|
||||
} else if (file.type === "attachment") {
|
||||
updateProgress({
|
||||
progress: `Saving attachments in backup... ${file.hash}`
|
||||
});
|
||||
if (await filesystem.exists(file.hash)) {
|
||||
await RNFetchBlob.fs.cp(
|
||||
`${cacheDir}/${file.hash}`,
|
||||
`${attachmentsDir}/${file.hash}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress({
|
||||
progress: "Creating backup zip file..."
|
||||
});
|
||||
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
androidBackupDirectory.uri,
|
||||
androidBackupDirectory?.uri,
|
||||
`${backupFileName}.nnbackupz`,
|
||||
"application/nnbackupz"
|
||||
);
|
||||
console.log("Copying zip file...");
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
console.log("Copyied zip file...");
|
||||
|
||||
console.log("Copied zip file...");
|
||||
path = file.uri;
|
||||
} else {
|
||||
path = zipOutputFile;
|
||||
}
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
updateNextBackupTime();
|
||||
|
||||
let showBackupCompleteSheet =
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
|
||||
updateNextBackupTime(backupType || "partial");
|
||||
|
||||
if (progress) {
|
||||
endProgress();
|
||||
}
|
||||
|
||||
const canShowCompletionStatus =
|
||||
progress && SettingsService.get().showBackupCompleteSheet;
|
||||
|
||||
if (context) {
|
||||
@@ -234,15 +289,12 @@ async function run(progress = false, context) {
|
||||
|
||||
await sleep(300);
|
||||
|
||||
if (showBackupCompleteSheet) {
|
||||
if (canShowCompletionStatus) {
|
||||
presentBackupCompleteSheet(path);
|
||||
} else {
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
}
|
||||
|
||||
ToastManager.show({
|
||||
heading: "Backup successful",
|
||||
message: "Your backup is stored in Notesnook folder on your phone.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -251,16 +303,26 @@ async function run(progress = false, context) {
|
||||
path: path
|
||||
};
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Backup failed", context || "global");
|
||||
ToastManager.error(e as Error, "Backup failed", context || "global");
|
||||
|
||||
if (e?.message?.includes("android.net.Uri") && androidBackupDirectory) {
|
||||
if (
|
||||
(e as Error)?.message?.includes("android.net.Uri") &&
|
||||
androidBackupDirectory
|
||||
) {
|
||||
SettingsService.setProperty("backupDirectoryAndroid", null);
|
||||
return run(progress, context);
|
||||
return run(progress, context, backupType);
|
||||
}
|
||||
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
|
||||
DatabaseLogger.error(e);
|
||||
await sleep(300);
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
if (progress) {
|
||||
endProgress();
|
||||
}
|
||||
return {
|
||||
error: e,
|
||||
report: true
|
||||
@@ -268,34 +330,41 @@ async function run(progress = false, context) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getLastBackupDate() {
|
||||
return SettingsService.get().lastBackupDate;
|
||||
}
|
||||
async function checkBackupRequired(
|
||||
type: "daily" | "off" | "useroff" | "weekly" | "monthly" | "never",
|
||||
lastBackupDateType: "lastBackupDate" | "lastFullBackupDate" = "lastBackupDate"
|
||||
) {
|
||||
console.log(lastBackupDateType, type);
|
||||
if (type === "off" || type === "useroff" || type === "never" || !type) return;
|
||||
const now = Date.now();
|
||||
const lastBackupDate = SettingsService.getProperty(lastBackupDateType) as
|
||||
| number
|
||||
| "never";
|
||||
|
||||
async function checkBackupRequired(type) {
|
||||
if (type === "off" || type === "useroff") return;
|
||||
let now = Date.now();
|
||||
let lastBackupDate = await getLastBackupDate();
|
||||
if (!lastBackupDate || lastBackupDate === "never") {
|
||||
if (
|
||||
lastBackupDate === undefined ||
|
||||
lastBackupDate === "never" ||
|
||||
lastBackupDate === 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
lastBackupDate = parseInt(lastBackupDate);
|
||||
|
||||
if (type === "daily" && lastBackupDate + MS_DAY < now) {
|
||||
DatabaseLogger.info("Daily backup started");
|
||||
DatabaseLogger.info(`Daily backup started: ${type}`);
|
||||
return true;
|
||||
} else if (type === "weekly" && lastBackupDate + MS_WEEK < now) {
|
||||
DatabaseLogger.info("Weekly backup started");
|
||||
DatabaseLogger.info(`Weekly backup started: ${type}`);
|
||||
return true;
|
||||
} else if (type === "monthly" && lastBackupDate + MONTH < now) {
|
||||
DatabaseLogger.info("Monthly backup started");
|
||||
DatabaseLogger.info(`Monthly backup started: ${type}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const checkAndRun = async () => {
|
||||
let settings = SettingsService.get();
|
||||
if (await checkBackupRequired(settings.reminder)) {
|
||||
const settings = SettingsService.get();
|
||||
if (await checkBackupRequired(settings?.reminder)) {
|
||||
try {
|
||||
await run();
|
||||
} catch (e) {
|
||||
@@ -30,6 +30,7 @@ import { scale, updateSize } from "../utils/size";
|
||||
import { DatabaseLogger } from "../common/database";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
|
||||
function reset() {
|
||||
const settings = get();
|
||||
if (settings.reminder !== "off" && settings.reminder !== "useroff") {
|
||||
@@ -40,10 +41,15 @@ function reset() {
|
||||
}
|
||||
|
||||
function resetSettings() {
|
||||
MMKV.setString(
|
||||
"appSettings",
|
||||
JSON.stringify({ ...defaultSettings, introCompleted: true })
|
||||
);
|
||||
const settings: SettingStore["settings"] = {
|
||||
...defaultSettings,
|
||||
introCompleted: true,
|
||||
serverUrls: get().serverUrls,
|
||||
backupDirectoryAndroid: undefined
|
||||
};
|
||||
|
||||
MMKV.setString("appSettings", JSON.stringify(settings));
|
||||
set(settings);
|
||||
init();
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ export const ignoredMessages = [
|
||||
"Sync already running",
|
||||
"Not allowed to start service intent",
|
||||
"WebSocket failed to connect",
|
||||
"Failed to start the HttpConnection before"
|
||||
"Failed to start the HttpConnection before",
|
||||
"Could not connect to the Sync server",
|
||||
"Network request failed"
|
||||
];
|
||||
let pendingSync: any = undefined;
|
||||
let syncTimer: NodeJS.Timeout;
|
||||
@@ -61,58 +63,45 @@ const run = async (
|
||||
|
||||
clearTimeout(syncTimer);
|
||||
syncTimer = setTimeout(async () => {
|
||||
const status = await NetInfo.fetch();
|
||||
const userstore = useUserStore.getState();
|
||||
userstore.setSyncing(true);
|
||||
const user = await db.user.getUser();
|
||||
if (!status.isInternetReachable) {
|
||||
DatabaseLogger.warn("Internet not reachable");
|
||||
}
|
||||
|
||||
if (
|
||||
!user ||
|
||||
!status.isInternetReachable ||
|
||||
SettingsService.get().disableSync
|
||||
) {
|
||||
if (!user || SettingsService.get().disableSync) {
|
||||
userstore.setSyncing(false);
|
||||
initAfterSync();
|
||||
pendingSync = undefined;
|
||||
return onCompleted?.(SyncStatus.Failed);
|
||||
}
|
||||
userstore.setSyncing(true);
|
||||
|
||||
let error = null;
|
||||
let error: Error | undefined = undefined;
|
||||
|
||||
try {
|
||||
await BackgroundSync.doInBackground(async () => {
|
||||
try {
|
||||
await db.sync({
|
||||
type: type,
|
||||
force: forced
|
||||
force: forced,
|
||||
offlineMode: SettingsService.get().offlineMode
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
error = e as Error;
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
if (error) throw error;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
error = e as Error;
|
||||
DatabaseLogger.error(error, "[Client] Failed to sync");
|
||||
if (
|
||||
!ignoredMessages.find((message) =>
|
||||
(e as Error).message?.includes(message)
|
||||
) &&
|
||||
userstore.user &&
|
||||
status.isConnected &&
|
||||
status.isInternetReachable
|
||||
!ignoredMessages.find((message) => error?.message?.includes(message)) &&
|
||||
userstore.user
|
||||
) {
|
||||
userstore.setSyncing(false, SyncStatus.Failed);
|
||||
const status = await NetInfo.fetch();
|
||||
if (status.isConnected && status.isInternetReachable) {
|
||||
ToastManager.error(e as Error, "Sync failed", context);
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseLogger.error(e, "[Client] Failed to sync");
|
||||
} finally {
|
||||
initAfterSync();
|
||||
userstore.setSyncing(
|
||||
@@ -120,16 +109,14 @@ const run = async (
|
||||
error ? SyncStatus.Failed : SyncStatus.Passed
|
||||
);
|
||||
onCompleted?.(error ? SyncStatus.Failed : SyncStatus.Passed);
|
||||
setImmediate(() => {
|
||||
if (pendingSync)
|
||||
Sync.run(
|
||||
pendingSync.context,
|
||||
pendingSync.forced,
|
||||
pendingSync.type,
|
||||
pendingSync.onCompleted,
|
||||
pendingSync.lastSyncTime
|
||||
);
|
||||
});
|
||||
if (pendingSync)
|
||||
Sync.run(
|
||||
pendingSync.context,
|
||||
pendingSync.forced,
|
||||
pendingSync.type,
|
||||
pendingSync.onCompleted,
|
||||
pendingSync.lastSyncTime
|
||||
);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,8 @@ import { FileType } from "react-native-scoped-storage";
|
||||
import create, { State } from "zustand";
|
||||
import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme";
|
||||
import { Reminder } from "@notesnook/core/dist/types";
|
||||
export const HostIds = ["API_HOST", "AUTH_HOST", "SSE_HOST"] as const;
|
||||
export type HostId = (typeof HostIds)[number];
|
||||
|
||||
export type Settings = {
|
||||
showToolbarOnTop?: boolean;
|
||||
@@ -32,7 +34,8 @@ export type Settings = {
|
||||
fontScale?: number;
|
||||
forcePortraitOnTablet?: boolean;
|
||||
useSystemTheme?: boolean;
|
||||
reminder?: string;
|
||||
reminder: "daily" | "off" | "useroff" | "weekly" | "monthly";
|
||||
fullBackupReminder: "never" | "weekly" | "monthly";
|
||||
encryptedBackup?: boolean;
|
||||
homepage?: string;
|
||||
sort?: string;
|
||||
@@ -42,7 +45,6 @@ export type Settings = {
|
||||
appLockTimer: number;
|
||||
appLockEnabled?: boolean;
|
||||
appLockMode?: "none" | "background" | "launch";
|
||||
telemetry?: boolean;
|
||||
notebooksListMode?: "normal" | "compact";
|
||||
notesListMode?: "normal" | "compact";
|
||||
devMode?: boolean;
|
||||
@@ -52,8 +54,8 @@ export type Settings = {
|
||||
rateApp?: boolean | number;
|
||||
migrated?: boolean;
|
||||
introCompleted?: boolean;
|
||||
nextBackupRequestTime?: number | undefined;
|
||||
lastBackupDate?: number | undefined;
|
||||
nextBackupRequestTime?: number;
|
||||
lastBackupDate?: number;
|
||||
userEmailConfirmed?: boolean;
|
||||
recoveryKeySaved?: boolean;
|
||||
backupDirectoryAndroid?: FileType | null;
|
||||
@@ -82,6 +84,10 @@ export type Settings = {
|
||||
backgroundSync?: boolean;
|
||||
applockKeyboardType: "numeric" | "default";
|
||||
settingsVersion?: number;
|
||||
backupType: "full" | "partial";
|
||||
offlineMode?: boolean;
|
||||
lastFullBackupDate?: number;
|
||||
serverUrls?: Record<HostId, string>;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -127,6 +133,12 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
applockKeyboardType: "numeric",
|
||||
appLockTimer: 0,
|
||||
showToolbarOnTop: false,
|
||||
disableAutoSync: false,
|
||||
disableRealtimeSync: false,
|
||||
disableSync: false,
|
||||
appLockEnabled: false,
|
||||
backupDirectoryAndroid: null,
|
||||
offlineMode: false,
|
||||
showKeyboardOnOpen: false,
|
||||
fontScale: 1,
|
||||
forcePortraitOnTablet: false,
|
||||
@@ -139,7 +151,6 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
screenshotMode: true,
|
||||
privacyScreen: false,
|
||||
appLockMode: "none",
|
||||
telemetry: false,
|
||||
notebooksListMode: "normal",
|
||||
notesListMode: "normal",
|
||||
devMode: false,
|
||||
@@ -150,7 +161,7 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
migrated: false,
|
||||
introCompleted: Config.isTesting ? true : false,
|
||||
nextBackupRequestTime: undefined,
|
||||
lastBackupDate: undefined,
|
||||
lastBackupDate: 0,
|
||||
userEmailConfirmed: false,
|
||||
recoveryKeySaved: false,
|
||||
showBackupCompleteSheet: true,
|
||||
@@ -171,7 +182,10 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
biometricsAuthEnabled: false,
|
||||
appLockHasPasswordSecurity: false,
|
||||
backgroundSync: true,
|
||||
settingsVersion: 0
|
||||
settingsVersion: 0,
|
||||
backupType: "partial",
|
||||
fullBackupReminder: "never",
|
||||
lastFullBackupDate: 0
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
|
||||
@@ -49,7 +49,6 @@ react {
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
hermesFlags = ["-O", "-output-source-map"]
|
||||
}
|
||||
println(projectDir);
|
||||
|
||||
/**
|
||||
* Set this to true to create two separate APKs instead of one:
|
||||
@@ -117,7 +116,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3022
|
||||
versionCode 3025
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -169,8 +168,12 @@ android {
|
||||
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
jniLibs {
|
||||
useLegacyPackaging isBuildingAAB == false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
- Bug fixes and performance improvements
|
||||
- Added full offline mode with enable downloading attachments on sync
|
||||
- Now you can change sync server urls in the app settings to use your own self hosted servers
|
||||
- Now you can take backups with attachments included
|
||||
- Removed all kinds of telemetry and analytics
|
||||
- Bug fixes and small improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
Binary file not shown.
@@ -1061,7 +1061,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2110;
|
||||
CURRENT_PROJECT_VERSION = 2112;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1135,7 +1135,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.13;
|
||||
MARKETING_VERSION = 3.0.15;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1166,7 +1166,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2110;
|
||||
CURRENT_PROJECT_VERSION = 2112;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1240,7 +1240,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.13;
|
||||
MARKETING_VERSION = 3.0.15;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1399,7 +1399,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2110;
|
||||
CURRENT_PROJECT_VERSION = 2112;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1411,7 +1411,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.13;
|
||||
MARKETING_VERSION = 3.0.15;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1442,7 +1442,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2110;
|
||||
CURRENT_PROJECT_VERSION = 2112;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1455,7 +1455,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.13;
|
||||
MARKETING_VERSION = 3.0.15;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1485,7 +1485,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2110;
|
||||
CURRENT_PROJECT_VERSION = 2112;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1559,7 +1559,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.13;
|
||||
MARKETING_VERSION = 3.0.15;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1590,7 +1590,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2110;
|
||||
CURRENT_PROJECT_VERSION = 2112;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1665,7 +1665,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.13;
|
||||
MARKETING_VERSION = 3.0.15;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
141
apps/mobile/package-lock.json
generated
141
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.13",
|
||||
"version": "3.0.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.13",
|
||||
"version": "3.0.14",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -7166,7 +7166,7 @@
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/prop-types": {
|
||||
"version": "15.7.11",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/q": {
|
||||
@@ -7186,7 +7186,7 @@
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/react": {
|
||||
"version": "18.2.39",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -7217,7 +7217,7 @@
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/scheduler": {
|
||||
"version": "0.16.8",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/semver": {
|
||||
@@ -12075,7 +12075,7 @@
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/immer": {
|
||||
"version": "9.0.21",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -22464,6 +22464,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../packages/editor/node_modules/json-parse-even-better-errors": {
|
||||
@@ -22519,6 +22520,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
@@ -23040,6 +23042,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
@@ -23058,6 +23061,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
@@ -23177,6 +23181,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/scheduler": {
|
||||
"version": "0.23.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
@@ -28713,6 +28718,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
@@ -28836,6 +28842,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
@@ -29106,6 +29113,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -29119,6 +29127,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -29268,6 +29277,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -29278,6 +29288,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-unicode-property-regex": {
|
||||
"version": "7.18.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
||||
@@ -29313,6 +29324,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-class-properties": {
|
||||
"version": "7.12.13",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.12.13"
|
||||
@@ -29323,6 +29335,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-class-static-block": {
|
||||
"version": "7.14.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.14.5"
|
||||
@@ -29359,6 +29372,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-export-namespace-from": {
|
||||
"version": "7.8.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.8.3"
|
||||
@@ -29383,6 +29397,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-assertions": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -29396,6 +29411,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-attributes": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -29409,6 +29425,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-meta": {
|
||||
"version": "7.10.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
@@ -29419,6 +29436,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-json-strings": {
|
||||
"version": "7.8.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.8.0"
|
||||
@@ -29515,6 +29533,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-top-level-await": {
|
||||
"version": "7.14.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.14.5"
|
||||
@@ -29541,6 +29560,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
|
||||
"version": "7.18.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
||||
@@ -29568,6 +29588,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.22.5",
|
||||
@@ -29599,6 +29620,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-block-scoped-functions": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -29625,6 +29647,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-class-properties": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.22.5",
|
||||
@@ -29639,6 +29662,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-class-static-block": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.22.5",
|
||||
@@ -29702,6 +29726,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dotall-regex": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
|
||||
@@ -29716,6 +29741,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-duplicate-keys": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -29729,6 +29755,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dynamic-import": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -29743,6 +29770,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-exponentiation-operator": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
|
||||
@@ -29757,6 +29785,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-export-namespace-from": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -29786,6 +29815,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-for-of": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -29814,6 +29844,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-json-strings": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -29841,6 +29872,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -29855,6 +29887,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-member-expression-literals": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -29868,6 +29901,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-amd": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.22.5",
|
||||
@@ -29897,6 +29931,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-systemjs": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
@@ -29913,6 +29948,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-umd": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.22.5",
|
||||
@@ -29941,6 +29977,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-new-target": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -29968,6 +30005,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-numeric-separator": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -29982,6 +30020,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-object-rest-spread": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.22.5",
|
||||
@@ -29999,6 +30038,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-object-super": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -30013,6 +30053,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-optional-catch-binding": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -30071,6 +30112,7 @@
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz",
|
||||
"integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.24.7",
|
||||
"@babel/helper-create-class-features-plugin": "^7.24.7",
|
||||
@@ -30086,6 +30128,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-property-literals": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -30155,6 +30198,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-regenerator": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -30169,6 +30213,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-reserved-words": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -30260,6 +30305,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-typeof-symbol": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -30289,6 +30335,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-escapes": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -30302,6 +30349,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-property-regex": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
|
||||
@@ -30330,6 +30378,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
|
||||
@@ -30344,6 +30393,7 @@
|
||||
},
|
||||
"node_modules/@babel/preset-env": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.22.5",
|
||||
@@ -30436,6 +30486,7 @@
|
||||
},
|
||||
"node_modules/@babel/preset-env/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -30459,6 +30510,7 @@
|
||||
},
|
||||
"node_modules/@babel/preset-modules": {
|
||||
"version": "0.1.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
@@ -33766,6 +33818,7 @@
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.40.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
@@ -33774,6 +33827,7 @@
|
||||
},
|
||||
"node_modules/@types/eslint-scope": {
|
||||
"version": "3.7.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint": "*",
|
||||
@@ -33782,6 +33836,7 @@
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
@@ -33884,12 +33939,12 @@
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.5",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.13",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -33932,7 +33987,7 @@
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.3",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
@@ -34244,6 +34299,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/helper-numbers": "1.11.6",
|
||||
@@ -34252,18 +34308,22 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/floating-point-hex-parser": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-api-error": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-buffer": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-numbers": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
|
||||
@@ -34273,10 +34333,12 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -34287,6 +34349,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/ieee754": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xtuc/ieee754": "^1.2.0"
|
||||
@@ -34294,6 +34357,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/leb128": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@xtuc/long": "4.2.2"
|
||||
@@ -34301,10 +34365,12 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/utf8": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-edit": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -34319,6 +34385,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-gen": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -34330,6 +34397,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-opt": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -34340,6 +34408,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-parser": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -34352,6 +34421,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/wast-printer": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -34409,10 +34479,12 @@
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xtuc/long": {
|
||||
"version": "4.2.2",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@yarnpkg/lockfile": {
|
||||
@@ -34465,6 +34537,7 @@
|
||||
},
|
||||
"node_modules/acorn-import-assertions": {
|
||||
"version": "1.9.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^8"
|
||||
@@ -35378,6 +35451,7 @@
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@@ -35824,7 +35898,7 @@
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.2",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
@@ -36401,29 +36475,9 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding": {
|
||||
"version": "0.1.13",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.15.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -36528,6 +36582,7 @@
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.3.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
@@ -36863,6 +36918,7 @@
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
@@ -36874,6 +36930,7 @@
|
||||
},
|
||||
"node_modules/eslint-scope/node_modules/estraverse": {
|
||||
"version": "4.3.0",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
@@ -37040,6 +37097,7 @@
|
||||
},
|
||||
"node_modules/esrecurse": {
|
||||
"version": "4.3.0",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.2.0"
|
||||
@@ -37050,6 +37108,7 @@
|
||||
},
|
||||
"node_modules/estraverse": {
|
||||
"version": "5.3.0",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
@@ -37057,6 +37116,7 @@
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -37820,6 +37880,7 @@
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/global": {
|
||||
@@ -39866,6 +39927,7 @@
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-ref-resolver": {
|
||||
@@ -40399,6 +40461,7 @@
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.11.5"
|
||||
@@ -42629,6 +42692,7 @@
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
@@ -43598,6 +43662,7 @@
|
||||
},
|
||||
"node_modules/regenerator-transform": {
|
||||
"version": "0.15.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.4"
|
||||
@@ -43845,12 +43910,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/sanitize-filename": {
|
||||
"version": "1.6.3",
|
||||
"dev": true,
|
||||
@@ -43965,6 +44024,7 @@
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.1",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
@@ -44617,6 +44677,7 @@
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -44695,6 +44756,7 @@
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.9",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
@@ -44727,6 +44789,7 @@
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -44739,6 +44802,7 @@
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
@@ -45332,6 +45396,7 @@
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
@@ -45354,6 +45419,7 @@
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.88.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
@@ -45468,6 +45534,7 @@
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.13",
|
||||
"version": "3.0.15",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -48,4 +48,4 @@
|
||||
"react-native-mmkv-storage": "^0.10.2",
|
||||
"react-native-actions-sheet": "^0.9.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/jetifier/index.js b/node_modules/jetifier/index.js
|
||||
index ae3979c..ed609e9 100644
|
||||
--- a/node_modules/jetifier/index.js
|
||||
+++ b/node_modules/jetifier/index.js
|
||||
@@ -6,7 +6,7 @@ const cpus = require('os').cpus().length;
|
||||
|
||||
const arg = process.argv.slice(2)[0];
|
||||
const mode = arg && ((arg === 'reverse') || (arg === '-r')) ? 'reverse' : 'forward';
|
||||
-const SEARCH_DIR = 'node_modules';
|
||||
+const SEARCH_DIR = '../node_modules';
|
||||
|
||||
const classesMapping = getClassesMapping();
|
||||
const files = readDir(SEARCH_DIR);
|
||||
File diff suppressed because one or more lines are too long
@@ -45,6 +45,7 @@ const EXTRA_ICON_NAMES = [
|
||||
"view-list-outline",
|
||||
"view-list",
|
||||
"tab-plus",
|
||||
"server",
|
||||
"play",
|
||||
"pause",
|
||||
"notebook-outline",
|
||||
|
||||
1
apps/theme-builder/package-lock.json
generated
1
apps/theme-builder/package-lock.json
generated
@@ -990,6 +990,7 @@
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@emotion/react": "11.11.1",
|
||||
"@hazae41/foras": "^2.1.4",
|
||||
"@henrygd/queue": "^1.0.6",
|
||||
"@mdi/js": "^7.2.96",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@notesnook-importer/core": "^2.0.0",
|
||||
|
||||
@@ -258,7 +258,7 @@ export class NoteContextMenuModel extends BaseProperties {
|
||||
|
||||
const zip = await downloadAndReadFile(
|
||||
this.noteLocator.page(),
|
||||
this.menu.getItem(format),
|
||||
() => this.menu.getItem(format).click(),
|
||||
null
|
||||
);
|
||||
|
||||
|
||||
@@ -113,16 +113,17 @@ export class SettingsViewModel {
|
||||
const item = await this.navigation.findItem("Backup & export");
|
||||
await item?.click();
|
||||
|
||||
const backupData = this.page
|
||||
.locator(getTestId("setting-create-backup"))
|
||||
.locator("button");
|
||||
|
||||
if (password) {
|
||||
await backupData.click();
|
||||
await fillPasswordDialog(this.page, password);
|
||||
}
|
||||
|
||||
return await downloadAndReadFile(this.page, backupData, "utf-8");
|
||||
return await downloadAndReadFile(
|
||||
this.page,
|
||||
async () => {
|
||||
const backupData = this.page
|
||||
.locator(getTestId("setting-create-backup"))
|
||||
.locator("select");
|
||||
await backupData.selectOption({ value: "partial", label: "Backup" });
|
||||
if (password) await fillPasswordDialog(this.page, password);
|
||||
},
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
async restoreData(filename: string, password?: string) {
|
||||
|
||||
@@ -103,12 +103,12 @@ async function editNote(page: Page, note: Partial<Note>, noDelay = false) {
|
||||
|
||||
async function downloadAndReadFile(
|
||||
page: Page,
|
||||
action: Locator,
|
||||
action: () => Promise<void>,
|
||||
encoding: BufferEncoding | null | undefined = "utf-8"
|
||||
) {
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
await action.click()
|
||||
action()
|
||||
]);
|
||||
|
||||
const dir = fs.mkdtempSync(join(tmpdir(), "nntests_"));
|
||||
|
||||
8753
apps/web/package-lock.json
generated
8753
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.13",
|
||||
"version": "3.0.15",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -15,6 +15,7 @@
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@emotion/react": "11.11.1",
|
||||
"@hazae41/foras": "^2.1.4",
|
||||
"@henrygd/queue": "^1.0.6",
|
||||
"@mdi/js": "^7.2.96",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@notesnook-importer/core": "^2.0.0",
|
||||
|
||||
7
apps/web/patches/libsodium+0.7.13.patch
Normal file
7
apps/web/patches/libsodium+0.7.13.patch
Normal file
File diff suppressed because one or more lines are too long
@@ -22,11 +22,16 @@ import { useStore } from "./stores/app-store";
|
||||
import { useStore as useUserStore } from "./stores/user-store";
|
||||
import { useEditorStore } from "./stores/editor-store";
|
||||
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
|
||||
import { resetNotices, scheduleBackups } from "./common/notices";
|
||||
import { useStore as useSettingStore } from "./stores/setting-store";
|
||||
import {
|
||||
resetNotices,
|
||||
scheduleBackups,
|
||||
scheduleFullBackups
|
||||
} from "./common/notices";
|
||||
import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { db } from "./common/db";
|
||||
import { EV, EVENTS } from "@notesnook/core/dist/common";
|
||||
import { CHECK_IDS, EV, EVENTS } from "@notesnook/core/dist/common";
|
||||
import { registerKeyMap } from "./common/key-map";
|
||||
import { isUserPremium } from "./hooks/use-is-user-premium";
|
||||
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
|
||||
@@ -40,6 +45,7 @@ import { desktop } from "./common/desktop-bridge";
|
||||
import { BuyDialog } from "./dialogs/buy-dialog";
|
||||
import { FeatureDialog } from "./dialogs/feature-dialog";
|
||||
import { AnnouncementDialog } from "./dialogs/announcement-dialog";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
type AppEffectsProps = {
|
||||
setShow: (show: boolean) => void;
|
||||
@@ -64,11 +70,27 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
if (isUserPremium()) {
|
||||
return { type, result: true };
|
||||
} else {
|
||||
showToast(
|
||||
"error",
|
||||
"Please upgrade your account to Pro to use this feature.",
|
||||
[{ text: "Upgrade now", onClick: () => BuyDialog.show({}) }]
|
||||
);
|
||||
let sentence = "Please upgrade your account to Pro ";
|
||||
switch (type) {
|
||||
case CHECK_IDS.noteColor:
|
||||
sentence += "to add colors.";
|
||||
break;
|
||||
case CHECK_IDS.noteTag:
|
||||
sentence += "to add more tags.";
|
||||
break;
|
||||
case CHECK_IDS.notebookAdd:
|
||||
sentence += "to add more notebooks.";
|
||||
break;
|
||||
case CHECK_IDS.vaultAdd:
|
||||
sentence += "to use the notes vault.";
|
||||
break;
|
||||
default:
|
||||
sentence += "to use this feature.";
|
||||
break;
|
||||
}
|
||||
showToast("error", sentence, [
|
||||
{ text: "Upgrade now", onClick: () => BuyDialog.show({}) }
|
||||
]);
|
||||
return { type, result: false };
|
||||
}
|
||||
}
|
||||
@@ -90,6 +112,10 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
if (onboardingKey) await OnboardingDialog.show({ type: onboardingKey });
|
||||
await FeatureDialog.show({ featureName: "highlights" });
|
||||
await scheduleBackups();
|
||||
await scheduleFullBackups();
|
||||
if (useSettingStore.getState().isFullOfflineMode)
|
||||
// NOTE: we deliberately don't await here because we don't want to pause execution.
|
||||
db.attachments.cacheAttachments().catch(logger.error);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -125,7 +125,9 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
|
||||
const middlePane = useRef<ImperativePanelHandle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsNarrow(isTablet);
|
||||
const size = navPane.current?.getSize();
|
||||
// Toggle `isNarrow` to true if panel size isn't set to narrow by user
|
||||
setIsNarrow((size && size <= 5) || isTablet);
|
||||
}, [isTablet]);
|
||||
|
||||
// useEffect(() => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { isFeatureSupported } from "../utils/feature-check";
|
||||
import { generatePassword } from "../utils/password-generator";
|
||||
import { deriveKey, useKeyStore } from "../interfaces/key-store";
|
||||
import { logManager } from "@notesnook/core/dist/logger";
|
||||
import Config from "../utils/config";
|
||||
|
||||
const db = database;
|
||||
async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
@@ -45,7 +46,8 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co"
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
...Config.get("serverUrls", {})
|
||||
});
|
||||
|
||||
const storage = new NNStorage(
|
||||
|
||||
@@ -31,7 +31,7 @@ import { PATHS } from "@notesnook/desktop";
|
||||
import { TaskManager } from "./task-manager";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { createWritableStream } from "./desktop-bridge";
|
||||
import { createZipStream } from "../utils/streams/zip-stream";
|
||||
import { createZipStream, ZipFile } from "../utils/streams/zip-stream";
|
||||
import { FeatureDialog, FeatureKeys } from "../dialogs/feature-dialog";
|
||||
import { ZipEntry, createUnzipIterator } from "../utils/streams/unzip-stream";
|
||||
import { User } from "@notesnook/core";
|
||||
@@ -41,6 +41,10 @@ import { formatDate } from "@notesnook/core/dist/utils/date";
|
||||
import { showPasswordDialog } from "../dialogs/password-dialog";
|
||||
import { BackupPasswordDialog } from "../dialogs/backup-password-dialog";
|
||||
import { ReminderDialog } from "../dialogs/reminder-dialog";
|
||||
import { Cipher, SerializedKey } from "@notesnook/crypto";
|
||||
import { ChunkedStream } from "../utils/streams/chunked-stream";
|
||||
import { isFeatureSupported } from "../utils/feature-check";
|
||||
import { NNCrypto } from "../interfaces/nncrypto";
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
@@ -78,9 +82,11 @@ export async function createBackup(
|
||||
options: {
|
||||
rescueMode?: boolean;
|
||||
noVerify?: boolean;
|
||||
} = {}
|
||||
mode?: "full" | "partial";
|
||||
background?: boolean;
|
||||
} = { mode: "partial" }
|
||||
) {
|
||||
const { rescueMode, noVerify } = options;
|
||||
const { rescueMode, noVerify, mode, background } = options;
|
||||
const { isLoggedIn } = useUserStore.getState();
|
||||
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
|
||||
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
|
||||
@@ -99,7 +105,7 @@ export async function createBackup(
|
||||
type: "date-time",
|
||||
dateFormat: "YYYY-MM-DD",
|
||||
timeFormat: "24-hour"
|
||||
})}-${new Date().getSeconds()}`,
|
||||
})}-${new Date().getSeconds()}${mode === "full" ? "-full" : ""}`,
|
||||
{ replacement: "-" }
|
||||
);
|
||||
const directory = Config.get("backupStorageLocation", PATHS.backupsDirectory);
|
||||
@@ -110,23 +116,47 @@ export async function createBackup(
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const error = await TaskManager.startTask<Error | void>({
|
||||
type: "modal",
|
||||
type: background ? "status" : "modal",
|
||||
id: "creating-backup",
|
||||
title: "Creating backup",
|
||||
subtitle: "We are creating a backup of your data. Please wait...",
|
||||
action: async (report) => {
|
||||
const writeStream = await createWritableStream(filePath);
|
||||
|
||||
await new ReadableStream({
|
||||
await new ReadableStream<ZipFile>({
|
||||
start() {},
|
||||
async pull(controller) {
|
||||
for await (const file of db.backup!.export("web", encryptedBackups)) {
|
||||
report({
|
||||
text: `Saving chunk ${file.path}`
|
||||
});
|
||||
controller.enqueue({
|
||||
path: file.path,
|
||||
data: encoder.encode(file.data)
|
||||
});
|
||||
const { streamablefs } = await import("../interfaces/fs");
|
||||
for await (const output of db.backup!.export({
|
||||
type: "web",
|
||||
encrypt: encryptedBackups,
|
||||
mode
|
||||
})) {
|
||||
if (output.type === "file") {
|
||||
const file = output;
|
||||
report({
|
||||
text: background
|
||||
? `Creating backup (${file.path})`
|
||||
: `Saving file ${file.path}`
|
||||
});
|
||||
controller.enqueue({
|
||||
path: file.path,
|
||||
data: encoder.encode(file.data)
|
||||
});
|
||||
} else if (output.type === "attachment") {
|
||||
report({
|
||||
text: background
|
||||
? `Creating backup (${output.hash})`
|
||||
: `Saving attachment ${output.hash}`,
|
||||
total: output.total,
|
||||
current: output.current
|
||||
});
|
||||
const handle = await streamablefs.readFile(output.hash);
|
||||
if (!handle) continue;
|
||||
controller.enqueue({
|
||||
path: output.path,
|
||||
data: handle.readable
|
||||
});
|
||||
}
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
@@ -191,6 +221,8 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
let cachedKey: string | undefined = undefined;
|
||||
// const { read, totalFiles } = await Reader(backupFile);
|
||||
const entries: ZipEntry[] = [];
|
||||
const attachments: ZipEntry[] = [];
|
||||
let attachmentsKey: SerializedKey | Cipher<"base64"> | undefined;
|
||||
let filesProcessed = 0;
|
||||
|
||||
let isValid = false;
|
||||
@@ -199,7 +231,13 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
isValid = true;
|
||||
continue;
|
||||
}
|
||||
entries.push(entry);
|
||||
if (entry.name === "attachments/.attachments_key")
|
||||
attachmentsKey = JSON.parse(await entry.text()) as
|
||||
| SerializedKey
|
||||
| Cipher<"base64">;
|
||||
else if (entry.name.startsWith("attachments/"))
|
||||
attachments.push(entry);
|
||||
else entries.push(entry);
|
||||
}
|
||||
if (!isValid)
|
||||
console.warn(
|
||||
@@ -212,18 +250,27 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
if (backup.encrypted) {
|
||||
if (!cachedPassword && !cachedKey) {
|
||||
const result = await BackupPasswordDialog.show({
|
||||
validate: async ({ password, key }) => {
|
||||
if (!password && !key) return false;
|
||||
await db.backup?.import(backup, password, key);
|
||||
validate: async ({ password, key: encryptionKey }) => {
|
||||
if (!password && !encryptionKey) return false;
|
||||
await db.backup?.import(backup, {
|
||||
password,
|
||||
encryptionKey,
|
||||
attachmentsKey
|
||||
});
|
||||
cachedPassword = password;
|
||||
cachedKey = key;
|
||||
cachedKey = encryptionKey;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!result) break;
|
||||
} else await db.backup?.import(backup, cachedPassword, cachedKey);
|
||||
} else
|
||||
await db.backup?.import(backup, {
|
||||
password: cachedPassword,
|
||||
encryptionKey: cachedKey,
|
||||
attachmentsKey
|
||||
});
|
||||
} else {
|
||||
await db.backup?.import(backup);
|
||||
await db.backup?.import(backup, { attachmentsKey });
|
||||
}
|
||||
|
||||
report({
|
||||
@@ -234,6 +281,37 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
}
|
||||
});
|
||||
await db.initCollections();
|
||||
|
||||
const { ABYTES, streamablefs } = await import("../interfaces/fs");
|
||||
let current = 0;
|
||||
for (const entry of attachments) {
|
||||
const hash = entry.name.replace("attachments/", "");
|
||||
|
||||
report({
|
||||
text: `Importing attachment ${hash}`,
|
||||
total: attachments.length,
|
||||
current: current++
|
||||
});
|
||||
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) continue;
|
||||
|
||||
await streamablefs.deleteFile(attachment.hash);
|
||||
const handle = await streamablefs.createFile(
|
||||
attachment.hash,
|
||||
attachment.size,
|
||||
attachment.mimeType
|
||||
);
|
||||
await entry
|
||||
.stream()
|
||||
.pipeThrough(
|
||||
new ChunkedStream(
|
||||
attachment.chunkSize + ABYTES,
|
||||
isFeatureSupported("opfs") ? "copy" : "nocopy"
|
||||
)
|
||||
)
|
||||
.pipeTo(handle.writeable);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (error) {
|
||||
@@ -326,10 +404,10 @@ export async function showUpgradeReminderDialogs() {
|
||||
async function restore(
|
||||
backup: LegacyBackupFile,
|
||||
password?: string,
|
||||
key?: string
|
||||
encryptionKey?: string
|
||||
) {
|
||||
try {
|
||||
await db.backup?.import(backup, password, key);
|
||||
await db.backup?.import(backup, { password, encryptionKey });
|
||||
showToast("success", "Backup restored!");
|
||||
} catch (e) {
|
||||
logger.error(e as Error, "Could not restore the backup");
|
||||
|
||||
@@ -46,6 +46,7 @@ async function moveNotesToTrash(ids: string[], confirm = true) {
|
||||
await TaskManager.startTask({
|
||||
type: "status",
|
||||
id: "deleteNotes",
|
||||
title: "Deleting notes",
|
||||
action: async (report) => {
|
||||
report({
|
||||
text: `Deleting ${pluralize(items.length, "note")}...`
|
||||
@@ -66,6 +67,7 @@ async function moveNotebooksToTrash(ids: string[]) {
|
||||
await TaskManager.startTask({
|
||||
type: "status",
|
||||
id: "deleteNotebooks",
|
||||
title: "Deleting notebooks",
|
||||
action: async (report) => {
|
||||
report({
|
||||
text: `Deleting ${pluralize(ids.length, "notebook")}...`
|
||||
@@ -92,6 +94,7 @@ async function deleteAttachments(ids: string[]) {
|
||||
await TaskManager.startTask({
|
||||
type: "status",
|
||||
id: "deleteAttachments",
|
||||
title: "Deleting attachments",
|
||||
action: async (report) => {
|
||||
for (let i = 0; i < ids.length; ++i) {
|
||||
const id = ids[i];
|
||||
@@ -119,6 +122,7 @@ async function moveRemindersToTrash(ids: string[]) {
|
||||
await TaskManager.startTask({
|
||||
type: "status",
|
||||
id: "deleteReminders",
|
||||
title: "Deleting reminders",
|
||||
action: async (report) => {
|
||||
report({
|
||||
text: `Deleting ${pluralize(ids.length, "reminder")}...`
|
||||
|
||||
@@ -38,12 +38,18 @@ export type Notice = {
|
||||
params?: any;
|
||||
};
|
||||
|
||||
export const BACKUP_CRON_EXPRESSIONS = [
|
||||
"",
|
||||
"0 0 8 * * *", // daily at 8 AM
|
||||
"0 0 8 * * 1", // Every monday at 8 AM
|
||||
"0 0 0 1 * *" // 1st day of every month
|
||||
];
|
||||
export const BACKUP_CRON_EXPRESSIONS = {
|
||||
0: "",
|
||||
1: "0 0 8 * * *", // daily at 8 AM
|
||||
2: "0 0 8 * * 1", // Every monday at 8 AM
|
||||
3: "0 0 0 1 * *" // 1st day of every month
|
||||
};
|
||||
|
||||
export const FULL_BACKUP_CRON_EXPRESSIONS = {
|
||||
0: "",
|
||||
1: "0 0 8 * * 1", // Every monday at 8 AM
|
||||
2: "0 0 0 1 * *" // 1st day of every month
|
||||
};
|
||||
|
||||
export async function scheduleBackups() {
|
||||
const backupInterval = Config.get("backupReminderOffset", 0);
|
||||
@@ -62,6 +68,23 @@ export async function scheduleBackups() {
|
||||
);
|
||||
}
|
||||
|
||||
export async function scheduleFullBackups() {
|
||||
const backupInterval = Config.get("fullBackupReminderOffset", 0);
|
||||
|
||||
await TaskScheduler.stop("automatic-full-backups");
|
||||
if (!backupInterval) return false;
|
||||
|
||||
console.log("Scheduling automatic full backups");
|
||||
await TaskScheduler.register(
|
||||
"automatic-full-backups",
|
||||
FULL_BACKUP_CRON_EXPRESSIONS[backupInterval],
|
||||
() => {
|
||||
console.log("Backing up automatically");
|
||||
saveBackup("full");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldAddAutoBackupsDisabledNotice() {
|
||||
const backupInterval = Config.get("backupReminderOffset", 0);
|
||||
if (!isUserPremium() && backupInterval) {
|
||||
@@ -175,9 +198,9 @@ function isIgnored(key: keyof typeof NoticesData) {
|
||||
}
|
||||
|
||||
let openedToast: { hide: () => void } | null = null;
|
||||
async function saveBackup() {
|
||||
async function saveBackup(mode: "full" | "partial" = "partial") {
|
||||
if (IS_DESKTOP_APP) {
|
||||
await createBackup({ noVerify: true });
|
||||
await createBackup({ noVerify: true, mode, background: true });
|
||||
} else if (isUserPremium() && !IS_TESTING) {
|
||||
if (openedToast !== null) return;
|
||||
openedToast = showToast(
|
||||
@@ -196,7 +219,7 @@ async function saveBackup() {
|
||||
{
|
||||
text: "Download",
|
||||
onClick: async () => {
|
||||
await createBackup();
|
||||
await createBackup({ mode });
|
||||
openedToast?.hide();
|
||||
openedToast = null;
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ProgressDialog } from "../dialogs/progress-dialog";
|
||||
import { removeStatus, updateStatus } from "../hooks/use-status";
|
||||
|
||||
type TaskType = "status" | "modal";
|
||||
type TaskAction<T> = (report: ProgressReportCallback) => T | Promise<T>;
|
||||
export type TaskAction<T> = (report: ProgressReportCallback) => T | Promise<T>;
|
||||
type BaseTaskDefinition<TTaskType extends TaskType, TReturnType> = {
|
||||
type: TTaskType;
|
||||
action: TaskAction<TReturnType>;
|
||||
@@ -31,6 +31,7 @@ type StatusTaskDefinition<TReturnType> = BaseTaskDefinition<
|
||||
"status",
|
||||
TReturnType
|
||||
> & {
|
||||
title: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
@@ -39,7 +40,7 @@ type ModalTaskDefinition<TReturnType> = BaseTaskDefinition<
|
||||
TReturnType
|
||||
> & {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
|
||||
type TaskDefinition<TReturnType> =
|
||||
@@ -59,6 +60,10 @@ export class TaskManager {
|
||||
switch (task.type) {
|
||||
case "status": {
|
||||
const statusTask = task;
|
||||
updateStatus({
|
||||
key: statusTask.id,
|
||||
status: task.title
|
||||
});
|
||||
const result = await statusTask.action((progress) => {
|
||||
let percentage: number | undefined = undefined;
|
||||
if (progress.current && progress.total)
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
Image as RebassImage,
|
||||
Text as RebassText
|
||||
} from "@theme-ui/components";
|
||||
import { ANALYTICS_EVENTS, trackEvent } from "../../utils/analytics";
|
||||
import {
|
||||
Sync,
|
||||
Notebook,
|
||||
@@ -284,7 +283,6 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
|
||||
onClick={async () => {
|
||||
if (dismissAnnouncement) dismissAnnouncement();
|
||||
DialogManager.closeAll();
|
||||
trackEvent(ANALYTICS_EVENTS.announcementCta, action);
|
||||
switch (action.type) {
|
||||
case "link": {
|
||||
const url = new URL(action.data);
|
||||
|
||||
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import { Cross } from "../icons";
|
||||
import { ANALYTICS_EVENTS, trackEvent } from "../../utils/analytics";
|
||||
import AnnouncementBody from "./body";
|
||||
import { useStore as useAnnouncementStore } from "../../stores/announcement-store";
|
||||
import Notice from "../notice";
|
||||
@@ -59,7 +58,6 @@ function Announcements() {
|
||||
}}
|
||||
title="Dismiss announcement"
|
||||
onClick={() => {
|
||||
trackEvent(ANALYTICS_EVENTS.announcementDismissed, announcement);
|
||||
dismiss(announcement.id);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -240,14 +240,13 @@ function withProgress<T>(
|
||||
({ type, total, loaded }: AttachmentProgress) => {
|
||||
if (type !== "encrypt") return;
|
||||
report({
|
||||
current: loaded,
|
||||
total: total,
|
||||
current: Math.round((loaded / total) * 100),
|
||||
total: 100,
|
||||
text: file.name
|
||||
});
|
||||
}
|
||||
);
|
||||
event.unsubscribe();
|
||||
return action();
|
||||
return action().finally(() => event.unsubscribe());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,8 +30,14 @@ export function ErrorText(props: ErrorTextProps) {
|
||||
<Flex
|
||||
bg="var(--background-error)"
|
||||
p={1}
|
||||
mt={2}
|
||||
sx={{ borderRadius: "default", ...sx, maxHeight: 300, overflowY: "auto" }}
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
mt: 2,
|
||||
...sx,
|
||||
alignItems: "flex-start",
|
||||
maxHeight: 300,
|
||||
overflowY: "auto"
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
<ErrorIcon size={15} color="var(--icon-error)" />
|
||||
|
||||
@@ -55,6 +55,7 @@ function Field(props: FieldProps) {
|
||||
type,
|
||||
inputRef,
|
||||
validate,
|
||||
disabled,
|
||||
...inputProps
|
||||
} = props;
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
@@ -66,6 +67,8 @@ function Field(props: FieldProps) {
|
||||
sx={{
|
||||
m: "2px",
|
||||
mr: "2px",
|
||||
opacity: disabled ? 0.7 : 1,
|
||||
pointerEvents: disabled ? "none" : "all",
|
||||
...sx,
|
||||
flexDirection: "column"
|
||||
}}
|
||||
|
||||
@@ -214,7 +214,8 @@ import {
|
||||
mdiLink,
|
||||
mdiWindowClose,
|
||||
mdiFileMusicOutline,
|
||||
mdiBroom
|
||||
mdiBroom,
|
||||
mdiServerSecurity
|
||||
} from "@mdi/js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
@@ -541,6 +542,7 @@ export const Documentation = createIcon(mdiFileDocumentOutline);
|
||||
export const Legal = createIcon(mdiGavel);
|
||||
export const Desktop = createIcon(mdiDesktopClassic);
|
||||
export const Notification = createIcon(mdiBellBadgeOutline);
|
||||
export const Servers = createIcon(mdiServerSecurity);
|
||||
export const Calendar = createIcon(mdiCalendarBlank);
|
||||
|
||||
export const WindowMinimize = createIcon("M4 20v-2h16v2H4Z");
|
||||
|
||||
@@ -28,8 +28,9 @@ function Placeholder(props: PlaceholderProps) {
|
||||
const { context, text } = props;
|
||||
const tip = useTip(context);
|
||||
const syncStatus = useAppStore((store) => store.syncStatus);
|
||||
const isFirstSync = useAppStore((store) => store.lastSynced === 0);
|
||||
|
||||
if (syncStatus.key === "syncing" && context === "notes") {
|
||||
if (isFirstSync && syncStatus.key === "syncing" && context === "notes") {
|
||||
return (
|
||||
<Flex
|
||||
variant="columnCenter"
|
||||
|
||||
@@ -175,12 +175,8 @@ function SideBar(props: SideBarProps) {
|
||||
const result = await TaskManager.startTask({
|
||||
type: "status",
|
||||
id: "trialActivation",
|
||||
action: (report) => {
|
||||
report({
|
||||
text: "Activating trial"
|
||||
});
|
||||
return db.user.activateTrial();
|
||||
}
|
||||
title: "Activating trial",
|
||||
action: () => db.user.activateTrial()
|
||||
});
|
||||
if (result) onClose();
|
||||
} catch (e) {
|
||||
|
||||
@@ -32,17 +32,19 @@ import {
|
||||
} from "./types";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { isFeatureSupported } from "../../utils/feature-check";
|
||||
|
||||
// const isDev = false; // import.meta.env.DEV;
|
||||
// const VENDOR_ID = isDev ? 1506 : 128190;
|
||||
const VENDOR_ID = import.meta.env.DEV || IS_TESTING ? 1506 : 128190;
|
||||
const PADDLE_ORIGIN =
|
||||
import.meta.env.DEV || IS_TESTING
|
||||
? "https://sandbox-buy.paddle.com"
|
||||
: "https://buy.paddle.com";
|
||||
const CHECKOUT_CREATE_ORIGIN =
|
||||
const SUBSCRIPTION_MANAGEMENT_URL =
|
||||
import.meta.env.DEV || IS_TESTING
|
||||
? "https://sandbox-create-checkout.paddle.com"
|
||||
: "https://create-checkout.paddle.com";
|
||||
? "https://sandbox-subscription-management.paddle.com"
|
||||
: "https://subscription-management.paddle.com";
|
||||
const CHECKOUT_SERVICE_ORIGIN =
|
||||
import.meta.env.DEV || IS_TESTING
|
||||
? "https://sandbox-checkout-service.paddle.com"
|
||||
@@ -102,6 +104,7 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
useEffect(() => {
|
||||
async function onMessage(ev: MessageEvent<PaddleEvent>) {
|
||||
if (ev.origin !== PADDLE_ORIGIN) return;
|
||||
logger.debug("Paddle event received", { data: ev.data });
|
||||
const { event_name, callback_data } = ev.data;
|
||||
const { checkout } = callback_data;
|
||||
|
||||
@@ -109,8 +112,10 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
!checkout ||
|
||||
!checkout.id ||
|
||||
SUBSCRIBED_EVENTS.indexOf(event_name) === -1
|
||||
)
|
||||
) {
|
||||
logger.debug("Ignoring paddle event", { event_name });
|
||||
return;
|
||||
}
|
||||
|
||||
if (event_name === PaddleEvents["Checkout.Complete"]) {
|
||||
onCompleted && onCompleted();
|
||||
@@ -124,9 +129,9 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
appliedCouponCode.current = pricingInfo.coupon;
|
||||
setCheckoutId(checkout.id);
|
||||
}
|
||||
window.addEventListener("message", onMessage);
|
||||
window.addEventListener("message", onMessage, false);
|
||||
return () => {
|
||||
window.removeEventListener("message", onMessage);
|
||||
window.removeEventListener("message", onMessage, false);
|
||||
};
|
||||
}, [onPriceUpdated, updatePrice, plan, onCompleted]);
|
||||
|
||||
@@ -188,6 +193,7 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
frameBorder={"0"}
|
||||
ref={checkoutRef}
|
||||
src={sourceUrl}
|
||||
allow={`payment ${PADDLE_ORIGIN} ${SUBSCRIPTION_MANAGEMENT_URL};`}
|
||||
style={{
|
||||
// padding: "0px 30px",
|
||||
height: "1000px",
|
||||
@@ -206,21 +212,28 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
|
||||
|
||||
async function getCheckoutURL(params: PaddleCheckoutProps) {
|
||||
const { plan, theme, user } = params;
|
||||
const BASE_URL = `${CHECKOUT_CREATE_ORIGIN}/checkout/product/${plan.id}`;
|
||||
const BASE_URL = `${PADDLE_ORIGIN}/paddlejs?ccsURL=${CHECKOUT_SERVICE_ORIGIN}/create/checkout/product/${plan.id}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.set("product", plan.id);
|
||||
queryParams.set("vendor", VENDOR_ID.toString());
|
||||
queryParams.set("passthrough", JSON.stringify({ userId: user.id }));
|
||||
queryParams.set("guest_email", user.email);
|
||||
queryParams.set("quantity_variable", "0");
|
||||
queryParams.set("disable_logout", "true");
|
||||
queryParams.set("display_mode_theme", theme);
|
||||
queryParams.set("display_mode", "inline");
|
||||
queryParams.set("apple_pay_enabled", "false");
|
||||
queryParams.set("paddlejs-version", "2.0.14");
|
||||
queryParams.set(
|
||||
"apple_pay_enabled",
|
||||
JSON.stringify(isFeatureSupported("applePaySupported"))
|
||||
);
|
||||
queryParams.set("paddlejs-version", "2.0.81");
|
||||
queryParams.set("checkout_initiated", new Date().getTime().toString());
|
||||
queryParams.set("popup", "true");
|
||||
queryParams.set("paddle_js", "true");
|
||||
queryParams.set("is_popup", "true");
|
||||
queryParams.set("parent_url", window.location.href);
|
||||
queryParams.set("parent_url", window.location.origin);
|
||||
queryParams.set("parentURL", window.location.origin);
|
||||
queryParams.set("referring_domain", window.location.hostname);
|
||||
const fullURL = `${BASE_URL}?${queryParams.toString()}`;
|
||||
return fullURL;
|
||||
}
|
||||
|
||||
@@ -17,14 +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 { Box, Checkbox, Label, Text } from "@theme-ui/components";
|
||||
import { Checkbox, Flex, Label, Text } from "@theme-ui/components";
|
||||
import { useRef } from "react";
|
||||
import { mdToHtml } from "../utils/md";
|
||||
import Dialog from "../components/dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { db } from "../common/db";
|
||||
import { AppVersion, getChangelog } from "../utils/version";
|
||||
import { getChangelog } from "../utils/version";
|
||||
import { downloadUpdate } from "../utils/updater";
|
||||
import { ErrorText } from "../components/error-text";
|
||||
|
||||
type Check = { text: string; default?: boolean };
|
||||
export type ConfirmDialogProps<TCheckId extends string> = BaseDialogProps<
|
||||
@@ -36,6 +37,7 @@ export type ConfirmDialogProps<TCheckId extends string> = BaseDialogProps<
|
||||
positiveButtonText?: string;
|
||||
negativeButtonText?: string;
|
||||
message?: string;
|
||||
warnings?: string[];
|
||||
checks?: Partial<Record<TCheckId, Check>>;
|
||||
};
|
||||
|
||||
@@ -50,6 +52,7 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog<
|
||||
negativeButtonText,
|
||||
positiveButtonText,
|
||||
message,
|
||||
warnings,
|
||||
checks
|
||||
} = props;
|
||||
const checkedItems = useRef<Record<TCheckId, boolean>>({} as any);
|
||||
@@ -62,6 +65,12 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog<
|
||||
width={width}
|
||||
description={subtitle}
|
||||
onClose={() => onClose(false)}
|
||||
onOpen={() => {
|
||||
for (const checkId in checks) {
|
||||
checkedItems.current[checkId as TCheckId] =
|
||||
checks[checkId as TCheckId]?.default || false;
|
||||
}
|
||||
}}
|
||||
positiveButton={
|
||||
positiveButtonText
|
||||
? {
|
||||
@@ -80,8 +89,10 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog<
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Box
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
pb: !negativeButtonText && !positiveButtonText ? 2 : 0,
|
||||
p: { m: 0 }
|
||||
}}
|
||||
@@ -93,6 +104,9 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog<
|
||||
dangerouslySetInnerHTML={{ __html: mdToHtml(message) }}
|
||||
/>
|
||||
) : null}
|
||||
{warnings?.map((text) => (
|
||||
<ErrorText key={text} error={text} sx={{ mt: 0 }} />
|
||||
))}
|
||||
{checks
|
||||
? Object.entries<Check | undefined>(checks).map(
|
||||
([id, check]) =>
|
||||
@@ -101,12 +115,17 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog<
|
||||
key={id}
|
||||
id={id}
|
||||
variant="text.body"
|
||||
sx={{ alignItems: "center" }}
|
||||
sx={{ fontWeight: "bold" }}
|
||||
>
|
||||
<Checkbox
|
||||
name={id}
|
||||
defaultChecked={check.default}
|
||||
sx={{ mr: "small", width: 18, height: 18 }}
|
||||
sx={{
|
||||
mr: "small",
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: "accent"
|
||||
}}
|
||||
onChange={(e) =>
|
||||
(checkedItems.current[id as TCheckId] =
|
||||
e.currentTarget.checked)
|
||||
@@ -117,7 +136,7 @@ export const ConfirmDialog = DialogManager.register(function ConfirmDialog<
|
||||
)
|
||||
)
|
||||
: null}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
@@ -143,13 +162,24 @@ export function showMultiPermanentDeleteConfirmation(length: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export function showLogoutConfirmation() {
|
||||
return ConfirmDialog.show({
|
||||
export async function showLogoutConfirmation() {
|
||||
return await ConfirmDialog.show({
|
||||
title: `Logout?`,
|
||||
message:
|
||||
"Logging out will clear all data stored on THIS DEVICE. Make sure you have synced all your changes before logging out.",
|
||||
"Are you sure you want to log out and clear all data stored on THIS DEVICE?",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
negativeButtonText: "No",
|
||||
warnings: (await db.hasUnsyncedChanges())
|
||||
? [
|
||||
"You have unsynced notes. Take a backup or sync your notes to avoid losing critical data."
|
||||
]
|
||||
: [],
|
||||
checks: {
|
||||
backup: {
|
||||
text: "Take a backup before logging out?",
|
||||
default: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export const CreateColorDialog = DialogManager.register(
|
||||
colorCode: form.color,
|
||||
title: form.title
|
||||
});
|
||||
props.onClose(colorId);
|
||||
props.onClose(colorId || false);
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
|
||||
@@ -90,9 +90,23 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
)
|
||||
}
|
||||
]
|
||||
: IS_DESKTOP_APP
|
||||
? []
|
||||
: [],
|
||||
: [
|
||||
{
|
||||
title: "Full offline mode",
|
||||
subtitle:
|
||||
"You can now download all your attachments upfront so they are always available on your device even without an Internet connection. To enable just go to Settings > Sync and toggle Full Offline Mode."
|
||||
},
|
||||
{
|
||||
title: "Backup with attachments",
|
||||
subtitle:
|
||||
"You can now backup your notes alongwith all your attachments - kind of like a full account snapshot."
|
||||
},
|
||||
{
|
||||
title: "Self hosting",
|
||||
subtitle:
|
||||
"This release also adds initial support for changing server URLs i.e. to connect your own instance of Notesnook. Please note that this is still experimental and may not work as expected."
|
||||
}
|
||||
],
|
||||
cta: {
|
||||
title: "Got it",
|
||||
icon: Checkmark,
|
||||
|
||||
@@ -93,8 +93,8 @@ export const CreateTagDialog = {
|
||||
title: "Create tag",
|
||||
subtitle: "You can create as many tags as you want."
|
||||
}).then(async (title) => {
|
||||
if (!title) return;
|
||||
await db.tags.add({ title });
|
||||
if (!title || !(await db.tags.add({ title }))) return;
|
||||
|
||||
showToast("success", "Tag created!");
|
||||
useTagStore.getState().refresh();
|
||||
})
|
||||
|
||||
@@ -1,57 +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 { useEffect } from "react";
|
||||
import { Box, Text } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { Loading } from "../components/icons";
|
||||
|
||||
type LoadingDialogProps<T> = {
|
||||
onClose: (result: T | boolean) => void;
|
||||
action: () => T | Promise<T>;
|
||||
title: string;
|
||||
description: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function LoadingDialog<T>(props: LoadingDialogProps<T>) {
|
||||
const { onClose, action, description, message, title } = props;
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
onClose(await action());
|
||||
} catch (e) {
|
||||
onClose(false);
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
}, [onClose, action]);
|
||||
|
||||
return (
|
||||
<Dialog isOpen={true} title={title} description={description}>
|
||||
<Box>
|
||||
<Text as="span" variant="body">
|
||||
{message}
|
||||
</Text>
|
||||
<Loading sx={{ my: 2 }} color="accent" />
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default LoadingDialog;
|
||||
@@ -37,7 +37,6 @@ import DarkUI from "../assets/dark1.png";
|
||||
import GooglePlay from "../assets/play.png";
|
||||
import AppleStore from "../assets/apple.png";
|
||||
import { useStore as useThemeStore } from "../stores/theme-store";
|
||||
import { Checkbox, Label } from "@theme-ui/components";
|
||||
import { Features } from "../components/announcements/body";
|
||||
import { TaskManager } from "../common/task-manager";
|
||||
import { db } from "../common/db";
|
||||
@@ -64,8 +63,7 @@ const newUserSteps: Step[] = [
|
||||
title: "Safe & encrypted notes",
|
||||
subtitle: "Write with freedom. Never compromise on privacy again.",
|
||||
buttonText: "Get started",
|
||||
image: <Note style={{ flexShrink: 0, width: 120, height: 120 }} />,
|
||||
component: TrackingConsent
|
||||
image: <Note style={{ flexShrink: 0, width: 120, height: 120 }} />
|
||||
},
|
||||
{
|
||||
title: "Choose your style",
|
||||
@@ -421,28 +419,6 @@ function ThemeSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
function TrackingConsent() {
|
||||
const [enableTelemetry, setEnableTelemetry] = usePersistentState(
|
||||
"telemetry",
|
||||
false
|
||||
);
|
||||
return (
|
||||
<Label variant="text.subBody" my={4} sx={{ width: "80%" }}>
|
||||
<Checkbox
|
||||
checked={enableTelemetry}
|
||||
onChange={(e) => {
|
||||
setEnableTelemetry(e.target.checked);
|
||||
}}
|
||||
sx={{ width: 14 }}
|
||||
/>
|
||||
<Text>
|
||||
Help improve Notesnook by sending completely anonymized product
|
||||
analytics.
|
||||
</Text>
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
function CrossPlatform() {
|
||||
return (
|
||||
<Flex my={4} sx={{ alignItems: "center" }}>
|
||||
@@ -513,12 +489,8 @@ function TrialOffer({ onClose }: { onClose: () => void }) {
|
||||
const result = await TaskManager.startTask({
|
||||
type: "status",
|
||||
id: "trialActivation",
|
||||
action: (report) => {
|
||||
report({
|
||||
text: "Activating trial"
|
||||
});
|
||||
return db.user.activateTrial();
|
||||
}
|
||||
title: "Activating trial",
|
||||
action: () => db.user.activateTrial()
|
||||
});
|
||||
if (result) onClose();
|
||||
} catch (e) {
|
||||
|
||||
@@ -29,7 +29,7 @@ type Progress = {
|
||||
};
|
||||
type ProgressDialogProps<T> = BaseDialogProps<T | Error> & {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
subtitle?: string;
|
||||
action: (report: (progress: Progress) => void) => T;
|
||||
};
|
||||
export const ProgressDialog = DialogManager.register(function ProgressDialog<T>(
|
||||
|
||||
@@ -131,14 +131,17 @@ export const ReminderDialog = DialogManager.register(function ReminderDialog(
|
||||
testId="reminder-dialog"
|
||||
isOpen={true}
|
||||
title={reminder.title}
|
||||
onClose={() => props.onClose(false)}
|
||||
onClose={() => {
|
||||
Config.set(reminderKey, true);
|
||||
props.onClose(false);
|
||||
}}
|
||||
showCloseButton
|
||||
description={reminder.description}
|
||||
positiveButton={{
|
||||
text: <Flex>{reminder.cta.title}</Flex>,
|
||||
onClick: async () => {
|
||||
if (reminder.cta.action) await reminder.cta.action();
|
||||
|
||||
Config.set(reminderKey, true);
|
||||
props.onClose(true);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { SettingsGroup } from "./types";
|
||||
import { useStore as useSettingStore } from "../../stores/setting-store";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { createBackup, importBackup, verifyAccount } from "../../common";
|
||||
import { db } from "../../common/db";
|
||||
import { exportNotes } from "../../common/export";
|
||||
@@ -37,14 +36,24 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
settings: [
|
||||
{
|
||||
key: "create-backup",
|
||||
title: "Backup now",
|
||||
description: "Create a backup file containing all your data",
|
||||
title: "Create backup",
|
||||
description:
|
||||
"Partial backups contain all your data except attachments. They are created from data already available on your device and do not require an Internet connection.",
|
||||
components: [
|
||||
{
|
||||
type: "button",
|
||||
title: "Create backup",
|
||||
action: createBackup,
|
||||
variant: "secondary"
|
||||
type: "dropdown",
|
||||
options: [
|
||||
{ value: "-", title: "Choose backup format" },
|
||||
{ value: "partial", title: "Backup" },
|
||||
{ value: "full", title: "Backup with attachments" }
|
||||
],
|
||||
selectedOption: () => "-",
|
||||
onSelectionChanged: async (value) => {
|
||||
if (value === "-") return;
|
||||
await createBackup({
|
||||
mode: value === "partial" ? "partial" : "full"
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -67,21 +76,21 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
},
|
||||
{
|
||||
key: "auto-backup",
|
||||
title: IS_DESKTOP_APP ? "Automatic backups" : "Backup reminders",
|
||||
description: IS_DESKTOP_APP
|
||||
? "Backup all your data automatically at a set interval."
|
||||
: "You will be shown regular reminders to backup your data.",
|
||||
isHidden: () => !isUserPremium(),
|
||||
title: "Automatic backup",
|
||||
description: `Set the interval to automatically create a backup.
|
||||
|
||||
Note: these backups do not contain your attachments.`,
|
||||
// isHidden: () => !isUserPremium(),
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.backupReminderOffset, listener),
|
||||
components: [
|
||||
{
|
||||
type: "dropdown",
|
||||
options: [
|
||||
{ value: "0", title: "Never" },
|
||||
{ value: "1", title: "Daily" },
|
||||
{ value: "2", title: "Weekly" },
|
||||
{ value: "3", title: "Monthly" }
|
||||
{ value: "0", title: "Never", premium: true },
|
||||
{ value: "1", title: "Daily", premium: true },
|
||||
{ value: "2", title: "Weekly", premium: true },
|
||||
{ value: "3", title: "Monthly", premium: true }
|
||||
],
|
||||
selectedOption: () =>
|
||||
useSettingStore.getState().backupReminderOffset.toString(),
|
||||
@@ -97,6 +106,40 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "auto-backup-with-attachments",
|
||||
title: "Automatic backup with attachments",
|
||||
description: `Set the interval to automatically create a full backup with attachments.
|
||||
|
||||
NOTE: Creating a backup with attachments can take a while, and also fail completely. The app will try to resume/restart the backup in case of interruptions.`,
|
||||
// isHidden: () => !isUserPremium(),
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe(
|
||||
(s) => s.fullBackupReminderOffset,
|
||||
listener
|
||||
),
|
||||
components: [
|
||||
{
|
||||
type: "dropdown",
|
||||
options: [
|
||||
{ value: "0", title: "Never", premium: true },
|
||||
{ value: "1", title: "Weekly", premium: true },
|
||||
{ value: "2", title: "Monthly", premium: true }
|
||||
],
|
||||
selectedOption: () =>
|
||||
useSettingStore.getState().fullBackupReminderOffset.toString(),
|
||||
onSelectionChanged: async (value) => {
|
||||
const verified =
|
||||
useSettingStore.getState().encryptBackups ||
|
||||
(await verifyAccount());
|
||||
if (verified)
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setFullBackupReminderOffset(parseInt(value));
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "encrypt-backups",
|
||||
title: "Backup encryption",
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
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 { Button, Flex, Text } from "@theme-ui/components";
|
||||
import Field from "../../../components/field";
|
||||
import { useState } from "react";
|
||||
import { HostId, HostIds, useStore } from "../../../stores/setting-store";
|
||||
import { useStore as useUserStore } from "../../../stores/user-store";
|
||||
import { ErrorText } from "../../../components/error-text";
|
||||
import { TaskManager } from "../../../common/task-manager";
|
||||
import { isServerCompatible } from "@notesnook/core";
|
||||
|
||||
export const ServerIds = ["notesnook-sync", "auth", "sse"] as const;
|
||||
export type ServerId = (typeof ServerIds)[number];
|
||||
type Server = {
|
||||
id: ServerId;
|
||||
host: HostId;
|
||||
title: string;
|
||||
example: string;
|
||||
description: string;
|
||||
};
|
||||
type VersionResponse = {
|
||||
version: number;
|
||||
id: string;
|
||||
instance: string;
|
||||
};
|
||||
const SERVERS: Server[] = [
|
||||
{
|
||||
id: "notesnook-sync",
|
||||
host: "API_HOST",
|
||||
title: "Sync server",
|
||||
example: "http://localhost:4326",
|
||||
description: "Server used to sync your notes & other data between devices."
|
||||
},
|
||||
{
|
||||
id: "auth",
|
||||
host: "AUTH_HOST",
|
||||
title: "Auth server",
|
||||
example: "http://localhost:5326",
|
||||
description: "Server used for login/sign up and authentication."
|
||||
},
|
||||
{
|
||||
id: "sse",
|
||||
host: "SSE_HOST",
|
||||
title: "Events server",
|
||||
example: "http://localhost:7326",
|
||||
description: "Server used to receive important notifications & events."
|
||||
}
|
||||
];
|
||||
export function ServersConfiguration() {
|
||||
const [error, setError] = useState<string>();
|
||||
const [success, setSuccess] = useState<boolean>();
|
||||
const [urls, setUrls] = useState<Partial<Record<HostId, string>>>(
|
||||
useStore.getState().serverUrls
|
||||
);
|
||||
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
|
||||
return (
|
||||
<>
|
||||
{isLoggedIn ? (
|
||||
<ErrorText
|
||||
error="You must log out in order to change/reset server URLs."
|
||||
sx={{ mb: 2, mt: 0 }}
|
||||
/>
|
||||
) : null}
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
{SERVERS.map((server) => (
|
||||
<Field
|
||||
disabled={isLoggedIn}
|
||||
key={server.id}
|
||||
label={`${server.title} URL`}
|
||||
helpText={server.description}
|
||||
placeholder={`e.g. ${server.example}`}
|
||||
validate={(text) => URL.canParse(text)}
|
||||
defaultValue={urls[server.host]}
|
||||
onChange={(e) =>
|
||||
setUrls((s) => {
|
||||
s[server.host] = e.target.value;
|
||||
return s;
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<ErrorText error={error} />
|
||||
{success === true ? (
|
||||
<Text
|
||||
className="selectable"
|
||||
variant={"error"}
|
||||
ml={1}
|
||||
sx={{
|
||||
whiteSpace: "pre-wrap",
|
||||
bg: "shade",
|
||||
color: "accent",
|
||||
p: 1,
|
||||
mt: 2,
|
||||
borderRadius: "default"
|
||||
}}
|
||||
>
|
||||
Connected to all servers successfully.
|
||||
</Text>
|
||||
) : null}
|
||||
<Flex sx={{ mt: 1, justifyContent: "end", gap: 1 }}>
|
||||
<Button
|
||||
variant="accent"
|
||||
disabled={!success}
|
||||
onClick={async () => {
|
||||
if (!success || isLoggedIn) return;
|
||||
useStore.getState().setServerUrls(urls);
|
||||
await TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: "App will reload in 5 seconds",
|
||||
subtitle:
|
||||
"Your changes have been saved and will be reflected after the app has refreshed.",
|
||||
action() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
resolve(undefined);
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLoggedIn}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
setError(undefined);
|
||||
try {
|
||||
for (const host of HostIds) {
|
||||
const url = urls[host];
|
||||
const server = SERVERS.find((s) => s.host === host);
|
||||
if (!server)
|
||||
throw new Error(`Server with host ${host} not found.`);
|
||||
if (!url) throw new Error("All server urls are required.");
|
||||
const version = await fetch(`${url}/version`)
|
||||
.then((r) => r.json() as Promise<VersionResponse>)
|
||||
.catch(() => undefined);
|
||||
if (!version)
|
||||
throw new Error(`Could not connect to ${server.title}.`);
|
||||
if (version.id !== server.id)
|
||||
throw new Error(
|
||||
`The URL you have given (${url}) does not point to the ${server.title}.`
|
||||
);
|
||||
if (!isServerCompatible(version.version))
|
||||
throw new Error(
|
||||
`The ${server.title} at ${url} is not compatible with this client.`
|
||||
);
|
||||
}
|
||||
setSuccess(true);
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLoggedIn}
|
||||
variant="errorSecondary"
|
||||
onClick={async () => {
|
||||
if (isLoggedIn) return;
|
||||
useStore.getState().setServerUrls();
|
||||
await TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: "App will reload in 5 seconds",
|
||||
subtitle:
|
||||
"Your changes have been saved and will be reflected after the app has refreshed.",
|
||||
action() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
resolve(undefined);
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +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 { Box, Text } from "@theme-ui/components";
|
||||
import { ANALYTICS_EVENTS } from "../../../utils/analytics";
|
||||
|
||||
const events = Object.values(ANALYTICS_EVENTS);
|
||||
export function TrackingDetails() {
|
||||
return (
|
||||
<table
|
||||
style={{ tableLayout: "fixed", borderCollapse: "collapse" }}
|
||||
cellPadding={0}
|
||||
cellSpacing={0}
|
||||
>
|
||||
<thead>
|
||||
<Box
|
||||
as="tr"
|
||||
sx={{
|
||||
height: 30,
|
||||
th: { borderBottom: "1px solid var(--separator)" }
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ id: "event-name", title: "Event name", width: "15%" },
|
||||
{ id: "event-detail", title: "Detail", width: "50%" }
|
||||
].map((column) =>
|
||||
!column.title ? (
|
||||
<th key={column.id} />
|
||||
) : (
|
||||
<Box
|
||||
as="th"
|
||||
key={column.id}
|
||||
sx={{
|
||||
width: column.width,
|
||||
px: 1,
|
||||
mb: 2,
|
||||
textAlign: "left"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ textAlign: "left", fontWeight: "normal" }}
|
||||
>
|
||||
{column.title}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map((event) => (
|
||||
<tr key={event.name} style={{ height: 30 }}>
|
||||
<Text
|
||||
as="td"
|
||||
sx={{ wordWrap: "break-word", maxWidth: 130 }}
|
||||
variant="body"
|
||||
>
|
||||
{event.name}
|
||||
</Text>
|
||||
<Text as="td" variant="body">
|
||||
{event.description}
|
||||
</Text>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
PasswordAndAuth,
|
||||
Privacy,
|
||||
Pro,
|
||||
Servers,
|
||||
ShieldLock,
|
||||
Sync
|
||||
} from "../../components/icons";
|
||||
@@ -74,6 +75,7 @@ import { SubscriptionSettings } from "./subscription-settings";
|
||||
import { ScopedThemeProvider } from "../../components/theme-provider";
|
||||
import { AppLockSettings } from "./app-lock-settings";
|
||||
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
|
||||
import { ServersSettings } from "./servers-settings";
|
||||
|
||||
type SettingsDialogProps = BaseDialogProps<false>;
|
||||
|
||||
@@ -116,7 +118,8 @@ const sectionGroups: SectionGroup[] = [
|
||||
icon: Desktop,
|
||||
isHidden: () => !IS_DESKTOP_APP
|
||||
},
|
||||
{ key: "notifications", title: "Notifications", icon: Notification }
|
||||
{ key: "notifications", title: "Notifications", icon: Notification },
|
||||
{ key: "servers", title: "Servers", icon: Servers }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -164,7 +167,8 @@ const SettingsGroups = [
|
||||
...LegalSettings,
|
||||
...SupportSettings,
|
||||
...AboutSettings,
|
||||
...SubscriptionSettings
|
||||
...SubscriptionSettings,
|
||||
...ServersSettings
|
||||
];
|
||||
|
||||
// Thoughts:
|
||||
|
||||
@@ -24,7 +24,6 @@ import { getPlatform } from "../../utils/platform";
|
||||
import { db } from "../../common/db";
|
||||
import Config from "../../utils/config";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { TrackingDetails } from "./components/tracking-details";
|
||||
import { PromptDialog } from "../prompt";
|
||||
|
||||
export const PrivacySettings: SettingsGroup[] = [
|
||||
@@ -33,26 +32,6 @@ export const PrivacySettings: SettingsGroup[] = [
|
||||
section: "privacy",
|
||||
header: "General",
|
||||
settings: [
|
||||
{
|
||||
key: "telemetry",
|
||||
title: "Telemetry",
|
||||
description: `Usage data & crash reports will be sent to us (no 3rd party involved) for analytics. All data is anonymous as mentioned in our privacy policy.
|
||||
|
||||
What data is collected & when?`,
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.telemetry, listener),
|
||||
components: [
|
||||
{
|
||||
type: "toggle",
|
||||
isToggled: () => !!useSettingStore.getState().telemetry,
|
||||
toggle: () => useSettingStore.getState().toggleTelemetry()
|
||||
},
|
||||
{
|
||||
type: "custom",
|
||||
component: TrackingDetails
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "marketing",
|
||||
title: "Marketing emails",
|
||||
@@ -136,8 +115,6 @@ This can sometimes bypass local ISP blockages on Notesnook traffic. Disable this
|
||||
title: "Custom CORS proxy",
|
||||
description:
|
||||
"CORS proxy is required to directly download images from within the Notesnook app. It allows Notesnook to bypass browser restrictions by using a proxy. You can set a custom self-hosted proxy URL to increase your privacy.",
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.telemetry, listener),
|
||||
components: [
|
||||
{
|
||||
type: "button",
|
||||
|
||||
@@ -23,15 +23,17 @@ import { showPasswordDialog } from "../../dialogs/password-dialog";
|
||||
import { db } from "../../common/db";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { UserProfile } from "./components/user-profile";
|
||||
import { verifyAccount } from "../../common";
|
||||
import { createBackup, verifyAccount } from "../../common";
|
||||
import { EmailChangeDialog } from "../email-change-dialog";
|
||||
import {
|
||||
ConfirmDialog,
|
||||
showClearSessionsConfirmation,
|
||||
showLogoutConfirmation
|
||||
} from "../confirm";
|
||||
import { TaskManager } from "../../common/task-manager";
|
||||
import { AttachmentsDialog } from "../attachments-dialog";
|
||||
import { RecoveryKeyDialog } from "../recovery-key-dialog";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
export const ProfileSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -141,15 +143,34 @@ export const ProfileSettings: SettingsGroup[] = [
|
||||
variant: "errorSecondary",
|
||||
title: "Logout",
|
||||
action: async () => {
|
||||
if (await showLogoutConfirmation()) {
|
||||
await TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: "You are being logged out",
|
||||
subtitle: "Please wait...",
|
||||
action: () => db.user.logout(true)
|
||||
});
|
||||
showToast("success", "You have been logged out.");
|
||||
const result = await showLogoutConfirmation();
|
||||
if (!result) return;
|
||||
|
||||
if (result.backup) {
|
||||
try {
|
||||
await createBackup({ mode: "partial" });
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to take backup before logout");
|
||||
if (
|
||||
!(await ConfirmDialog.show({
|
||||
title: "Failed to take backup",
|
||||
message:
|
||||
"Failed to take backup of your data. Do you want to continue logging out?",
|
||||
negativeButtonText: "No",
|
||||
positiveButtonText: "Yes"
|
||||
}))
|
||||
)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: "You are being logged out",
|
||||
subtitle: "Please wait...",
|
||||
action: () => db.user.logout(true)
|
||||
});
|
||||
showToast("success", "You have been logged out.");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -17,15 +17,25 @@ 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 { useEffect, useState } from "react";
|
||||
import { isTelemetryEnabled, setTelemetry } from "../utils/telemetry";
|
||||
import { ServersConfiguration } from "./components/servers-configuration";
|
||||
import { SettingsGroup } from "./types";
|
||||
|
||||
export function useTelemetry() {
|
||||
const [enabled, setEnabled] = useState(isTelemetryEnabled());
|
||||
|
||||
useEffect(() => {
|
||||
setTelemetry(enabled);
|
||||
}, [enabled]);
|
||||
|
||||
return [enabled, setEnabled] as const;
|
||||
}
|
||||
export const ServersSettings: SettingsGroup[] = [
|
||||
{
|
||||
header: "Servers configuration",
|
||||
key: "servers",
|
||||
section: "servers",
|
||||
settings: [
|
||||
{
|
||||
key: "config",
|
||||
title: "",
|
||||
components: [
|
||||
{
|
||||
type: "custom",
|
||||
component: ServersConfiguration
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { SettingsGroup } from "./types";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { useStore as useSettingStore } from "../../stores/setting-store";
|
||||
import { ConfirmDialog } from "../confirm";
|
||||
|
||||
export const SyncSettings: SettingsGroup[] = [
|
||||
@@ -76,6 +77,21 @@ export const SyncSettings: SettingsGroup[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "full-offline-mode",
|
||||
title: "Full offline mode",
|
||||
description: "Download everything including attachments on sync",
|
||||
keywords: ["offline mode"],
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.isFullOfflineMode, listener),
|
||||
components: [
|
||||
{
|
||||
type: "toggle",
|
||||
isToggled: () => useSettingStore.getState().isFullOfflineMode,
|
||||
toggle: () => useSettingStore.getState().toggleFullOfflineMode()
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "force-sync",
|
||||
title: "Having problems with sync?",
|
||||
|
||||
@@ -28,6 +28,7 @@ export type SectionKeys =
|
||||
| "behaviour"
|
||||
| "desktop"
|
||||
| "notifications"
|
||||
| "servers"
|
||||
| "editor"
|
||||
| "backup-export"
|
||||
| "export"
|
||||
|
||||
5
apps/web/src/global.d.ts
vendored
5
apps/web/src/global.d.ts
vendored
@@ -69,4 +69,9 @@ declare global {
|
||||
visible: boolean;
|
||||
};
|
||||
}
|
||||
interface Window {
|
||||
ApplePaySession?: {
|
||||
canMakePayments(): boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { initializeDatabase } from "../common/db";
|
||||
import "../utils/analytics";
|
||||
|
||||
const memory = {
|
||||
isDatabaseLoaded: false
|
||||
|
||||
@@ -51,6 +51,21 @@ export class IndexedDBFileStore implements IFileStorage {
|
||||
readChunk(chunkName: string): Promise<Uint8Array | undefined> {
|
||||
return this.storage.get(chunkName);
|
||||
}
|
||||
async listChunks(chunkPrefix: string): Promise<string[]> {
|
||||
const keys = await this.storage.keys();
|
||||
return keys.filter((k) =>
|
||||
(k as string).startsWith(chunkPrefix)
|
||||
) as string[];
|
||||
}
|
||||
|
||||
async chunkSize(chunkName: string): Promise<number> {
|
||||
const chunk = await this.storage.get<Uint8Array>(chunkName);
|
||||
return chunk?.length || 0;
|
||||
}
|
||||
|
||||
async list(): Promise<string[]> {
|
||||
return (await this.storage.keys()) as string[];
|
||||
}
|
||||
}
|
||||
|
||||
export class CacheStorageFileStore implements IFileStorage {
|
||||
@@ -89,10 +104,10 @@ export class CacheStorageFileStore implements IFileStorage {
|
||||
await cache.put(
|
||||
this.toURL(chunkName),
|
||||
new Response(data, {
|
||||
headers: new Headers({
|
||||
headers: {
|
||||
"Content-Length": data.length.toString(),
|
||||
"Content-Type": "application/encrypted-octet-stream"
|
||||
})
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -108,6 +123,28 @@ export class CacheStorageFileStore implements IFileStorage {
|
||||
return response ? new Uint8Array(await response.arrayBuffer()) : undefined;
|
||||
}
|
||||
|
||||
async listChunks(chunkPrefix: string): Promise<string[]> {
|
||||
const cache = await this.getCache();
|
||||
const keys = await cache.keys();
|
||||
return keys
|
||||
.filter((k) => k.url.includes(`/${chunkPrefix}`))
|
||||
.map((r) => r.url.slice(r.url.lastIndexOf("/") + 1));
|
||||
}
|
||||
|
||||
async list(): Promise<string[]> {
|
||||
const cache = await this.getCache();
|
||||
const keys = await cache.keys();
|
||||
return keys.map((r) => r.url.slice(1));
|
||||
}
|
||||
|
||||
async chunkSize(chunkName: string): Promise<number> {
|
||||
const cache = await this.getCache();
|
||||
const response = await cache.match(this.toURL(chunkName));
|
||||
const length = response?.headers.get("Content-Length");
|
||||
if (length) return parseInt(length);
|
||||
return response ? (await response.arrayBuffer()).byteLength : 0;
|
||||
}
|
||||
|
||||
private toURL(chunkName: string) {
|
||||
return `/${chunkName}`;
|
||||
}
|
||||
@@ -158,4 +195,16 @@ export class OriginPrivateFileSystem implements IFileStorage {
|
||||
await this.create();
|
||||
return this.worker.readChunk(this.name, chunkName);
|
||||
}
|
||||
async listChunks(chunkPrefix: string): Promise<string[]> {
|
||||
await this.create();
|
||||
return (await this.worker.listChunks(this.name, chunkPrefix)) || [];
|
||||
}
|
||||
async chunkSize(chunkName: string): Promise<number> {
|
||||
await this.create();
|
||||
return await this.worker.chunkSize(this.name, chunkName);
|
||||
}
|
||||
async list(): Promise<string[]> {
|
||||
await this.create();
|
||||
return (await this.worker.list(this.name)) || [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import "web-streams-polyfill/dist/ponyfill";
|
||||
import { xxhash64, createXXHash64 } from "hash-wasm";
|
||||
import axios, { AxiosProgressEvent } from "axios";
|
||||
import axios from "axios";
|
||||
import { AppEventManager, AppEvents } from "../common/app-events";
|
||||
import { StreamableFS } from "@notesnook/streamable-fs";
|
||||
import { NNCrypto } from "./nncrypto";
|
||||
@@ -50,15 +50,16 @@ import {
|
||||
RequestOptions
|
||||
} from "@notesnook/core/dist/interfaces";
|
||||
import { logger } from "../utils/logger";
|
||||
import { newQueue } from "@henrygd/queue";
|
||||
|
||||
const ABYTES = 17;
|
||||
export const ABYTES = 17;
|
||||
const CHUNK_SIZE = 512 * 1024;
|
||||
const ENCRYPTED_CHUNK_SIZE = CHUNK_SIZE + ABYTES;
|
||||
const UPLOAD_PART_REQUIRED_CHUNKS = Math.ceil(
|
||||
(5 * 1024 * 1024) / ENCRYPTED_CHUNK_SIZE
|
||||
(10 * 1024 * 1024) / ENCRYPTED_CHUNK_SIZE
|
||||
);
|
||||
const MINIMUM_MULTIPART_FILE_SIZE = 25 * 1024 * 1024;
|
||||
const streamablefs = new StreamableFS(
|
||||
export const streamablefs = new StreamableFS(
|
||||
isFeatureSupported("opfs")
|
||||
? new OriginPrivateFileSystem("streamable-fs")
|
||||
: isFeatureSupported("cache")
|
||||
@@ -234,23 +235,21 @@ async function uploadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptionsWithSignal
|
||||
) {
|
||||
const fileHandle = await streamablefs.readFile(filename);
|
||||
if (!fileHandle || !(await exists(fileHandle)))
|
||||
throw new Error(
|
||||
`File is corrupt or missing data. Please upload the file again. (File hash: ${filename})`
|
||||
);
|
||||
if (fileHandle.file.additionalData?.uploaded) return true;
|
||||
|
||||
// if file already exists on the server, we just return true
|
||||
// we don't reupload the file i.e. overwriting is not possible.
|
||||
const uploadedFileSize = await getUploadedFileSize(filename);
|
||||
if (uploadedFileSize === -1) return false;
|
||||
if (uploadedFileSize > 0) return true;
|
||||
if (uploadedFileSize > 0 && uploadedFileSize === (await fileHandle.size()))
|
||||
return true;
|
||||
|
||||
const fileHandle = await streamablefs.readFile(filename);
|
||||
if (!fileHandle || !(await exists(filename)))
|
||||
throw new Error(
|
||||
`File is corrupt or missing data. Please upload the file again. (File hash: ${filename})`
|
||||
);
|
||||
try {
|
||||
if (fileHandle.file.additionalData?.uploaded) {
|
||||
await checkUpload(filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
const uploaded =
|
||||
fileHandle.file.size < MINIMUM_MULTIPART_FILE_SIZE
|
||||
? await singlePartUploadFile(fileHandle, filename, requestOptions)
|
||||
@@ -258,11 +257,7 @@ async function uploadFile(
|
||||
|
||||
if (uploaded) {
|
||||
await checkUpload(filename);
|
||||
|
||||
await fileHandle.addAdditionalData("uploaded", true);
|
||||
if (isAttachmentDeletable(fileHandle.file.type)) {
|
||||
await streamablefs.deleteFile(filename);
|
||||
}
|
||||
}
|
||||
|
||||
return uploaded;
|
||||
@@ -340,7 +335,7 @@ async function multiPartUploadFile(
|
||||
{}) as UploadAdditionalData;
|
||||
|
||||
const TOTAL_PARTS = Math.ceil(
|
||||
fileHandle.file.chunks / UPLOAD_PART_REQUIRED_CHUNKS
|
||||
fileHandle.chunks.length / UPLOAD_PART_REQUIRED_CHUNKS
|
||||
);
|
||||
const { uploadedChunks = [] } = additionalData;
|
||||
let { uploadedBytes = 0, uploadId = "" } = additionalData;
|
||||
@@ -368,11 +363,11 @@ async function multiPartUploadFile(
|
||||
|
||||
await fileHandle.addAdditionalData("uploadId", uploadId);
|
||||
|
||||
const onUploadProgress = (ev: AxiosProgressEvent) => {
|
||||
const onUploadProgress = () => {
|
||||
reportProgress(
|
||||
{
|
||||
total: fileHandle.file.size + ABYTES * TOTAL_PARTS,
|
||||
loaded: uploadedBytes + ev.loaded
|
||||
loaded: uploadedBytes
|
||||
},
|
||||
{
|
||||
type: "upload",
|
||||
@@ -381,41 +376,49 @@ async function multiPartUploadFile(
|
||||
);
|
||||
};
|
||||
|
||||
onUploadProgress({ bytes: 0, loaded: 0 });
|
||||
onUploadProgress();
|
||||
const queue = newQueue(4);
|
||||
for (let i = uploadedChunks.length; i < TOTAL_PARTS; ++i) {
|
||||
const blob = await fileHandle.readChunks(
|
||||
i * UPLOAD_PART_REQUIRED_CHUNKS,
|
||||
const from = i * UPLOAD_PART_REQUIRED_CHUNKS;
|
||||
const length = Math.min(
|
||||
fileHandle.chunks.length - from,
|
||||
UPLOAD_PART_REQUIRED_CHUNKS
|
||||
);
|
||||
const url = parts[i];
|
||||
const response = await axios
|
||||
.request({
|
||||
url,
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "" },
|
||||
signal,
|
||||
data: blob,
|
||||
onUploadProgress
|
||||
})
|
||||
.catch((e) => {
|
||||
throw new WrappedError(`Failed to upload part at offset ${i}`, e);
|
||||
});
|
||||
|
||||
if (!response.headers.etag || typeof response.headers.etag !== "string")
|
||||
throw new Error(
|
||||
`Failed to upload part at offset ${i}: invalid etag. ETag: ${response.headers.etag}`
|
||||
queue.add(async () => {
|
||||
const blob = await fileHandle.readChunks(
|
||||
i * UPLOAD_PART_REQUIRED_CHUNKS,
|
||||
length
|
||||
);
|
||||
const response = await axios
|
||||
.request({
|
||||
url,
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "" },
|
||||
signal,
|
||||
data: blob,
|
||||
onUploadProgress: (ev) => {
|
||||
uploadedBytes += ev.bytes;
|
||||
onUploadProgress();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
throw new WrappedError(`Failed to upload part at offset ${i}`, e);
|
||||
});
|
||||
|
||||
uploadedBytes += blob.size;
|
||||
uploadedChunks.push({
|
||||
PartNumber: i + 1,
|
||||
ETag: JSON.parse(response.headers.etag)
|
||||
if (!response.headers.etag || typeof response.headers.etag !== "string")
|
||||
throw new Error(
|
||||
`Failed to upload part at offset ${i}: invalid etag. ETag: ${response.headers.etag}`
|
||||
);
|
||||
uploadedChunks.push({
|
||||
PartNumber: i + 1,
|
||||
ETag: JSON.parse(response.headers.etag)
|
||||
});
|
||||
await fileHandle.addAdditionalData("uploadedChunks", uploadedChunks);
|
||||
await fileHandle.addAdditionalData("uploadedBytes", uploadedBytes);
|
||||
});
|
||||
await fileHandle.addAdditionalData("uploadedChunks", uploadedChunks);
|
||||
await fileHandle.addAdditionalData("uploadedBytes", uploadedBytes);
|
||||
|
||||
onUploadProgress({ bytes: 0, loaded: 0 });
|
||||
}
|
||||
await queue.done();
|
||||
|
||||
await axios
|
||||
.post(
|
||||
@@ -423,7 +426,7 @@ async function multiPartUploadFile(
|
||||
{
|
||||
Key: filename,
|
||||
UploadId: uploadId,
|
||||
PartETags: uploadedChunks
|
||||
PartETags: uploadedChunks.sort((a, b) => a.PartNumber - b.PartNumber)
|
||||
},
|
||||
{
|
||||
headers,
|
||||
@@ -446,10 +449,14 @@ async function resetUpload(fileHandle: FileHandle) {
|
||||
}
|
||||
|
||||
async function checkUpload(filename: string) {
|
||||
if ((await getUploadedFileSize(filename)) <= 0) {
|
||||
const error = `Upload verification failed: file size is 0. Please upload this file again. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
const size = await getUploadedFileSize(filename);
|
||||
const error =
|
||||
size === 0
|
||||
? `Upload verification failed: file size is 0. Please upload this file again. (File hash: ${filename})`
|
||||
: size === -1
|
||||
? `Upload verification failed.`
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
function reportProgress(
|
||||
@@ -473,12 +480,8 @@ async function downloadFile(
|
||||
const { url, headers, chunkSize, signal } = requestOptions;
|
||||
const handle = await streamablefs.readFile(filename);
|
||||
|
||||
if (
|
||||
handle &&
|
||||
handle.file.size === (await handle.size()) - handle.file.chunks * ABYTES
|
||||
)
|
||||
return true;
|
||||
else if (handle) await handle.delete();
|
||||
if (handle && (await exists(handle))) return true;
|
||||
if (handle) await handle.delete();
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) throw new Error("Attachment doesn't exist.");
|
||||
@@ -501,51 +504,40 @@ async function downloadFile(
|
||||
signal
|
||||
});
|
||||
|
||||
logger.debug("Got attachment", { filename });
|
||||
const size = parseInt(response.headers.get("content-length") || "0");
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType === "application/xml") {
|
||||
const error = parseS3Error(await response.text());
|
||||
if (error.Code !== "Unknown") {
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
}
|
||||
const contentLength = parseInt(
|
||||
response.headers.get("content-length") || "0"
|
||||
);
|
||||
if (contentLength === 0 || isNaN(contentLength)) {
|
||||
if (size <= 0) {
|
||||
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
const error = `The download response does not contain a body. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(contentLength / chunkSize);
|
||||
const decryptedLength = contentLength - totalChunks * ABYTES;
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
if (attachment && attachment.size !== decryptedLength) {
|
||||
const error = `File length mismatch. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const fileHandle = await streamablefs.createFile(
|
||||
filename,
|
||||
decryptedLength,
|
||||
attachment?.mimeType || "application/octet-stream"
|
||||
);
|
||||
if (response.headers.get("content-type") === "application/xml") {
|
||||
const error = parseS3Error(await response.text());
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
const tempFileHandle = await streamablefs.createFile(
|
||||
`${filename}-temp`,
|
||||
decryptedLength,
|
||||
attachment.mimeType || "application/octet-stream",
|
||||
{ overwrite: true }
|
||||
);
|
||||
await response.body
|
||||
.pipeThrough(
|
||||
?.pipeThrough(
|
||||
new ProgressStream((totalRead, done) => {
|
||||
reportProgress(
|
||||
{
|
||||
total: contentLength,
|
||||
loaded: done ? contentLength : totalRead
|
||||
total: size,
|
||||
loaded: done ? size : totalRead
|
||||
},
|
||||
{ type: "download", hash: filename }
|
||||
);
|
||||
@@ -557,7 +549,17 @@ async function downloadFile(
|
||||
isFeatureSupported("opfs") ? "copy" : "nocopy"
|
||||
)
|
||||
)
|
||||
.pipeTo(fileHandle.writeable);
|
||||
.pipeTo(tempFileHandle.writeable);
|
||||
|
||||
await streamablefs.moveFile(
|
||||
tempFileHandle,
|
||||
await streamablefs.createFile(
|
||||
filename,
|
||||
decryptedLength,
|
||||
attachment.mimeType || "application/octet-stream",
|
||||
{ overwrite: true }
|
||||
)
|
||||
);
|
||||
|
||||
logger.debug("Attachment downloaded", { filename });
|
||||
return true;
|
||||
@@ -569,14 +571,24 @@ async function downloadFile(
|
||||
}
|
||||
}
|
||||
|
||||
async function exists(filename: string) {
|
||||
const handle = await streamablefs.readFile(filename);
|
||||
async function exists(filename: string | FileHandle) {
|
||||
const handle =
|
||||
typeof filename === "string"
|
||||
? await streamablefs.readFile(filename)
|
||||
: filename;
|
||||
return (
|
||||
!!handle &&
|
||||
handle.file.size === (await handle.size()) - handle.file.chunks * ABYTES
|
||||
handle.file.size === (await handle.size()) - handle.chunks.length * ABYTES
|
||||
);
|
||||
}
|
||||
|
||||
async function bulkExists(filenames: string[]) {
|
||||
const files = (await streamablefs.list()).map((c) =>
|
||||
c.replace(/-chunk-\d+/, "")
|
||||
);
|
||||
return Array.from(new Set(filenames).difference(new Set(files)).values());
|
||||
}
|
||||
|
||||
type FileMetadata = {
|
||||
key: SerializedKey;
|
||||
iv: string;
|
||||
@@ -610,14 +622,11 @@ export async function streamingDecryptFile(
|
||||
|
||||
export async function saveFile(filename: string, fileMetadata: FileMetadata) {
|
||||
logger.debug("Saving file", { filename });
|
||||
const { name, type, isUploaded } = fileMetadata;
|
||||
const { name, type } = fileMetadata;
|
||||
|
||||
const decrypted = await decryptFile(filename, fileMetadata);
|
||||
logger.debug("Decrypting file", { filename, result: !!decrypted });
|
||||
if (decrypted) saveAs(decrypted, getFileNameWithExtension(name, type));
|
||||
|
||||
if (isUploaded && isAttachmentDeletable(type))
|
||||
await streamablefs.deleteFile(filename);
|
||||
}
|
||||
|
||||
async function deleteFile(
|
||||
@@ -681,13 +690,11 @@ export const FileStorage: IFileStorage = {
|
||||
deleteFile,
|
||||
exists,
|
||||
clearFileStorage,
|
||||
hashBase64
|
||||
hashBase64,
|
||||
getUploadedFileSize,
|
||||
bulkExists
|
||||
};
|
||||
|
||||
function isAttachmentDeletable(type: string) {
|
||||
return !type.startsWith("image/") && !type.startsWith("application/pdf");
|
||||
}
|
||||
|
||||
function isSuccessStatusCode(statusCode: number) {
|
||||
return statusCode >= 200 && statusCode <= 299;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ import { INNCrypto } from "@notesnook/crypto";
|
||||
import CryptoWorker from "./nncrypto.worker?worker";
|
||||
import { wrap } from "comlink";
|
||||
|
||||
export const NNCrypto =
|
||||
IS_DESKTOP_APP && window.NativeNNCrypto
|
||||
? new window.NativeNNCrypto()
|
||||
: (wrap<INNCrypto>(new CryptoWorker()) as INNCrypto);
|
||||
export const NNCrypto = wrap<INNCrypto>(new CryptoWorker()) as INNCrypto;
|
||||
// TODO: disable until we fix the `pull failed` errors for good.
|
||||
// IS_DESKTOP_APP && window.NativeNNCrypto
|
||||
// ? new window.NativeNNCrypto()
|
||||
// : (wrap<INNCrypto>(new CryptoWorker()) as INNCrypto);
|
||||
|
||||
@@ -93,6 +93,36 @@ class OriginPrivateFileStore implements IFileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
async chunkSize(chunkName: string): Promise<number> {
|
||||
try {
|
||||
if (Object.hasOwn(FileSystemSyncAccessHandle.prototype, "mode")) {
|
||||
return readFileSize(this.directory, chunkName);
|
||||
}
|
||||
return await this.safeOp(chunkName, () =>
|
||||
readFileSize(this.directory, chunkName)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Failed to get chunk size", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async listChunks(chunkPrefix: string): Promise<string[]> {
|
||||
const chunks: string[] = [];
|
||||
for await (const entry of this.directory.keys()) {
|
||||
if (entry.startsWith(chunkPrefix)) chunks.push(entry);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
async list(): Promise<string[]> {
|
||||
const chunks: string[] = [];
|
||||
for await (const entry of this.directory.keys()) {
|
||||
chunks.push(entry);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private async safeOp<T>(chunkName: string, createPromise: () => Promise<T>) {
|
||||
const lock = this.locks.get(chunkName);
|
||||
if (lock) await lock;
|
||||
@@ -139,6 +169,15 @@ const workerModule = {
|
||||
async readChunk(directoryName: string, chunkName: string) {
|
||||
const chunk = await fileStores.get(directoryName)?.readChunk(chunkName);
|
||||
return chunk ? transfer(chunk, [chunk.buffer]) : undefined;
|
||||
},
|
||||
async listChunks(directoryName: string, chunkPrefix: string) {
|
||||
return (await fileStores.get(directoryName)?.listChunks(chunkPrefix)) || [];
|
||||
},
|
||||
async list(directoryName: string) {
|
||||
return (await fileStores.get(directoryName)?.list()) || [];
|
||||
},
|
||||
async chunkSize(directoryName: string, chunkName: string) {
|
||||
return (await fileStores.get(directoryName)?.chunkSize(chunkName)) || 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,3 +193,11 @@ async function readFile(directory: FileSystemDirectoryHandle, name: string) {
|
||||
handle.close();
|
||||
return buffer;
|
||||
}
|
||||
async function readFileSize(
|
||||
directory: FileSystemDirectoryHandle,
|
||||
name: string
|
||||
) {
|
||||
const file = await directory.getFileHandle(name);
|
||||
const handle = await file.createSyncAccessHandle({ mode: "read-only" });
|
||||
return handle.getSize();
|
||||
}
|
||||
|
||||
@@ -94,16 +94,13 @@ function RouteWrapper(props: {
|
||||
}) {
|
||||
const { component, path, routeProps } = props;
|
||||
const result = usePromise(async () => {
|
||||
await import("./hooks/use-database").then(({ loadDatabase }) =>
|
||||
loadDatabase(
|
||||
path !== "/sessionexpired" || Config.get("sessionExpired", false)
|
||||
? "db"
|
||||
: "memory"
|
||||
)
|
||||
const { loadDatabase } = await import("./hooks/use-database");
|
||||
await loadDatabase(
|
||||
path !== "/sessionexpired" || Config.get("sessionExpired", false)
|
||||
? "db"
|
||||
: "memory"
|
||||
);
|
||||
|
||||
const { default: Component } = await component();
|
||||
return Component;
|
||||
return (await component()).default;
|
||||
}, [component, path]);
|
||||
|
||||
if (result.status === "rejected") {
|
||||
|
||||
@@ -57,6 +57,7 @@ type SyncStatus = {
|
||||
};
|
||||
const networkCheck = new NetworkCheck();
|
||||
let syncStatusTimeout = 0;
|
||||
let syncTimeout = 0;
|
||||
let pendingSync: SyncOptions | undefined = undefined;
|
||||
|
||||
class AppStore extends BaseStore<AppStore> {
|
||||
@@ -83,6 +84,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
// this needs to happen here so reminders can be set on app load.
|
||||
reminderStore.refresh();
|
||||
announcementStore.refresh();
|
||||
this.get().sync({ type: "full" });
|
||||
|
||||
EV.subscribe(EVENTS.appRefreshRequested, () => this.refresh());
|
||||
db.eventManager.subscribe(EVENTS.syncCompleted, () => this.refresh());
|
||||
@@ -128,6 +130,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
// a slight delay to make sure sockets are open and can be connected
|
||||
// to. Otherwise, this fails miserably.
|
||||
await networkCheck.waitForInternet();
|
||||
await this.get().sync({ type: "full" });
|
||||
await db.connectSSE({ force: true }).catch(logger.error);
|
||||
}
|
||||
});
|
||||
@@ -136,6 +139,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
if (documentHidden) return;
|
||||
|
||||
logger.info("Page visibility changed. Reconnecting SSE...");
|
||||
await this.get().sync({ type: "full" });
|
||||
await db.connectSSE({ force: false }).catch(logger.error);
|
||||
});
|
||||
};
|
||||
@@ -263,12 +267,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
};
|
||||
|
||||
sync = async (options: SyncOptions = { type: "full", force: false }) => {
|
||||
if (
|
||||
this.isSyncing() ||
|
||||
!this.get().isSyncEnabled ||
|
||||
!navigator.onLine ||
|
||||
!(await networkCheck.waitForInternet())
|
||||
) {
|
||||
if (this.isSyncing() || !this.get().isSyncEnabled) {
|
||||
logger.info("Ignoring duplicate sync", {
|
||||
options,
|
||||
syncing: this.isSyncing(),
|
||||
@@ -279,46 +278,52 @@ class AppStore extends BaseStore<AppStore> {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(syncStatusTimeout);
|
||||
this.updateLastSynced();
|
||||
clearTimeout(syncTimeout);
|
||||
syncTimeout = setTimeout(async () => {
|
||||
clearTimeout(syncStatusTimeout);
|
||||
this.updateSyncStatus("syncing");
|
||||
this.updateLastSynced();
|
||||
|
||||
this.updateSyncStatus("syncing");
|
||||
try {
|
||||
const result = await db.sync(options);
|
||||
try {
|
||||
options.offlineMode = settingStore.get().isFullOfflineMode;
|
||||
const result = await db.sync(options);
|
||||
|
||||
if (!result) return this.updateSyncStatus("failed");
|
||||
this.updateSyncStatus("completed", true);
|
||||
if (!result) return this.updateSyncStatus("failed");
|
||||
this.updateSyncStatus("completed", true);
|
||||
|
||||
await this.updateLastSynced();
|
||||
await this.updateLastSynced();
|
||||
|
||||
if (pendingSync) {
|
||||
logger.info("Running pending sync", pendingSync);
|
||||
const syncOptions = { ...pendingSync };
|
||||
pendingSync = undefined;
|
||||
await this.get().sync(syncOptions);
|
||||
if (pendingSync) {
|
||||
logger.info("Running pending sync", pendingSync);
|
||||
const syncOptions = { ...pendingSync };
|
||||
pendingSync = undefined;
|
||||
await this.get().sync(syncOptions);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(err);
|
||||
if (err.cause === "MERGE_CONFLICT") {
|
||||
await this.refresh();
|
||||
this.updateSyncStatus("conflicts");
|
||||
} else {
|
||||
this.updateSyncStatus("failed");
|
||||
}
|
||||
|
||||
if (
|
||||
err?.message?.indexOf("Failed to fetch") > -1 ||
|
||||
err?.message?.indexOf("Could not connect to the Sync server.") > -1 ||
|
||||
!navigator.onLine ||
|
||||
!(await networkCheck.waitForInternet())
|
||||
)
|
||||
return;
|
||||
|
||||
showToast("error", err.message);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(err);
|
||||
if (err.cause === "MERGE_CONFLICT") {
|
||||
await this.refresh();
|
||||
this.updateSyncStatus("conflicts");
|
||||
} else {
|
||||
this.updateSyncStatus("failed");
|
||||
}
|
||||
|
||||
if (
|
||||
err?.message?.indexOf("Failed to fetch") > -1 ||
|
||||
err?.message?.indexOf("Could not connect to the Sync server.") > -1
|
||||
)
|
||||
return;
|
||||
|
||||
showToast("error", err.message);
|
||||
}
|
||||
}, 300) as unknown as number;
|
||||
};
|
||||
|
||||
abortSync = async (status: SyncState = "offline") => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user