Compare commits

..

25 Commits

Author SHA1 Message Date
Abdullah Atta
c9a8134265 core: fix notes & notebooks sorting 2024-05-05 00:00:49 +05:00
Ammar Ahmed
cd7a5d2ec0 mobile: fix error not rendered correctly 2024-05-04 23:52:47 +05:00
Abdullah Atta
b92bc03fa9 web: handle errors when saving attachment 2024-05-04 23:51:42 +05:00
Abdullah Atta
5f5e472800 web: bump version to 3.0.2 2024-05-04 23:35:49 +05:00
Ammar Ahmed
0951906cac mobile: do not check for .nnbackup file in zip 2024-05-04 23:29:59 +05:00
Abdullah Atta
6f0fe03760 core: do not throw if user not found during legacy backup 2024-05-04 23:28:37 +05:00
Muhammad Ali
159ce97e42 core: fix crash due to unsanitized sort options (#5324)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-05-04 23:26:58 +05:00
Ammar Ahmed
8a9848634c mobile: fix note not opening from notifications 2024-05-04 23:26:07 +05:00
Ammar Ahmed
282705a683 mobile: add logging 2024-05-04 23:11:30 +05:00
Ammar Ahmed
c91b706ed0 mobile: correctly show mime type 2024-05-04 23:11:30 +05:00
Ammar Ahmed
16afc07b2f mobile: fix all attachments show in audios 2024-05-04 23:11:30 +05:00
Ammar Ahmed
04f8be5a62 core: add missing audios attachments filter 2024-05-04 23:11:30 +05:00
Ammar Ahmed
0e8a5ca1fa editor: fix n.startsWith is not a function 2024-05-04 23:03:54 +05:00
Ammar Ahmed
1491bf771e Fix image and file uploads on mobile (#5304)
* mobile: fix image file formats support

* mobile: fix image and file uploads

---------

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-04 23:02:07 +05:00
Abdullah Atta
0f5ce465c3 web: add logging related to attachments 2024-05-04 22:51:01 +05:00
Abdullah Atta
647ff7ae0e web: prevent overwriting uploaded attachments 2024-05-04 22:51:01 +05:00
Abdullah Atta
45a552adef web: handle errors during uploaded file size check 2024-05-04 22:51:01 +05:00
Abdullah Atta
8e207d787d web: delete file before reuploading 2024-05-04 22:51:01 +05:00
Abdullah Atta
00754593ac web: fix attachment not getting marked as failed on error 2024-05-04 22:51:01 +05:00
Abdullah Atta
b22770f1ae core: do not reset attachment dateUploaded when marking as failed 2024-05-04 22:51:01 +05:00
Abdullah Atta
523da4e91f core: only delete attachment file (not the metadata) on sync conflict 2024-05-04 22:51:01 +05:00
Abdullah Atta
de6c8154a3 web: fix password recovery not working 2024-05-04 22:50:44 +05:00
Abdullah Atta
9ef32e8081 web: do not hide attachment manager for logged out user 2024-05-01 20:14:20 +05:00
Abdullah Atta
5f53593a26 web: do not throw if backup doesn't contain .nnbackup file 2024-05-01 20:12:23 +05:00
Abdullah Atta
1f404944d9 web: re-enable service worker 2024-05-01 16:38:09 +05:00
33 changed files with 431 additions and 252 deletions

View File

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

View File

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

View File

@@ -119,18 +119,17 @@ 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) {
return 0;
DatabaseLogger.error(e);
return -1;
}
}
@@ -144,7 +143,9 @@ export async function checkAttachment(hash) {
try {
const size = await getUploadedFileSize(hash);
if (size <= 0) return { failed: "File length is 0." };
if (size === -1) return { success: true };
if (size === 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e?.message };
}

View File

@@ -25,6 +25,7 @@ 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";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
@@ -33,6 +34,18 @@ export async function uploadFile(filename, data, cancelToken) {
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
DatabaseLogger.log("Upload verification failed.");
return false;
}
const isUploaded = uploadedFileSize !== 0;
if (isUploaded) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, {
method: "PUT",
headers
@@ -50,7 +63,15 @@ export async function uploadFile(filename, data, cancelToken) {
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(uploadFilePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
let request = RNFetchBlob.config({

View File

@@ -56,6 +56,7 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import SheetProvider from "../sheet-provider";
const Actions = ({
attachment,
@@ -218,6 +219,7 @@ const Actions = ({
}}
>
<Dialog context={contextId} />
<SheetProvider context={contextId} />
<View
style={{
borderBottomWidth: 1,
@@ -350,7 +352,7 @@ const Actions = ({
{failed ? (
<Notice
type="alert"
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
size="small"
/>
) : null}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useState } from "react";
import { Image, ScrollView, TouchableOpacity, View } from "react-native";
import { ImagePickerResponse } from "react-native-image-picker";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "../../../../../../packages/theme/dist";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
@@ -32,7 +32,7 @@ export default function AttachImage({
onAttach,
close
}: {
response: ImagePickerResponse;
response: ImageType[];
onAttach: ({ compress }: { compress: boolean }) => void;
close: ((ctx?: string | undefined) => void) | undefined;
}) {
@@ -58,14 +58,14 @@ export default function AttachImage({
}}
>
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
Attaching {response.assets?.length} image(s):
Attaching {response?.length} image(s):
</Paragraph>
<ScrollView horizontal>
{response.assets?.map((item) => (
<TouchableOpacity key={item.fileName} activeOpacity={0.9}>
{response?.map((item) => (
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
<Image
source={{
uri: item.uri
uri: item.sourceURL || item.path
}}
style={{
width: 100,
@@ -142,7 +142,7 @@ export default function AttachImage({
<Button
title={`${
(response.assets?.length || 0) > 1 ? "Attach Images" : "Attach Image"
(response?.length || 0) > 1 ? "Attach Images" : "Attach Image"
}`}
type="accent"
width="100%"
@@ -156,10 +156,16 @@ export default function AttachImage({
);
}
AttachImage.present = (response: ImagePickerResponse) => {
return new Promise((resolve) => {
AttachImage.present = (response: ImageType[], context?: string) => {
return new Promise<
| {
compress: boolean;
}
| undefined
>((resolve) => {
let resolved = false;
presentSheet({
context: context,
component: (ref, close, update) => (
<AttachImage
response={response}

View File

@@ -19,10 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import Sodium from "@ammarahmed/react-native-sodium";
import dataurl from "@notesnook/core/dist/utils/dataurl";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
import ImageViewer from "react-native-image-zoom-viewer";
import { db } from "../../common/database";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { cacheDir } from "../../common/filesystem/utils";
import {
@@ -32,13 +34,13 @@ import {
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
const [visible, setVisible] = useState(false);
const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false);
const imageRef = useRef<ImageAttributes>();
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
@@ -48,6 +50,7 @@ const ImagePreview = () => {
}, []);
const open = async (image: ImageAttributes) => {
imageRef.current = image;
setVisible(true);
setLoading(true);
setTimeout(async () => {
@@ -60,6 +63,9 @@ const ImagePreview = () => {
type: "base64",
uri: ""
});
if (imageRef.current) {
imageRef.current.hash = hash;
}
}
if (!hash) return;
//@ts-ignore // FIX ME
@@ -80,7 +86,13 @@ const ImagePreview = () => {
return (
visible && (
<BaseDialog animation="slide" visible={true} onRequestClose={close}>
<BaseDialog
background="black"
animation="slide"
visible={true}
onRequestClose={close}
transparent
>
<View
style={{
width: "100%",
@@ -101,6 +113,21 @@ const ImagePreview = () => {
color={colors.primary.accent}
borderColor="transparent"
/>
<IconButton
onPress={() => {
if (imageRef.current?.hash) {
db.fs().cancel(imageRef.current?.hash);
}
close();
}}
style={{
position: "absolute",
top: Platform.OS === "android" ? 35 : 0,
right: 12
}}
color={colors.static.white}
name="close"
/>
</View>
) : (
<ImageViewer
@@ -117,13 +144,12 @@ const ImagePreview = () => {
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 80,
marginTop: 0,
paddingHorizontal: 12,
height: 50,
paddingHorizontal: 24,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
paddingTop: Platform.OS === "android" ? 30 : 0
marginTop: Platform.OS === "android" ? 30 : 0
}}
>
<IconButton
@@ -137,7 +163,7 @@ const ImagePreview = () => {
)}
imageUrls={[
{
url: image
url: image as string
}
]}
/>

View File

@@ -285,9 +285,9 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
throw new Error("Backup file is invalid");
}
// if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
// throw new Error("Backup file is invalid");
// }
await db.transaction(async () => {
let password;

View File

@@ -21,12 +21,11 @@ import Sodium from "@ammarahmed/react-native-sodium";
import { isImage } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import DocumentPicker from "react-native-document-picker";
import {
ImagePickerResponse,
launchCamera,
launchImageLibrary
} from "react-native-image-picker";
import DocumentPicker, {
DocumentPickerOptions,
DocumentPickerResponse
} from "react-native-document-picker";
import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { compressToFile } from "../../../common/filesystem/compress";
@@ -43,7 +42,7 @@ import { eCloseSheet } from "../../../utils/events";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
const showEncryptionSheet = (file) => {
const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({
title: "Encrypting attachment",
paragraph: `Please wait while we encrypt ${file.name} file for upload`,
@@ -51,24 +50,25 @@ const showEncryptionSheet = (file) => {
});
};
const santizeUri = (uri) => {
const santizeUri = (uri: string) => {
uri = decodeURI(uri);
uri = Platform.OS === "ios" ? uri.replace("file:///", "/") : uri;
return uri;
};
/**
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} fileOptions
*/
const file = async (fileOptions) => {
type PickerOptions = {
noteId?: string;
tabId?: number;
type: "image" | "camera" | "file";
reupload: boolean;
hash?: string;
context?: string;
outputType?: "base64" | "url" | "cache";
};
const file = async (fileOptions: PickerOptions) => {
try {
const options = {
const options: DocumentPickerOptions<"ios"> = {
mode: "import",
allowMultiSelection: false
};
@@ -87,11 +87,11 @@ const file = async (fileOptions) => {
file = file[0];
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri;
let uri = Platform.OS === "ios" ? file.fileCopyUri || file.uri : file.uri;
if (file.size > FILE_SIZE_LIMIT) {
if ((file.size || 0) > FILE_SIZE_LIMIT) {
ToastManager.show({
title: "File too large",
heading: "File too large",
message: "The maximum allowed size per file is 500 MB",
type: "error"
});
@@ -115,22 +115,34 @@ const file = async (fileOptions) => {
uri: uri,
type: "url"
});
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions)))
if (
!(await attachFile(
uri,
hash,
file.type || "application/octet-stream",
file.name,
fileOptions
))
)
return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
if (!fileOptions.tabId) return;
if (
useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
fileOptions.tabId &&
fileOptions.noteId &&
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
fileOptions.noteId
) {
if (isImage(file.type)) {
if (isImage(file.type || "application/octet-stream")) {
editorController.current?.commands.insertImage(
{
hash: hash,
filename: file.name,
mime: file.type,
size: file.size,
dataurl: await db.attachments.read(hash, "base64"),
title: file.name
mime: file.type || "application/octet-stream",
size: file.size || 0,
dataurl: (await db.attachments.read(hash, "base64")) as string,
type: "image"
},
fileOptions.tabId
);
@@ -139,8 +151,9 @@ const file = async (fileOptions) => {
{
hash: hash,
filename: file.name,
mime: file.type,
size: file.size
mime: file.type || "application/octet-stream",
size: file.size || 0,
type: "file"
},
fileOptions.tabId
);
@@ -152,7 +165,7 @@ const file = async (fileOptions) => {
}, 1000);
} catch (e) {
ToastManager.show({
heading: e.message,
heading: (e as Error).message,
message: "You need internet access to attach a file",
type: "error",
context: "global"
@@ -161,29 +174,51 @@ const file = async (fileOptions) => {
}
};
/**
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} options
*/
const camera = async (options) => {
const camera = async (options: PickerOptions) => {
try {
await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
launchCamera(
{
includeBase64: true,
mediaType: "photo"
},
(response) => handleImageResponse(response, options)
);
openCamera({
mediaType: "photo",
includeBase64: true,
cropping: false,
multiple: true,
maxFiles: 10,
writeTempFile: true
})
.then((response) => handleImageResponse(response, options))
.catch((e) => {
console.log("camera error: ", e);
});
} catch (e) {
ToastManager.show({
heading: e.message,
heading: (e as Error).message,
type: "error",
context: "global"
});
console.log("attachment error:", e);
}
};
const gallery = async (options: PickerOptions) => {
try {
await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
openPicker({
includeBase64: true,
mediaType: "photo",
maxFiles: 10,
cropping: false,
multiple: true
})
.then((response) => handleImageResponse(response, options))
.catch((e) => {
console.log("gallery error: ", e);
});
} catch (e) {
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
ToastManager.show({
heading: (e as Error).message,
message: "You need internet access to attach a file",
type: "error",
context: "global"
@@ -192,53 +227,9 @@ const camera = async (options) => {
}
};
const gallery = async (options) => {
try {
await db.attachments.generateKey();
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
launchImageLibrary(
{
includeBase64: true,
mediaType: "photo",
selectionLimit: 10
},
(response) => handleImageResponse(response, options)
);
} catch (e) {
ToastManager.show({
heading: e.message,
message: "You need internet access to attach a file",
type: "error",
context: "global"
});
console.log("attachment error:", e);
}
};
/**
*
* @typedef {{
* noteId?: string,
* tabId?: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* context?: string
* }} ImagePickerOptions
*
* @param {{
* noteId?: string,
* tabId?: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* context?: string
* }} options
* @returns
*/
const pick = async (options) => {
const pick = async (options: PickerOptions) => {
if (!PremiumService.get()) {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (editorState().isFocused) {
editorState().isFocused = true;
}
@@ -259,58 +250,49 @@ const pick = async (options) => {
file(options);
}
};
/**
*
* @param {ImagePickerResponse} response
* @param {ImagePickerOptions} options
* @returns
*/
const handleImageResponse = async (response, options) => {
if (
response.didCancel ||
response.errorMessage ||
!response.assets ||
response.assets?.length === 0
) {
return;
}
const result = await AttachImage.present(response);
const handleImageResponse = async (
response: Image[],
options: PickerOptions
) => {
const result = await AttachImage.present(response, options.context);
if (!result) return;
const compress = result.compress;
for (let image of response.assets) {
const isPng = /(png)/g.test(image.type);
const isJpeg = /(jpeg|jpg)/g.test(image.type);
for (const image of response) {
const isPng = /(png)/g.test(image.mime);
const isJpeg = /(jpeg|jpg)/g.test(image.mime);
if (compress && (isPng || isJpeg)) {
image.uri = await compressToFile(
Platform.OS === "ios" ? "file://" + image.uri : image.uri,
image.path = await compressToFile(
Platform.OS === "ios" ? "file://" + image.path : image.path,
isPng ? "PNG" : "JPEG"
);
const stat = await RNFetchBlob.fs.stat(image.uri.replace("file://", ""));
image.fileSize = stat.size;
const stat = await RNFetchBlob.fs.stat(image.path.replace("file://", ""));
image.size = stat.size;
image.path =
Platform.OS === "ios" ? image.path.replace("file://", "") : image.path;
}
if (image.fileSize > IMAGE_SIZE_LIMIT) {
if (image.size > IMAGE_SIZE_LIMIT) {
ToastManager.show({
title: "File too large",
heading: "File too large",
message: "The maximum allowed size per image is 50 MB",
type: "error"
});
return;
}
let b64 = `data:${image.type};base64, ` + image.base64;
const uri = decodeURI(image.uri);
const b64 = `data:${image.mime};base64, ` + image.data;
const uri = decodeURI(image.path);
const hash = await Sodium.hashFile({
uri: uri,
type: "url"
});
let fileName = image.originalFileName || image.fileName;
const fileName = image.filename || "image";
console.log("attaching file...");
if (!(await attachFile(uri, hash, image.type, fileName, options))) return;
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
console.log("attaching image to note...");
@@ -322,11 +304,13 @@ const handleImageResponse = async (response, options) => {
editorController.current?.commands.insertImage(
{
hash: hash,
mime: image.type,
title: fileName,
mime: image.mime,
type: "image",
dataurl: b64,
size: image.fileSize,
filename: fileName
size: image.size,
filename: fileName as string,
width: image.width,
height: image.height
},
options.tabId
);
@@ -343,10 +327,16 @@ const handleImageResponse = async (response, options) => {
* @param {ImagePickerOptions} options
* @returns
*/
export async function attachFile(uri, hash, type, filename, options) {
export async function attachFile(
uri: string,
hash: string,
type: string,
filename: string,
options: PickerOptions
) {
try {
let exists = await db.attachments.exists(hash);
let encryptionInfo;
const exists = await db.attachments.exists(hash);
let encryptionInfo: any;
if (options?.hash && options.hash !== hash) {
ToastManager.show({
heading: "Please select the same file for reuploading",
@@ -358,26 +348,36 @@ export async function attachFile(uri, hash, type, filename, options) {
}
if (!options.reupload && exists) {
options.reupload = (await filesystem.getUploadedFileSize(hash)) <= 0;
options.reupload = (await filesystem.getUploadedFileSize(hash)) === 0;
}
if (options.reupload) {
DatabaseLogger.log(`Deleting file before reupload. ${hash}`);
const deleted = await db.fs().deleteFile(hash, false);
if (!deleted)
throw new Error(`Failed to delete file before reupload. ${hash}`);
}
if (!exists || options?.reupload) {
let key = await db.attachments.generateKey();
const key = await db.attachments.generateKey();
encryptionInfo = await Sodium.encryptFile(key, {
uri: uri,
type: options.type || "url",
type: options.outputType || "url",
hash: hash
});
} as any);
encryptionInfo.mimeType = type;
encryptionInfo.filename = filename;
encryptionInfo.alg = "xcha-stream";
encryptionInfo.size = encryptionInfo.length;
encryptionInfo.key = key;
if (options?.reupload && exists) await db.attachments.reset(hash);
if (options?.reupload && exists) {
const attachment = await db.attachments.attachment(hash);
if (attachment) await db.attachments.reset(attachment?.id);
}
} else {
encryptionInfo = { hash: hash };
}
await db.attachments.add(encryptionInfo, options.noteId);
await db.attachments.add(encryptionInfo);
return true;
} catch (e) {
DatabaseLogger.error(e);

View File

@@ -77,6 +77,7 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState, openInternalLink } from "./utils";
const publishNote = async () => {
const user = useUserStore.getState().user;
if (!user) {
@@ -449,7 +450,7 @@ export const useEditorEvents = (
break;
case EventTypes.filepicker:
editorState().isAwaitingResult = true;
const { pick } = require("./picker.js").default;
const { pick } = require("./picker").default;
pick({
type: editorMessage.value,
noteId: noteId,

View File

@@ -164,12 +164,6 @@ export const LICENSES = [
author: "dooboolab",
link: "https://github.com/dooboolab/react-native-iap"
},
{
name: "react-native-image-picker",
licenseType: "MIT",
author: "Johan du Toit (Johan-dutoit)",
link: "https://github.com/react-native-image-picker/react-native-image-picker"
},
{
name: "react-native-keychain",
licenseType: "MIT",

View File

@@ -50,6 +50,7 @@ import { eSendEvent } from "./event-manager";
import Navigation from "./navigation";
import SettingsService from "./settings";
import { useUserStore } from "../stores/use-user-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
let pinned: DisplayedNotification[] = [];
@@ -140,6 +141,7 @@ const onEvent = async ({ type, detail }: Event) => {
}
editorState().movedAway = false;
const noteId = notification?.id;
console.log("NOTE ID", noteId);
loadNote(noteId as string, true);
}
@@ -408,18 +410,31 @@ async function loadNote(id: string, jump: boolean) {
if (!DDS.isTab && jump) {
tabBarRef.current?.goToPage(1);
}
eSendEvent("loadingNote", note);
setTimeout(
() => {
eSendEvent(eOnLoadNote, {
item: note
});
if (!jump && !DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
},
tabBarRef?.current ? 0 : 2000
MMKV.setString(
"appState",
JSON.stringify({
editing: true,
movedAway: false,
timestamp: Date.now()
})
);
const isLocked = await db.vaults.itemExists({
type: "note",
id: id
});
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
useTabStore.getState().focusTab(tab);
} else {
useTabStore.getState().focusPreviewTab(id, {
noteId: id,
readonly: note.readonly,
noteLocked: isLocked
});
}
}
async function getChannelId(id: "silent" | "vibrate" | "urgent" | "default") {

View File

@@ -319,8 +319,6 @@ PODS:
- React-Core
- react-native-html-to-pdf-lite (0.9.1):
- React
- react-native-image-picker (4.1.2):
- React-Core
- react-native-image-resizer (3.0.5):
- React-Core
- react-native-in-app-review (4.3.3):
@@ -597,7 +595,6 @@ DEPENDENCIES:
- react-native-get-random-values (from `../../node_modules/react-native-get-random-values`)
- react-native-gzip (from `../../node_modules/react-native-gzip`)
- react-native-html-to-pdf-lite (from `../../node_modules/react-native-html-to-pdf-lite`)
- react-native-image-picker (from `../../node_modules/react-native-image-picker`)
- "react-native-image-resizer (from `../../node_modules/@bam.tech/react-native-image-resizer`)"
- react-native-in-app-review (from `../../node_modules/react-native-in-app-review`)
- "react-native-keep-awake (from `../../node_modules/@sayem314/react-native-keep-awake`)"
@@ -736,8 +733,6 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native-gzip"
react-native-html-to-pdf-lite:
:path: "../../node_modules/react-native-html-to-pdf-lite"
react-native-image-picker:
:path: "../../node_modules/react-native-image-picker"
react-native-image-resizer:
:path: "../../node_modules/@bam.tech/react-native-image-resizer"
react-native-in-app-review:
@@ -899,7 +894,6 @@ SPEC CHECKSUMS:
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-gzip: c5e87ee9e359f02350e3a2ee52eb35eddc398868
react-native-html-to-pdf-lite: 21bfb169bf4cbcd7bec9f736975ee1b3f5292d4a
react-native-image-picker: 9c8a2687b69300ad9e95cec5d38f35ab9d32467d
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4
react-native-keep-awake: caee3ff89eaa21dfe29010f0d143566874a04441
@@ -964,4 +958,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
COCOAPODS: 1.12.1
COCOAPODS: 1.14.2

View File

@@ -41,7 +41,6 @@
"react-native-gzip": "1.1.0",
"react-native-html-to-pdf-lite": "^0.9.1",
"react-native-iap": "12.11.0",
"react-native-image-picker": "4.1.2",
"react-native-in-app-review": "4.3.3",
"react-native-keychain": "4.0.5",
"react-native-mmkv-storage": "^0.10.0-alpha.12",

View File

@@ -28473,7 +28473,6 @@
"react-native-html-to-pdf-lite": "^0.9.1",
"react-native-iap": "12.11.0",
"react-native-image-crop-picker": "^0.40.2",
"react-native-image-picker": "4.1.2",
"react-native-in-app-review": "4.3.3",
"react-native-keychain": "4.0.5",
"react-native-mmkv-storage": "^0.10.0-alpha.12",
@@ -45061,14 +45060,6 @@
"react-native": "*"
}
},
"node_modules/react-native-image-picker": {
"version": "4.1.2",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-image-zoom-viewer": {
"version": "3.0.1",
"license": "MIT",

View File

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

View File

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

View File

@@ -18,12 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { lazify } from "../utils/lazify";
import { logger } from "../utils/logger";
import { showToast } from "../utils/toast";
import { db } from "./db";
async function download(hash: string, groupId?: string) {
const attachment = await db.attachments.attachment(hash);
if (!attachment) return;
if (!attachment) {
logger.debug("could not find attachment for download", { hash, groupId });
return;
}
const downloadResult = await db
.fs()
.downloadFile(
@@ -40,19 +44,27 @@ async function download(hash: string, groupId?: string) {
}
export async function saveAttachment(hash: string) {
const response = await download(hash);
if (!response) return;
try {
const response = await download(hash);
if (!response) return;
const { attachment, key } = response;
await lazify(import("../interfaces/fs"), ({ saveFile }) =>
saveFile(attachment.hash, {
key,
iv: attachment.iv,
name: attachment.filename,
type: attachment.mimeType,
isUploaded: !!attachment.dateUploaded
})
);
const { attachment, key } = response;
await lazify(import("../interfaces/fs"), ({ saveFile }) =>
saveFile(attachment.hash, {
key,
iv: attachment.iv,
name: attachment.filename,
type: attachment.mimeType,
isUploaded: !!attachment.dateUploaded
})
);
} catch (e) {
console.error(e);
showToast(
"error",
`Failed to download attachment: ${hash} (error: ${(e as Error).message})`
);
}
}
type OutputTypeToReturnType = {
@@ -68,6 +80,7 @@ export async function downloadAttachment<
type: TType,
groupId?: string
): Promise<TOutputType | undefined> {
logger.debug("downloading attachment", { hash, type, groupId });
try {
const response = await download(hash, groupId);
if (!response) return;
@@ -85,6 +98,7 @@ export async function downloadAttachment<
isUploaded: !!attachment.dateUploaded
})
);
logger.debug("Attachment decrypted", { hash });
if (!blob) return;
return blob as TOutputType;
@@ -106,9 +120,12 @@ export async function checkAttachment(hash: string) {
import("../interfaces/fs"),
({ getUploadedFileSize }) => getUploadedFileSize(hash)
);
if (size <= 0) return { failed: "File length is 0." };
if (size === 0) throw new Error("File length is 0.");
else if (size === -1) throw new Error("File verification check failed.");
} catch (e) {
return { failed: e instanceof Error ? e.message : "Unknown error." };
const reason = e instanceof Error ? e.message : "Unknown error.";
await db.attachments.markAsFailed(attachment.id, reason);
return { failed: reason };
}
return { success: true };
}

View File

@@ -195,7 +195,10 @@ export async function restoreBackupFile(backupFile: File) {
}
entries.push(entry);
}
if (!isValid) throw new Error("Invalid backup.");
if (!isValid)
console.warn(
"The backup file does not contain the verification .nnbackup file."
);
await db.transaction(async () => {
for (const entry of entries) {

View File

@@ -488,12 +488,25 @@ export function Editor(props: EditorProps) {
editor?.attachFile(attachment);
}
}}
onGetAttachmentData={(attachment) => {
return downloadAttachment(
onGetAttachmentData={async (attachment) => {
logger.debug("Getting attachment data", {
hash: attachment.hash,
type: attachment.type
});
const result = await downloadAttachment(
attachment.hash,
attachment.type === "web-clip" ? "text" : "base64",
id?.toString()
);
if (!result)
logger.debug("Got no result after downloading attachment", {
hash: attachment.hash,
type: attachment.type
});
return result;
}}
onAttachFiles={async (files) => {
const editor = useEditorManager.getState().getEditor(id)?.editor;

View File

@@ -189,17 +189,21 @@ async function addAttachment(
const exists = await db.attachments.attachment(hash);
if (!forceWrite && exists) {
forceWrite = (await getUploadedFileSize(hash)) <= 0;
forceWrite = (await getUploadedFileSize(hash)) === 0;
}
if (forceWrite || !exists) {
if (forceWrite && exists) {
if (!(await db.fs().deleteFile(hash, false)))
throw new Error("Failed to delete attachment from server.");
await db.attachments.reset(exists.id);
}
const key: SerializedKey = await getEncryptionKey();
const output = await writeEncryptedFile(file, key, hash);
if (!output) throw new Error("Could not encrypt file.");
if (forceWrite && exists) await db.attachments.reset(hash);
await db.attachments.add({
...output,
hash,

View File

@@ -64,7 +64,6 @@ export const ProfileSettings: SettingsGroup[] = [
key: "manage-attachments",
title: "Attachments",
description: "Manage all your attachments in one place.",
isHidden: () => !useUserStore.getState().isLoggedIn,
components: [
{
type: "button",

View File

@@ -40,7 +40,7 @@ async function renderApp() {
const { useKeyStore } = await import("./interfaces/key-store");
await useKeyStore.getState().init();
// if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
const { default: Component } = await component();
const { default: AppLock } = await import("./views/app-lock");

View File

@@ -49,6 +49,7 @@ import {
Output,
RequestOptions
} from "@notesnook/core/dist/interfaces";
import { logger } from "../utils/logger";
const ABYTES = 17;
const CHUNK_SIZE = 512 * 1024;
@@ -233,6 +234,12 @@ async function uploadFile(
filename: string,
requestOptions: RequestOptionsWithSignal
) {
// 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;
const fileHandle = await streamablefs.readFile(filename);
if (!fileHandle || !(await exists(filename)))
throw new Error(
@@ -456,7 +463,7 @@ async function downloadFile(
requestOptions: RequestOptionsWithSignal
) {
try {
console.log("DOWNLOADING FILE", filename);
logger.debug("DOWNLOADING FILE", { filename });
const { url, headers, chunkSize, signal } = requestOptions;
const handle = await streamablefs.readFile(filename);
@@ -468,6 +475,7 @@ async function downloadFile(
else if (handle) await handle.delete();
const attachment = await db.attachments.attachment(filename);
if (!attachment) throw new Error("Attachment doesn't exist.");
reportProgress(
{ total: 100, loaded: 0 },
@@ -481,10 +489,14 @@ async function downloadFile(
})
).data;
logger.debug("Got attachment signed url", { filename });
const response = await fetch(signedUrl, {
signal
});
logger.debug("Got attachment", { filename });
const contentType = response.headers.get("content-type");
if (contentType === "application/xml") {
const error = parseS3Error(await response.text());
@@ -497,13 +509,13 @@ async function downloadFile(
);
if (contentLength === 0 || isNaN(contentLength)) {
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
await db.attachments.markAsFailed(filename, error);
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(filename, error);
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
@@ -511,7 +523,7 @@ async function downloadFile(
const decryptedLength = contentLength - totalChunks * ABYTES;
if (attachment && attachment.size !== decryptedLength) {
const error = `File length mismatch. Please upload this file again from the attachment manager. (File hash: ${filename})`;
await db.attachments.markAsFailed(filename, error);
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
@@ -541,9 +553,10 @@ async function downloadFile(
)
.pipeTo(fileHandle.writeable);
logger.debug("Attachment downloaded", { filename });
return true;
} catch (e) {
console.error(e);
logger.error(e, "Could not download file", { filename });
showError(toS3Error(e), "Could not download file");
reportProgress(undefined, { type: "download", hash: filename });
return false;
@@ -590,9 +603,11 @@ export async function streamingDecryptFile(
}
export async function saveFile(filename: string, fileMetadata: FileMetadata) {
logger.debug("Saving file", { filename });
const { name, type, isUploaded } = 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))
@@ -624,6 +639,13 @@ async function deleteFile(
}
}
/**
* `-1` means an error during file size
*
* `0` means file either doesn't exist or file is actually of 0 length
*
* `>0` means file is valid
*/
export async function getUploadedFileSize(filename: string) {
try {
const url = `${hosts.API_HOST}/s3?name=${filename}`;
@@ -636,8 +658,8 @@ export async function getUploadedFileSize(filename: string) {
const contentLength = parseInt(attachmentInfo.headers["content-length"]);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
console.error(e);
return 0;
logger.error(e, "Failed to get uploaded file size.", { filename });
return -1;
}
}

View File

@@ -130,8 +130,6 @@ function useAuthenticateUser({
async function authenticateUser() {
setIsAuthenticating(true);
try {
await db.init();
const accessToken = await db.tokenManager.getAccessToken();
if (!accessToken) {
await db.tokenManager.getAccessTokenFromAuthorizationCode(

View File

@@ -331,6 +331,39 @@ describe("format reminder time", () => {
});
});
test("sorting reminders by dateEdited shouldn't throw", () =>
databaseTest().then(async (db) => {
await db.reminders.add({
recurringMode: "day",
date: new Date(0).setHours(14),
mode: "repeat",
title: "Random reminder"
});
await expect(
db.reminders.all.ids({
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
})
).resolves.toBeDefined();
await expect(
db.reminders.all.groups({
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
})
).resolves.toBeDefined();
await expect(
db.reminders.all
.grouped({
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
})
.then((g) => g.item(0))
).resolves.toBeDefined();
}));
async function compareReminder(reminder) {
const db = await databaseTest();
const id = await db.reminders.add(reminder);

View File

@@ -93,14 +93,18 @@ class Merger {
isDeleted(localItem) ||
isDeleted(remoteItem) ||
!localItem.dateUploaded ||
!remoteItem.dateUploaded
!remoteItem.dateUploaded ||
localItem.dateUploaded === remoteItem.dateUploaded
) {
return this.mergeItem(remoteItem, localItem);
}
if (localItem.dateUploaded > remoteItem.dateUploaded) return;
const isRemoved = await this.db.attachments.remove(localItem.hash, true);
logger.debug("Removing local attachment file due to conflict", {
hash: localItem.hash
});
const isRemoved = await this.db.fs().deleteFile(localItem.hash, true);
if (!isRemoved)
throw new Error(
"Conflict could not be resolved in one of the attachments."

View File

@@ -100,6 +100,9 @@ export class Attachments implements ICollection {
async init() {
await this.collection.init();
logger.debug("attachments initialized", {
total: await this.collection.count()
});
}
async add(
@@ -199,8 +202,12 @@ export class Attachments implements ICollection {
}
async remove(hashOrId: string, localOnly: boolean) {
logger.debug("Removing attachment", { hashOrId, localOnly });
const attachment = await this.attachment(hashOrId);
if (!attachment) return false;
if (!attachment) {
logger.debug("Attachment not found", { hashOrId, localOnly });
return false;
}
if (!localOnly && !(await this.canDetach(attachment)))
throw new Error("This attachment is inside a locked note.");
@@ -326,9 +333,11 @@ export class Attachments implements ICollection {
}
async attachment(hashOrId: string): Promise<Attachment | undefined> {
return this.all.find((eb) =>
const attachment = await this.all.find((eb) =>
eb.or([eb("id", "==", hashOrId), eb("hash", "==", hashOrId)])
);
if (attachment) logger.debug("attachment exists", { hashOrId });
return attachment;
}
markAsUploaded(id: string) {
@@ -346,7 +355,6 @@ export class Attachments implements ICollection {
markAsFailed(id: string, reason?: string) {
return this.collection.update([id], {
dateUploaded: null,
failed: reason
});
}

View File

@@ -134,11 +134,7 @@ export class Settings implements ICollection {
}
getGroupOptions(key: GroupingKey) {
const options = this.get(`groupOptions:${key}`);
// TODO: remove this check
if (key === "tags" && options.sortBy === "dateEdited")
options.sortBy = "dateModified";
return options;
return this.get(`groupOptions:${key}`);
}
setGroupOptions(key: GroupingKey, groupOptions: GroupOptions) {

View File

@@ -165,11 +165,10 @@ export default class Backup {
async *exportLegacy(type: BackupPlatform, encrypt = false) {
if (!validTypes.some((t) => t === type))
throw new Error("Invalid type. It must be one of 'mobile' or 'web'.");
if (encrypt && !(await this.db.user.getLegacyUser()))
throw new Error("Please login to create encrypted backups.");
if (encrypt && !(await this.db.user.getLegacyUser())) encrypt = false;
const key = await this.db.user.getLegacyEncryptionKey();
if (encrypt && !key) throw new Error("No encryption key found.");
if (encrypt && !key) encrypt = false;
const keys = await this.db.storage().getAllKeys();
const chunks = toChunks(keys, 20);

View File

@@ -140,6 +140,8 @@ export class FileStorage {
}
async downloadFile(groupId: string, filename: string, chunkSize: number) {
logger.debug("[downloadFile] downloading", { filename, groupId });
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const token = await this.tokenManager.getAccessToken();
const { execute, cancel } = this.fs.downloadFile(filename, {

View File

@@ -443,6 +443,7 @@ export class FilteredSelector<T extends Item> {
}
async grouped(options: GroupOptions) {
sanitizeSortOptions(this.type, options);
const count = await this.count();
return new VirtualizedGrouping<T>(
count,
@@ -466,6 +467,8 @@ export class FilteredSelector<T extends Item> {
}
async groups(options: GroupOptions) {
sanitizeSortOptions(this.type, options);
const fields: Array<
| AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>
| AnyColumn<DatabaseSchema, keyof DatabaseSchema>
@@ -558,6 +561,8 @@ export class FilteredSelector<T extends Item> {
options: GroupOptions | SortOptions,
hasDueDate?: boolean
) {
sanitizeSortOptions(this.type, options);
const sortBy: Set<SortOptions["sortBy"]> = new Set();
if (isGroupOptions(options)) {
if (options.groupBy === "abc") sortBy.add("title");
@@ -612,7 +617,6 @@ export class FilteredSelector<T extends Item> {
);
}
}
return qb;
};
}
@@ -634,3 +638,30 @@ function isSortByDate(options: SortOptions | GroupOptions) {
options.sortBy === "dueDate"
);
}
const BASE_FIELDS: SortOptions["sortBy"][] = ["dateCreated", "dateModified"];
const VALID_SORT_OPTIONS: Record<
keyof DatabaseSchema,
SortOptions["sortBy"][]
> = {
reminders: ["dueDate", "title"],
tags: ["title"],
attachments: ["filename", "dateUploaded", "size"],
colors: ["title"],
notebooks: ["title", "dateDeleted", "dateEdited"],
notes: ["title", "dateDeleted", "dateEdited"],
content: [],
notehistory: [],
relations: [],
sessioncontent: [],
settings: [],
shortcuts: [],
vaults: []
};
function sanitizeSortOptions(type: keyof DatabaseSchema, options: SortOptions) {
const validFields = [...VALID_SORT_OPTIONS[type], ...BASE_FIELDS];
if (!validFields.includes(options.sortBy)) options.sortBy = validFields[0];
return options;
}

View File

@@ -166,7 +166,7 @@ export const Link = Mark.create<LinkOptions>({
renderHTML({ HTMLAttributes }) {
// False positive; we're explicitly checking for javascript: links to ignore them
if (HTMLAttributes.href?.startsWith("javascript:")) {
if (HTMLAttributes.href?.startsWith?.("javascript:")) {
// strip out the href
return [
"a",