mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 19:19:34 +02:00
Compare commits
25 Commits
fix/attach
...
v3.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9a8134265 | ||
|
|
cd7a5d2ec0 | ||
|
|
b92bc03fa9 | ||
|
|
5f5e472800 | ||
|
|
0951906cac | ||
|
|
6f0fe03760 | ||
|
|
159ce97e42 | ||
|
|
8a9848634c | ||
|
|
282705a683 | ||
|
|
c91b706ed0 | ||
|
|
16afc07b2f | ||
|
|
04f8be5a62 | ||
|
|
0e8a5ca1fa | ||
|
|
1491bf771e | ||
|
|
0f5ce465c3 | ||
|
|
647ff7ae0e | ||
|
|
45a552adef | ||
|
|
8e207d787d | ||
|
|
00754593ac | ||
|
|
b22770f1ae | ||
|
|
523da4e91f | ||
|
|
de6c8154a3 | ||
|
|
9ef32e8081 | ||
|
|
5f53593a26 | ||
|
|
1f404944d9 |
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
9
apps/mobile/package-lock.json
generated
9
apps/mobile/package-lock.json
generated
@@ -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",
|
||||
|
||||
4
apps/web/package-lock.json
generated
4
apps/web/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user