Compare commits

..

117 Commits

Author SHA1 Message Date
Abdullah Atta
10d7e46342 core: add support for archiving notes 2023-06-03 23:06:21 +05:00
ammarahm-ed
cb9e6578c1 mobile: downgrade react-native-bootsplash version 2023-06-01 21:16:17 +05:00
Abdullah Atta
17a1475ded editor: do not create 2 toolbar groups for static tools on mobile 2023-06-01 21:15:17 +05:00
Abdullah Atta
2cee5a4d89 editor: set preview attachment icon to eye 2023-06-01 21:15:17 +05:00
Abdullah Atta
337f88c373 editor: fix link popup font family 2023-06-01 21:15:17 +05:00
Abdullah Atta
53d5073690 editor: double click to preview image 2023-06-01 21:15:17 +05:00
Ramiro Pruis
9cc9660b48 editor: allow download & preview of attachments in readonly mode (#2621)
Signed-off-by: Ramiro Pruis<ramapruis@gmail.com>
2023-06-01 20:39:10 +05:00
ammarahm-ed
e6162b2580 mobile: fix crash on download all attachement 2023-05-31 11:31:51 +05:00
ammarahm-ed
abc14d2d1f mobile: set fdroid build to false 2023-05-30 00:17:39 +05:00
ammarahm-ed
fa11b5fabb mobile: update packages 2023-05-29 23:52:43 +05:00
ammarahm-ed
d904916863 mobile: release v2.5.0 2023-05-29 23:01:53 +05:00
Abdullah Atta
0cfc2e1609 web: bump version to 2.5.0 2023-05-29 19:14:53 +05:00
Abdullah Atta
3742e923c9 editor: fix crash due to attachment.mime being undefined 2023-05-29 13:12:28 +05:00
Abdullah Atta
792a254513 web: fix split pane sash styling 2023-05-29 12:09:50 +05:00
Abdullah Atta
17928ba182 web: disable loading announcements during tests 2023-05-27 16:50:24 +05:00
Abdullah Atta
2da0f164c6 web: fix list crash on shift+click when there's no selection 2023-05-27 16:47:54 +05:00
Abdullah Atta
4eabe79e7e web: fix attachment caching issues 2023-05-27 16:14:12 +05:00
Abdullah Atta
89dd8f5b6c web: minor ui fixes for pdf attachment preview 2023-05-27 16:14:12 +05:00
Abdullah Atta
b4d3662897 web: never delete image & pdf attachments 2023-05-27 16:14:12 +05:00
ammarahm-ed
675e1e2c6f mobile: do not cleanup documents from cache after download 2023-05-27 16:14:12 +05:00
ammarahm-ed
0f7f3bae7e mobile: save pdf snapshot 2023-05-27 16:14:12 +05:00
Abdullah Atta
c917d95a7b web: remove unnecessary css import 2023-05-27 16:14:12 +05:00
Abdullah Atta
3ae9001504 mobile: remove rn-fetch-blob patch 2023-05-27 16:14:12 +05:00
Abdullah Atta
cc97b8beed global: update package-lock.json 2023-05-27 16:14:12 +05:00
Abdullah Atta
954bdebcef web: remove @react-pdf-viewer/default-layout 2023-05-27 16:14:12 +05:00
Abdullah Atta
04b62f4e53 misc: update package-lock.json 2023-05-27 16:14:12 +05:00
Abdullah Atta
a2dcb67b36 web: add support for previewing pdf attachments 2023-05-27 16:14:12 +05:00
Abdullah Atta
cc9141c981 editor: enable preview attachment button on desktop 2023-05-27 16:14:12 +05:00
Abdullah Atta
85484b1762 fs: add support for getting actual file size 2023-05-27 16:14:12 +05:00
Abdullah Atta
1a0c94896a config: add new "fs" scope for commits 2023-05-27 16:14:12 +05:00
ammarahm-ed
068a99463d mobile: fix loading 2023-05-27 16:14:12 +05:00
ammarahm-ed
b413315777 mobile: add pdf attachment previews 2023-05-27 16:14:12 +05:00
ammarahm-ed
7227513783 editor: enable attachment previews for files 2023-05-27 16:14:12 +05:00
Abdullah Atta
083e4e1150 web: add basic support for attachment previews 2023-05-27 16:14:12 +05:00
Abdullah Atta
09098410a5 editor: enable previewing attachments on desktop/web 2023-05-27 16:14:12 +05:00
Abdullah Atta
2c9f5df530 web: fix font family in unlock note view 2023-05-25 16:15:01 +05:00
Abdullah Atta
3ad346362b web: fix notebooks in move note dialog not getting checked sometimes 2023-05-25 16:15:01 +05:00
ammarahm-ed
e176b1bf76 mobile: reset premium status 2023-05-25 15:55:14 +05:00
ammarahm-ed
b368232598 mobile: fix app hangs on adding >3 notebooks on iOS 2023-05-25 15:54:54 +05:00
Abdullah Atta
bcd27e10c1 web: add support for search inside notebook 2023-05-25 15:52:58 +05:00
Abdullah Atta
af9b9b7083 web: fix notebook item date font family 2023-05-25 15:44:09 +05:00
ammarahm-ed
ea1565048a mobile: tag multiple notes from manage tags sheet 2023-05-25 15:41:43 +05:00
Muhammad Ali
59b081704b web: add support for setting tags in bulk (#2328)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-25 14:10:32 +05:00
Muhammad Ali
2761222b36 web: add support for printing notes (#2551)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-25 12:19:21 +05:00
Muhammad Ali
571f15b556 web: close deleted note on sync (#2580)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-25 12:14:49 +05:00
Muhammad Ali
283efa6e41 editor: prevent table column size reset in readonly mode (#2586) 2023-05-25 12:11:57 +05:00
Abdullah Atta
b3b1b7470e web: do not show save file dialog if download is canceled 2023-05-25 12:02:50 +05:00
Abdullah Atta
a9e59b17dc web: use fixed size columns in attachment manager 2023-05-25 12:02:50 +05:00
Abdullah Atta
22d946b677 web: fix app lag when downloading attachment 2023-05-25 12:02:50 +05:00
ammarahm-ed
6a748c3f98 mobile: fix download all attachments progress 2023-05-25 10:18:21 +05:00
Abdullah Atta
520296106a editor: bump version to 1.5.0 2023-05-25 09:48:22 +05:00
Abdullah Atta
3b727e6f64 editor: update tiptap packages to v2.0.3 2023-05-25 09:47:20 +05:00
ammarahm-ed
b77711a53d mobile: use isImage check from core 2023-05-23 12:52:50 +05:00
ammarahm-ed
aebb2d414b mobile: allow attaching images from file manager 2023-05-23 12:52:50 +05:00
ammarahm-ed
e0e505371c mobile: update download attachments 2023-05-23 12:52:50 +05:00
ammarahm-ed
b9040a9358 mobile: show error on file check failed 2023-05-23 12:52:50 +05:00
ammarahm-ed
68f10ce308 core: add isAudio function 2023-05-23 12:52:50 +05:00
Abdullah Atta
8a475f654f web: improve attachments manager ui/ux 2023-05-23 12:52:50 +05:00
ammarahm-ed
1043b7df55 mobile: improved attachments manager 2023-05-23 12:52:50 +05:00
ammarahm-ed
8383e09243 mobile: show currently downloading file 2023-05-23 12:52:50 +05:00
ammarahm-ed
df58a6cfc4 mobile: fix recheck attachments 2023-05-23 12:52:50 +05:00
ammarahm-ed
a9df577478 mobile: update deps 2023-05-23 12:52:50 +05:00
ammarahm-ed
e021d93893 mobile: download mutliple attachments 2023-05-23 12:52:50 +05:00
Abdullah Atta
24e13f488e web: fully move to using web streams everywhere 2023-05-23 12:52:50 +05:00
Muhammad Ali
6f88b70937 web: add support for downloading all attachments as zip 2023-05-23 12:52:50 +05:00
ammarahm-ed
a90f957aa4 mobile: always check editor is ready on enter foreground 2023-05-23 08:08:10 +05:00
ammarahm-ed
748953b3eb mobile: don't lock app on showing app review 2023-05-23 08:07:27 +05:00
Muhammad Ali
874c892986 editor: make cell properties work with header cells (#2506) 2023-05-23 07:17:29 +05:00
Muhammad Ali
ab678fa3a9 core: fix duplication of notes that are directly inside a notebook (#2552)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-23 06:40:07 +05:00
Muhammad Ali
c2351ab2d3 editor: add support for link markdown syntax (#2576) 2023-05-23 06:36:34 +05:00
Muhammad Ali
3fd3e02256 web: fix wrong date font in notebook item (#2560)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-23 06:20:12 +05:00
Muhammad Ali
d2fd26abd5 editor: fix position of outline list bullet/arrow (#2555) 2023-05-20 08:03:39 +05:00
ammarahm-ed
dd84b9ca2f mobile: add fdroid changelog 2023-05-18 20:06:37 +05:00
Abdullah Atta
269f083dce clipper: bump version to 0.2.1 2023-05-18 15:14:25 +05:00
Abdullah Atta
e18867d3f1 clipper: request scripting permission 2023-05-18 15:13:44 +05:00
Abdullah Atta
e221882f8e clipper: change version to 0.2.0 2023-05-17 11:35:52 +05:00
Abdullah Atta
153ee4bce3 clipper: load content scripts on demand to reduce host permissions 2023-05-17 11:35:52 +05:00
ammarahm-ed
a2ecdab61c mobile: release 2.4.17 2023-05-17 11:24:29 +05:00
Ammar Ahmed
eb089e6211 Merge pull request #2543 from streetwriters/feat/in-app-review
mobile: add in-app-review flow
2023-05-17 11:18:22 +05:00
ammarahm-ed
1981c3a2f8 mobile: add in-app-review flow 2023-05-17 11:14:49 +05:00
Abdullah Atta
68b3ebe318 clipper: get rid of <all_urls> permission 2023-05-17 10:11:47 +05:00
ammarahm-ed
8a0b6264d2 mobile: fix pro features locked for pro users 2023-05-17 10:09:37 +05:00
Abdullah Atta
239174c3e7 web: bump version to 2.4.11 2023-05-17 09:29:16 +05:00
Abdullah Atta
db1503d828 clipper: remove contextMenus permission 2023-05-16 18:05:20 +05:00
Abdullah Atta
7b1cbba9e7 clipper: bump version to 0.2 2023-05-16 17:39:21 +05:00
Abdullah Atta
2c3c193629 clipper: default to simplified mode if user is no longer pro 2023-05-16 17:35:59 +05:00
Abdullah Atta
899e56e9cd clipper(core): escape % & ' during css stringify 2023-05-16 17:35:59 +05:00
Abdullah Atta
9c9582ae12 clipper: improve error messages 2023-05-16 17:35:59 +05:00
Abdullah Atta
b918667b63 clipper: adjust icon size in inline tag 2023-05-16 17:35:59 +05:00
Abdullah Atta
878c901e06 clipper: add support for assigning multiple notebooks 2023-05-16 17:35:59 +05:00
Muhammad Ali
fdd3897a60 web: close tag dropdown on tab press (#2508) 2023-05-16 17:32:00 +05:00
ammarahm-ed
c958657b01 mobile: release 2.4.16 2023-05-16 12:11:43 +05:00
Abdullah Atta
e7175f48a0 clipper: do not exclude localhost tabs in production mode 2023-05-13 17:51:10 +05:00
Abdullah Atta
4e1862d653 clipper: improve connection reliability
this adds support for working with multiple Notesnook tabs
even though it is not currently recommended that you
open multiple Notesnook tabs
2023-05-13 17:51:10 +05:00
Abdullah Atta
1d5ac37222 clipper: always clip tab from current active window 2023-05-13 17:50:53 +05:00
Abdullah Atta
32d027fff6 clipper(core): don't clip images in simplified mode 2023-05-13 17:50:41 +05:00
ammarahm-ed
9434993373 mobile: remove usesCleartextTraffic in release builds 2023-05-08 18:30:36 +05:00
Abdullah Atta
2bd0f1a304 web: allow users to view their billing history 2023-05-08 15:25:20 +05:00
Abdullah Atta
7f873f240e web: allow requesting refunds from settings 2023-05-08 15:25:20 +05:00
Abdullah Atta
a0229729b7 web: use sandbox checkout during development 2023-05-08 15:25:20 +05:00
Abdullah Atta
dd368a4170 web: show success on checkout completion 2023-05-08 15:25:20 +05:00
ammarahm-ed
6d3e985c9d mobile: fix failing build 2023-05-08 09:27:55 +05:00
ammarahm-ed
15858db21b mobile: sync quick notes only when online 2023-05-08 08:19:17 +05:00
Ammar Ahmed
b69a94a96f mobile: fix premium toast is not shown when using premium features on mobile (#2504) 2023-05-08 08:18:38 +05:00
Muhammad Ali
73b965eb7c web: close properties pane when note is switched (#2486) 2023-05-06 16:16:01 +05:00
Abdullah Atta
5738f48360 core: expire session if key is not found 2023-05-06 15:27:58 +05:00
Muhammad Ali
de74b8c2cd editor: position search box correctly in readonly mode (#2478) 2023-05-03 13:22:25 +05:00
Muhammad Ali
2d07d79057 web: fix issue where notes directly in a notebook could not be unlinked (#2466)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-05-02 12:49:24 +05:00
ammarahm-ed
5f6a38c8b1 mobile: release 2.4.15 2023-04-29 09:07:46 +05:00
Muhammad Ali
3496e6bfa9 editor: increased hit Area 2023-04-29 08:33:20 +05:00
ammarahm-ed
fc5b62ba61 mobile: remove editor flicker on app launch 2023-04-29 08:29:48 +05:00
ammarahm-ed
9d0a206240 mobile: fix editor not rendered when enter foreground 2023-04-29 08:29:48 +05:00
ammarahm-ed
ee6bbca18c mobile: only call verify biometrics once on focus 2023-04-29 05:47:05 +05:00
ammarahm-ed
e6d5472514 mobile: fix rerender crash 2023-04-29 05:46:06 +05:00
Abdullah Atta
1e5f13087b web: try to reconnect sync on window focus 2023-04-28 10:39:59 +05:00
Abdullah Atta
abb89ecbc3 web: bump version to 2.4.10 2023-04-27 15:15:46 +05:00
Abdullah Atta
aa168bec5e desktop: revert "desktop: remove custom protocol interception for https requests" 2023-04-27 15:14:30 +05:00
206 changed files with 17209 additions and 41405 deletions

View File

@@ -20,6 +20,7 @@ const SCOPES = [
"logger",
"theme",
"core",
"fs",
"clipper",
"config",
"ci",

View File

@@ -31,14 +31,6 @@ import { TipManager } from "./services/tip-manager";
import { useUserStore } from "./stores/use-user-store";
import { View } from "react-native";
import { useState } from "react";
import NetInfo from "@react-native-community/netinfo";
NetInfo.configure({
reachabilityUrl: "https://notesnook.com",
reachabilityTest: (response) => {
if (!response) return false;
return response?.status >= 200 && response?.status < 300;
}
});
SettingsService.init();
SettingsService.checkOrientation();

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import {
decrypt,
deriveCryptoKey,

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Dimensions, Platform } from "react-native";
import ImageResizer from "@bam.tech/react-native-image-resizer";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
/**
* Scale down & compress images to screen width
* for loading in editor.

View File

@@ -17,25 +17,169 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import {
getFileNameWithExtension,
isImage,
isDocument
} from "@notesnook/core/utils/filename";
import React from "react";
import { Platform } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import { ToastEvent, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir } from "./utils";
import { getFileNameWithExtension } from "@notesnook/core/utils/filename";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export function downloadAllAttachments() {
const attachments = db.attachments.all;
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(
attachments,
onProgress,
canceled,
groupId
) {
if (!attachments || !attachments.length) return;
const result = new Map();
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder))
await RNFetchBlob.fs.unlink(zipSourceFolder);
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = attachments[i];
const hash = attachment.metadata.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
onProgress?.(
i + 1 / attachments.length,
`Downloading attachments (${i + 1}/${
attachments.length
})... Please wait`
);
// Download to cache
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Success
});
} catch (e) {
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Fail,
reason: e
});
console.log("Error downloading attachment", e);
}
}
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (result?.size) {
let sub;
try {
onProgress?.(0, `Zipping... Please wait`);
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
onProgress(
progress,
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
);
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
onProgress(1, `Saving zip file... Please wait`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
onProgress?.(1, `Done`);
releasePermissions(outputFolder);
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
console.log("Error zipping attachments", e);
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
return result;
}
export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false
cache: false,
throwError: false,
groupId: undefined
}
) {
let attachment = db.attachments.attachment(hash);
@@ -55,8 +199,13 @@ export default async function downloadAttachment(
}
try {
await db.fs.downloadFile(
console.log(
"starting download attachment",
attachment.metadata.hash,
options.groupId
);
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
if (
@@ -99,7 +248,8 @@ export default async function downloadAttachment(
if (
attachment.dateUploaded &&
!attachment.metadata?.type?.startsWith("image")
!isImage(attachment.metadata?.type) &&
!isDocument(attachment.metadata?.type)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
@@ -131,5 +281,8 @@ export default async function downloadAttachment(
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.metadata.hash);
if (options.throwError) {
throw e;
}
}
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import hosts from "@notesnook/core/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Platform } from "react-native";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { cacheDir, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";

View File

@@ -17,10 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir } from "./utils";
import { isImage, isDocument } from "@notesnook/core/utils/filename";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
@@ -63,7 +64,10 @@ export async function uploadFile(filename, data, cancelToken) {
if (result) {
let attachment = db.attachments.attachment(filename);
if (!attachment) return result;
if (!attachment.metadata.type.startsWith("image/")) {
if (
!isImage(attachment.metadata.type) &&
!isDocument(attachment.metadata?.type)
) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
}

View File

@@ -17,7 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import RNFetchBlob from "rn-fetch-blob";
import * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
@@ -62,3 +64,22 @@ export function cancelable(operation) {
};
};
}
export function copyFileAsync(source, dest) {
return new Promise((resolve) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
console.log(e, r);
resolve();
});
});
}
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}

View File

@@ -102,15 +102,21 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastEvent.show({
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
} else {
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
}
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
setAttachments([...db.attachments.all]);
setLoading({
name: null

View File

@@ -22,7 +22,6 @@ import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { formatBytes } from "../../utils";
import { SIZE } from "../../utils/size";
@@ -36,24 +35,27 @@ function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
/**
*
* @param {any} param0
* @returns
*/
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
export const AttachmentItem = ({
attachment,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading
}) => {
const colors = useThemeStore((state) => state.colors);
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const encryptionProgress = useAttachmentStore(
(state) => state.encryptionProgress
);
const onPress = () => {
if (!pressable) return;
Actions.present(attachment, setAttachments, attachment.metadata.hash);
};
return (
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -116,22 +118,22 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
{attachment.metadata.filename}
</Paragraph>
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
</View>
</View>
{currentProgress ||
(encryptionProgress && encryptionProgress !== "0.00") ||
encryption ? (
{currentProgress ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption) return;
if (encryption || !pressable) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
@@ -144,13 +146,7 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={
encryptionProgress
? encryptionProgress
: currentProgress?.value
? currentProgress?.value / 100
: 0
}
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
showsText
textStyle={{
fontSize: 10

View File

@@ -0,0 +1,273 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { presentSheet } from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeStore } from "../../stores/use-theme-store";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
const colors = useThemeStore((state) => state.colors);
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
value: 0,
statusText: "Download started... Please wait"
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef();
const onDownload = async () => {
update({
disableClosing: true
});
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
attachments,
(progress, statusText) => setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update({
disableClosing: false
});
};
const cancel = async () => {
update({
disableClosing: false
});
canceled.current = true;
console.log(groupId.current, "canceling groupId downloads");
await db.fs.cancel(groupId.current);
setDownloading(false);
groupId.current = null;
};
const successResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 1) results.push(db.attachments.attachment(key));
}
return results;
};
const failedResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 0) results.push(db.attachments.attachment(key));
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
}
return (
<View
style={{
alignItems: "center",
width: "100%",
paddingVertical: 12,
paddingHorizontal: 12
}}
>
<Heading>
{downloading
? "Downloading attachments"
: result?.size
? "Downloaded attachments"
: "Download attachments"}
</Heading>
{downloading ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{progress.statusText}
</Paragraph>
) : result?.size ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{getResultText()}
</Paragraph>
) : (
<Paragraph
style={{
textAlign: "center"
}}
>
Are you sure you want to download all attachments
{isNote ? " of this note?" : "?"}
</Paragraph>
)}
{downloading ? (
<View
style={{
width: 200,
marginTop: 10
}}
>
<ProgressBarComponent
height={5}
width={null}
animated={true}
useNativeDriver
progress={progress.value ? progress.value / attachments.length : 0}
unfilledColor={colors.nav}
color={colors.accent}
borderWidth={0}
/>
</View>
) : null}
<FlatList
style={{
maxHeight: 300,
width: "100%",
minHeight: 60,
backgroundColor: colors.nav,
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments : []}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 60
}}
>
<Paragraph color={colors.icon}>No downloads in progress.</Paragraph>
</View>
}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
return (
<AttachmentItem
attachment={item}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
/>
);
}}
/>
{result?.size ? (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={close}
type="accent"
title="Done"
/>
) : !downloading ? (
<View
style={{
flexDirection: "row",
width: "100%",
marginTop: 20
}}
>
<Button
style={{
flex: 1,
borderRadius: 100,
marginRight: 5
}}
onPress={close}
type="grayBg"
title="No"
/>
<Button
style={{
flex: 1,
borderRadius: 100,
marginLeft: 5
}}
onPress={onDownload}
type="accent"
title="Yes"
/>
</View>
) : (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={cancel}
type="error"
title="Cancel"
/>
)}
</View>
);
};
DownloadAttachments.present = (context, attachments, isNote) => {
presentSheet({
context: context,
component: (ref, close, update) => (
<DownloadAttachments
close={close}
attachments={attachments}
isNote={isNote}
update={update}
/>
)
});
};
export default DownloadAttachments;

View File

@@ -18,44 +18,60 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { View } from "react-native";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { presentSheet } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import { FlatList } from "react-native-actions-sheet";
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/utils/filename";
export const AttachmentDialog = ({ data }) => {
export const AttachmentDialog = ({ note }) => {
const colors = useThemeStore((state) => state.colors);
const [note, setNote] = useState(data);
const [attachments, setAttachments] = useState(
data
? db.attachments.ofNote(data.id, "all")
note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const onChangeText = (text) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments([...db.attachments.all]);
setAttachments([...attachments]);
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
db.attachments.all,
attachments,
attachmentSearchValue.current
);
if (results.length === 0) return;
@@ -67,6 +83,77 @@ export const AttachmentDialog = ({ data }) => {
<AttachmentItem setAttachments={setAttachments} attachment={item} />
);
const onCheck = async () => {
setLoading(true);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
if (result.failed) {
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
await db.attachments.markAsFailed(attachment.id, null);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
setLoading(false);
};
const attachmentTypes = [
{
title: "All",
filterBy: "all"
},
{
title: "Images",
filterBy: "images"
},
{
title: "Documents",
filterBy: "documents"
},
{
title: "Video",
filterBy: "video"
},
{
title: "Audio",
filterBy: "audio"
}
];
const filterAttachments = (type) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
isDocument;
switch (type) {
case "all":
return attachments;
case "images":
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
case "video":
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
case "audio":
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
case "documents":
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
}
};
return (
<View
style={{
@@ -75,43 +162,71 @@ export const AttachmentDialog = ({ data }) => {
paddingHorizontal: 12
}}
>
<DialogHeader
title={note ? "Attachments" : "Manage attachments"}
paragraph="Tap on an attachment to view properties"
button={{
title: "Check all",
type: "grayAccent",
loading: loading,
onPress: async () => {
setLoading(true);
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(
attachment.metadata.hash
<SheetProvider context="attachments-list" />
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading>Attachments</Heading>
<View
style={{
flexDirection: "row"
}}
>
{loading ? (
<ActivityIndicator
style={{
height: 40,
width: 40,
marginRight: 10
}}
size={SIZE.lg}
/>
) : (
<IconButton
name="check-all"
customStyle={{
height: 40,
width: 40,
marginRight: 10
}}
color={colors.pri}
size={SIZE.lg}
onPress={onCheck}
/>
)}
<IconButton
name="download"
customStyle={{
height: 40,
width: 40
}}
color={colors.pri}
onPress={() => {
DownloadAttachments.present(
"attachments-list",
attachments,
!!note
);
if (result.failed) {
db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
db.attachments.markAsFailed(attachment.id, null);
}
setAttachments([...db.attachments.all]);
}
setLoading(false);
}
}}
size={SIZE.lg}
/>
</View>
</View>
<Seperator />
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
}}
/>
<Seperator />
{!note ? (
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
}}
/>
) : null}
<FlatList
keyboardDismissMode="none"
@@ -119,6 +234,45 @@ export const AttachmentDialog = ({ data }) => {
maxToRenderPerBatch={10}
initialNumToRender={10}
windowSize={5}
stickyHeaderIndices={[0]}
ListHeaderComponent={
<ScrollView
style={{
width: "100%",
height: 50,
flexDirection: "row",
backgroundColor: colors.bg
}}
contentContainerStyle={{
minWidth: "100%"
}}
horizontal
>
{attachmentTypes.map((item) => (
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
style={{
borderRadius: 0,
borderBottomWidth: 1,
flexGrow: 1,
borderBottomColor:
currentFilter !== item.filterBy
? "transparent"
: colors.accent
}}
onPress={() => {
setCurrentFilter(item.filterBy);
setAttachments(filterAttachments(item.filterBy));
}}
/>
))}
</ScrollView>
}
ListEmptyComponent={
<View
style={{
@@ -162,6 +316,6 @@ export const AttachmentDialog = ({ data }) => {
AttachmentDialog.present = (note) => {
presentSheet({
component: () => <AttachmentDialog data={note} />
component: () => <AttachmentDialog note={note} />
});
};

View File

@@ -36,6 +36,7 @@ import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import PDFPreview from "../dialogs/pdf-preview";
const DialogProvider = () => {
const colors = useThemeStore((state) => state.colors);
@@ -60,6 +61,7 @@ const DialogProvider = () => {
{loading ? null : <Expiring />}
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
</>
);
};

View File

@@ -0,0 +1,324 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import {
addOrientationListener,
removeOrientationListener
} from "react-native-orientation";
import Pdf from "react-native-pdf";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { cacheDir } from "../../../common/filesystem/utils";
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import { presentDialog } from "../../dialog/functions";
import SheetProvider from "../../sheet-provider";
import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { sleep } from "../../../utils/time";
import { MMKV } from "../../../common/database/mmkv";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
const attachmentSnapshotsKey = "___attachmentsnapshots";
const usePDFSnapshot = (attachment) => {
const snapshots = useRef(MMKV.getMap(attachmentSnapshotsKey) || {});
const snapshot = useRef(snapshots[attachment?.id]);
function saveSnapshot(ss) {
if (!attachment) return;
snapshots.current[attachment.id] = ss;
MMKV.setMap(attachmentSnapshotsKey, snapshots.current);
snapshot.current = snapshots.current[attachment.id];
}
return [snapshot, saveSnapshot];
};
const PDFPreview = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [pdfSource, setPDFSource] = useState();
const [loading, setLoading] = useState(false);
const [width, setWidth] = useState(WIN_WIDTH);
const insets = useGlobalSafeAreaInsets();
const [numPages, setNumPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const inputRef = useRef();
const pdfRef = useRef();
const [attachment, setAttachment] = useState(null);
const [password, setPassword] = useState("");
const [progress] = useAttachmentProgress(attachment);
const [snapshot, saveSnapshot] = usePDFSnapshot(attachment);
const snapshotValue = useRef(snapshot.current);
useEffect(() => {
eSubscribeEvent("PDFPreview", open);
return () => {
eUnSubscribeEvent("PDFPreview", open);
};
}, []);
const onOrientationChange = (o) => {
if (o.includes("LANDSCAPE")) {
setWidth(WIN_HEIGHT);
} else {
setWidth(WIN_WIDTH);
}
};
useEffect(() => {
addOrientationListener(onOrientationChange);
return () => {
removeOrientationListener(onOrientationChange);
};
}, []);
const open = async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.metadata.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
setPDFSource(null);
setVisible(false);
setPassword("");
};
const onError = async (error) => {
if (error?.message === "Password required or incorrect password.") {
await sleep(300);
presentDialog({
context: attachment?.metadata?.hash,
input: true,
inputPlaceholder: "Enter password",
positiveText: "Unlock",
title: "Decrypt",
paragraph: "Please input password to view pdf.",
positivePress: (value) => {
setTimeout(() => {
setPassword(value);
});
},
onClose: () => {
close();
}
});
}
};
return (
visible && (
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<SheetProvider context={attachment?.metadata?.hash} />
<Dialog context={attachment?.metadata?.hash} />
<View
style={{
width: "100%",
height: "100%",
backgroundColor: "black"
}}
>
{loading ? (
<Animated.View
exiting={FadeOut}
style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}
>
<ProgressBarComponent
indeterminate={!progress}
color={colors.accent}
borderColor="transparent"
progress={parseInt(progress?.value || "100") / 100}
/>
<Paragraph
style={{
marginTop: 10
}}
color={colors.light}
>
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
... Please wait
</Paragraph>
</Animated.View>
) : (
<>
<View
style={{
width: "100%",
height: 50,
marginTop: insets.top,
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 12,
paddingLeft: 6
}}
>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.light}
name="arrow-left"
onPress={close}
customStyle={{
marginRight: 12
}}
size={SIZE.xxl}
/>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 12
}}
>
<TextInput
ref={inputRef}
defaultValue={currentPage + ""}
style={{
color: colors.pri,
padding: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
paddingVertical: 0,
height: 25,
backgroundColor: colors.nav,
width: 40,
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "OpenSans-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"
onSubmitEditing={(event) => {
setCurrentPage(event.nativeEvent.text);
pdfRef.current?.setPage(parseInt(event.nativeEvent.text));
}}
blurOnSubmit
/>
<Paragraph color={colors.light}>/{numPages}</Paragraph>
</View>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.light}
name="download"
onPress={() => {
downloadAttachment(attachment.metadata.hash, false);
}}
/>
</View>
</View>
{pdfSource ? (
<Animated.View
style={{
flex: 1
}}
entering={FadeIn}
>
<Pdf
source={{
uri: pdfSource
}}
ref={pdfRef}
onLoadComplete={(numberOfPages) => {
setNumPages(numberOfPages);
}}
onPageChanged={(page) => {
setCurrentPage(page);
inputRef.current?.setNativeProps({
text: page + ""
});
saveSnapshot({
currentPage: page,
scale: snapshot?.current?.scale
});
}}
// scale={snapshotValue.current?.scale}
// onScaleChanged={(scale) => {
// saveSnapshot({
// currentPage: snapshot?.current?.currentPage,
// scale: scale
// });
// }}
page={snapshotValue?.current?.currentPage}
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {
console.log(`Link pressed: ${uri}`);
}}
style={{
flex: 1,
width: width,
height: Dimensions.get("window").height
}}
/>
</Animated.View>
) : null}
</>
)}
</View>
</BaseDialog>
)
);
};
export default PDFPreview;

View File

@@ -70,6 +70,7 @@ const Launcher = React.memo(
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const verifying = useRef(false);
const loadNotes = useCallback(async () => {
if (verifyUser) {
@@ -206,6 +207,9 @@ const Launcher = React.memo(
setVerifyUser(false);
enabled(false);
password.current = null;
setTimeout(() => {
verifying.current = false;
}, 1);
}
}, [setVerifyUser]);
@@ -214,7 +218,10 @@ const Launcher = React.memo(
}, [init, verifyUser]);
useEffect(() => {
if (verifying.current || useUserStore.getState().shouldBlockVerifyUser)
return;
if (verifyUser && appState === "active") {
verifying.current = true;
onUnlockBiometrics();
}
}, [appState, onUnlockBiometrics, verifyUser]);

View File

@@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
import { editorState } from "../../screens/editor/tiptap/utils";
import useKeyboard from "../../hooks/use-keyboard";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
@@ -31,6 +31,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { getElevation } from "../../utils";
import {
eCloseActionSheet,
eCloseSheet,
eOpenPremiumDialog,
eShowGetPremium
} from "../../utils/events";
@@ -39,12 +40,12 @@ import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
export const PremiumToast = ({ context = "global", offset = 0 }) => {
const colors = useThemeStore((state) => state.colors);
const [msg, setMsg] = useState(null);
const timer = useRef();
const keyboard = useKeyboard();
const open = useCallback(
(event) => {
@@ -72,7 +73,6 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
useEffect(() => {
eSubscribeEvent(eShowGetPremium, open);
return () => {
clearTimeout(timer.current);
eUnSubscribeEvent(eShowGetPremium, open);
};
}, [open]);
@@ -80,9 +80,7 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
const onPress = async () => {
open(null);
eSendEvent(eCloseActionSheet);
if (editorState().isFocused) {
//tiny.call(EditorWebView, tiny.blur);
}
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenPremiumDialog);
};
@@ -102,9 +100,13 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
flexDirection: "row",
alignSelf: "center",
justifyContent: "space-between",
top: offset,
top: offset + keyboard.keyboardHeight,
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
}}
onTouchEnd={() => {
setMsg(null);
clearTimeout(timer.current);
}}
>
<View
style={{

View File

@@ -47,7 +47,7 @@ export const Tags = ({ item, close }) => {
>
<Button
onPress={async () => {
ManageTagsSheet.present(item);
ManageTagsSheet.present([item]);
}}
buttonType={{
text: colors.accent

View File

@@ -36,6 +36,7 @@ import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import ManageTagsSheet from "../sheets/manage-tags";
export const SelectionHeader = React.memo(() => {
const colors = useThemeStore((state) => state.colors);
@@ -209,6 +210,20 @@ export const SelectionHeader = React.memo(() => {
screen === "Notebook" ||
screen === "Reminders" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.pri}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
//setSelectionMode(false);
@@ -224,6 +239,7 @@ export const SelectionHeader = React.memo(() => {
name="plus"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);

View File

@@ -42,6 +42,7 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { requestInAppReview } from "../../../services/app-review";
const ExportNotesSheet = ({ notes, update }) => {
const colors = useThemeStore((state) => state.colors);
@@ -72,6 +73,7 @@ const ExportNotesSheet = ({ notes, update }) => {
update({ disableClosing: false });
setComplete(true);
setExporting(false);
requestInAppReview();
};
const actions = [

View File

@@ -22,7 +22,7 @@ import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { presentSheet, ToastEvent } from "../../../services/event-manager";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useTagStore } from "../../../stores/use-tag-store";
import { useThemeStore } from "../../../stores/use-theme-store";
@@ -31,9 +31,10 @@ import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { IconButton } from "../../ui/icon-button";
const ManageTagsSheet = (props) => {
const colors = useThemeStore((state) => state.colors);
const [note, setNote] = useState(props.note);
const [notes, setNotes] = useState(props.notes || []);
const allTags = useTagStore((state) => state.tags);
const [tags, setTags] = useState([]);
const [query, setQuery] = useState(null);
@@ -42,7 +43,7 @@ const ManageTagsSheet = (props) => {
useEffect(() => {
sortTags();
}, [allTags, note, query, sortTags]);
}, [allTags, notes, query, sortTags]);
const sortTags = useCallback(() => {
let _tags = [...allTags];
@@ -51,13 +52,14 @@ const ManageTagsSheet = (props) => {
if (query) {
_tags = _tags.filter((t) => t.title.startsWith(query));
}
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
if (!note || !note.tags) {
if (!tagsMerged || !tagsMerged.length) {
setTags(_tags);
return;
}
let noteTags = [];
for (let tag of note.tags) {
for (let tag of tagsMerged) {
let index = _tags.findIndex((t) => t.title === tag);
if (index !== -1) {
noteTags.push(_tags[index]);
@@ -67,7 +69,7 @@ const ManageTagsSheet = (props) => {
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
let combinedTags = [...noteTags, ..._tags];
setTags(combinedTags);
}, [allTags, note, query]);
}, [allTags, notes, query]);
useEffect(() => {
useTagStore.getState().setTags();
@@ -85,15 +87,23 @@ const ManageTagsSheet = (props) => {
}
let tag = _query;
setNote({ ...note, tags: note.tags ? [...note.tags, tag] : [tag] });
setNotes(
notes.map((note) => ({
...note,
tags: note.tags ? [...note.tags, tag] : [tag]
}))
);
setQuery(null);
inputRef.current?.setNativeProps({
text: ""
});
try {
await db.notes.note(note.id).tag(tag);
for (let note of notes) {
await db.notes.note(note.id).tag(tag);
}
useTagStore.getState().setTags();
setNote(db.notes.note(note.id).data);
setNotes(notes.map((note) => db.notes.note(note.id).data));
} catch (e) {
ToastEvent.show({
heading: "Cannot add tag",
@@ -180,41 +190,52 @@ const ManageTagsSheet = (props) => {
) : null}
{tags.map((item) => (
<TagItem key={item.title} tag={item} note={note} setNote={setNote} />
<TagItem
key={item.title}
tag={item}
notes={notes}
setNotes={setNotes}
/>
))}
</ScrollView>
</View>
);
};
ManageTagsSheet.present = (note) => {
ManageTagsSheet.present = (notes) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} note={note} />;
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({ tag, note, setNote }) => {
const TagItem = ({ tag, notes, setNotes }) => {
const colors = useThemeStore((state) => state.colors);
const someNotesTagged = notes.some(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const allNotesTagged = notes.every(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const onPress = async () => {
let prevNote = { ...note };
try {
if (prevNote.tags.indexOf(tag.title) !== -1) {
await db.notes
.note(note.id)
.untag(prevNote.tags[prevNote.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
for (let note of notes) {
try {
if (someNotesTagged) {
await db.notes
.note(note.id)
.untag(note.tags[note.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
}
} catch (e) {
console.error(e);
}
useTagStore.getState().setTags();
setNote(db.notes.note(note.id).data);
} catch (e) {
console.error(e);
}
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
@@ -225,39 +246,36 @@ const TagItem = ({ tag, note, setNote }) => {
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
justifyContent: "flex-start",
height: 40
}}
onPress={onPress}
type={
note && note.tags.findIndex((t) => t === tag.title) !== -1
? "shade"
: "grayBg"
}
type="gray"
>
<Heading
size={SIZE.sm}
color={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? colors.accent
: colors.pri
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={someNotesTagged || allNotesTagged ? colors.accent : colors.icon}
testID={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
>
{"#" + tag.title}
</Heading>
<Icon
name={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? "minus"
: "plus"
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
color={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? colors.accent
: colors.accent
}
size={SIZE.lg}
/>
<Paragraph size={SIZE.sm}>{"#" + tag.title}</Paragraph>
</PressableButton>
);
};

View File

@@ -35,6 +35,7 @@ import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { requestInAppReview } from "../../../services/app-review";
const PublishNoteSheet = ({ note: item, update }) => {
const colors = useThemeStore((state) => state.colors);
@@ -66,6 +67,7 @@ const PublishNoteSheet = ({ note: item, update }) => {
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
requestInAppReview();
} catch (e) {
ToastEvent.show({
heading: "Could not publish note",

View File

@@ -43,7 +43,7 @@ import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
class RecoveryKeySheet extends React.Component {
constructor(props) {

View File

@@ -44,7 +44,7 @@ import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
const RestoreDataSheet = () => {
const [visible, setVisible] = useState(false);

View File

@@ -19,4 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [];
export const features: FeatureType[] = [
{
title: "Attachments preview",
body: "You can now preview PDFs & Images directly inside Notesnook."
},
{
title: "New attachments manager",
body: "The new attachments manager makes is much easier to view & interact with your attachments. It also allows you to download all (or some) of your attachments."
},
{
title: "Assign tags to multiple notes",
body: "You can now easily tag multiple notes without first opening them."
}
];

View File

@@ -520,7 +520,7 @@ export const useActions = ({ close = () => null, item }) => {
}
async function showAttachments() {
AttachmentDialog.present();
AttachmentDialog.present(item);
}
async function exportNote() {

View File

@@ -489,7 +489,8 @@ export const useAppEvents = () => {
if (
SettingsService.get().appLockMode === "background" &&
!useSettingStore.getState().requestBiometrics &&
!useUserStore.getState().verifyUser
!useUserStore.getState().verifyUser &&
!useUserStore.getState().shouldBlockVerifyUser
) {
useUserStore.getState().setVerifyUser(true);
if (Platform.OS === "ios") {

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState } from "react";
import { useEffect, useState } from "react";
import { useAttachmentStore } from "../stores/use-attachment-store";
type AttachmentProgress = {
@@ -41,22 +41,26 @@ export const useAttachmentProgress = (
: undefined
);
const attachmentProgress = progress?.[attachment.metadata.hash];
if (attachmentProgress) {
const type = attachmentProgress.type;
const loaded =
attachmentProgress.type === "download"
? attachmentProgress.recieved
: attachmentProgress.sent;
const value = loaded / attachmentProgress.total;
setCurrentProgress({
value: value * 100,
percent: (value * 100).toFixed(0) + "%",
type: type
});
} else {
setCurrentProgress(undefined);
}
useEffect(() => {
const attachmentProgress = progress?.[attachment?.metadata?.hash];
if (attachmentProgress) {
const type = attachmentProgress.type;
const loaded =
attachmentProgress.type === "download"
? attachmentProgress.recieved
: attachmentProgress.sent;
const value = loaded / attachmentProgress.total;
setCurrentProgress({
value: value * 100,
percent: (value * 100).toFixed(0) + "%",
type: type
});
} else {
setTimeout(() => {
setCurrentProgress(undefined);
}, 300);
}
}, [attachment, progress]);
return [currentProgress, setCurrentProgress];
};

View File

@@ -27,7 +27,8 @@
"validator": "^13.5.2",
"zustand": "^3.6.0",
"fflate": "^0.7.3",
"timeago.js": "4.0.2"
"timeago.js": "4.0.2",
"react-native-blob-util": "0.17.3"
},
"sideEffects": false
}

View File

@@ -128,7 +128,6 @@ const Editor = React.memo(
renderKey.current =
renderKey.current === `editor-0` ? `editor-1` : `editor-0`;
editor.setLoading(true);
setTimeout(() => editor.setLoading(false), 10);
}, [editor]);
useEffect(() => {
@@ -167,6 +166,7 @@ const Editor = React.memo(
injectedJavaScript={`globalThis.sessionId="${editor.sessionId}";`}
javaScriptEnabled={true}
focusable={true}
onContentProcessDidTerminate={onError}
setSupportMultipleWindows={false}
overScrollMode="never"
scrollEnabled={false}

View File

@@ -101,8 +101,9 @@ const EditorOverlay = ({ editorId = "", editor }) => {
setTimeout(() => {
if (!loadingState.current.startTime) {
translateValue.value = 6000;
opacity.value = 0;
}
}, 1000);
}, 3000);
eSubscribeEvent("loadingNote" + editorId, load);
return () => {
clearTimers();

View File

@@ -22,7 +22,7 @@ import React from "react";
import { Platform, View } from "react-native";
import DocumentPicker from "react-native-document-picker";
import { launchCamera, launchImageLibrary } from "react-native-image-picker";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { db } from "../../../common/database";
import { compressToBase64 } from "../../../common/filesystem/compress";
import { AttachmentItem } from "../../../components/attachments/attachment-item";
@@ -34,6 +34,7 @@ import {
import PremiumService from "../../../services/premium";
import { eCloseSheet } from "../../../utils/events";
import { editorController, editorState } from "./utils";
import { isImage } from "@notesnook/core/utils/filename";
const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
@@ -74,7 +75,7 @@ const file = async (fileOptions) => {
mode: "import",
allowMultiSelection: false
};
if (Platform.OS == "ios") {
if (Platform.OS === "ios") {
options.copyTo = "cachesDirectory";
}
await db.attachments.generateKey();
@@ -87,14 +88,9 @@ const file = async (fileOptions) => {
}
file = file[0];
if (file.type.startsWith("image")) {
ToastEvent.show({
title: "Type not supported",
message: "Please add images from gallery or camera picker.",
type: "error"
});
return;
}
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri;
if (file.size > FILE_SIZE_LIMIT) {
ToastEvent.show({
title: "File too large",
@@ -114,7 +110,6 @@ const file = async (fileOptions) => {
return;
}
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri;
console.log("file uri: ", uri);
uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
showEncryptionSheet(file);
@@ -125,12 +120,24 @@ const file = async (fileOptions) => {
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions)))
return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
editorController.current?.commands.insertAttachment({
hash: hash,
filename: file.name,
type: file.type,
size: file.size
});
if (isImage(file.type)) {
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
});
} else {
editorController.current?.commands.insertAttachment({
hash: hash,
filename: file.name,
mime: file.type,
size: file.size
});
}
setTimeout(() => {
eSendEvent(eCloseSheet);
}, 1000);
@@ -255,7 +262,7 @@ const handleImageResponse = async (response, options) => {
editorController.current?.commands.insertImage({
hash: hash,
type: image.type,
mime: image.type,
title: fileName,
dataurl: b64,
size: image.fileSize,

View File

@@ -349,7 +349,7 @@ export const useEditorEvents = (
});
return;
}
ManageTagsSheet.present(editor.note.current);
ManageTagsSheet.present([editor.note.current]);
break;
case EventTypes.tag:
if (editorMessage.value) {
@@ -398,9 +398,18 @@ export const useEditorEvents = (
openLinkInBrowser(editorMessage.value as string);
break;
case EventTypes.previewAttachment:
eSendEvent("ImagePreview", editorMessage.value);
case EventTypes.previewAttachment: {
const hash = (editorMessage.value as Attachment)?.hash;
const attachment = db.attachments?.attachment(hash);
if (attachment.metadata.type.startsWith("image/")) {
eSendEvent("ImagePreview", editorMessage.value);
} else {
eSendEvent("PDFPreview", attachment);
}
break;
}
default:
break;
}

View File

@@ -80,8 +80,8 @@ export const useEditor = (
const lockedSessionId = useRef<string>();
const postMessage = useCallback(
async <T>(type: string, data: T) =>
await post(editorRef, sessionIdRef.current, type, data),
async <T>(type: string, data: T, waitFor = 300) =>
await post(editorRef, sessionIdRef.current, type, data, waitFor),
[sessionIdRef]
);
@@ -120,9 +120,13 @@ export const useEditor = (
[editorId]
);
if (loading) {
setLoading(false);
}
useEffect(() => {
if (loading) {
setLoading(false);
} else {
state.current.ready = false;
}
}, [loading]);
const withTimer = useCallback(
(id: string, fn: () => void, duration: number) => {
@@ -336,7 +340,6 @@ export const useEditor = (
) => {
state.current.currentlyEditing = true;
const editorState = useEditorStore.getState();
if (item && item.type === "new") {
currentNote.current && (await reset());
const nextSessionId = makeSessionId(item as NoteType);
@@ -344,7 +347,7 @@ export const useEditor = (
sessionIdRef.current = nextSessionId;
sessionHistoryId.current = Date.now();
await commands.setSessionId(nextSessionId);
await commands.focus();
if (state.current?.ready) await commands.focus();
lastContentChangeTime.current = 0;
useEditorStore.getState().setReadonly(false);
} else {
@@ -358,7 +361,7 @@ export const useEditor = (
!currentContent.current?.data ||
currentContent.current?.data.length < 50000
) {
overlay(false);
if (state.current.ready) overlay(false);
} else {
overlay(true);
}
@@ -372,7 +375,11 @@ export const useEditor = (
currentNote.current = item as NoteType;
await commands.setStatus(timeConverter(item.dateEdited), "Saved");
await postMessage(EditorEvents.title, item.title);
await postMessage(EditorEvents.html, currentContent.current?.data);
await postMessage(
EditorEvents.html,
currentContent.current?.data,
10000
);
useEditorStore.getState().setReadonly(item.readonly);
await commands.setTags(currentNote.current);
commands.setSettings();
@@ -578,6 +585,10 @@ export const useEditor = (
lastContentChangeTime.current = Date.now();
};
useEffect(() => {
state.current.saveCount = 0;
}, [sessionId, loading]);
const onReady = useCallback(async () => {
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) {
eSendEvent("webview_reset");
@@ -586,30 +597,24 @@ export const useEditor = (
}
}, [isDefaultEditor, restoreEditorState]);
useEffect(() => {
state.current.saveCount = 0;
(async () => {
await commands.setSessionId(sessionIdRef.current);
if (sessionIdRef.current) {
if (!state.current?.ready) return;
await onReady();
}
})();
}, [sessionId, loading, commands, onReady]);
const onLoad = useCallback(async () => {
state.current.ready = true;
onReady();
postMessage(EditorEvents.theme, theme || useThemeStore.getState().colors);
commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
);
if (currentNote.current) {
loadNote({ ...currentNote.current, forced: true });
} else {
await commands.setPlaceholder(placeholderTip.current);
}
commands.setSettings();
if (currentNote.current) overlay(true);
clearTimeout(timers.current["editor:loaded"]);
timers.current["editor:loaded"] = setTimeout(async () => {
postMessage(EditorEvents.theme, theme || useThemeStore.getState().colors);
commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
);
await commands.setSessionId(sessionIdRef.current);
await onReady();
await commands.setSettings();
if (currentNote.current) {
loadNote({ ...currentNote.current, forced: true });
} else {
await commands.setPlaceholder(placeholderTip.current);
}
state.current.ready = true;
}, 300);
}, [
onReady,
postMessage,
@@ -617,7 +622,8 @@ export const useEditor = (
commands,
isDefaultEditor,
insets,
loadNote
loadNote,
overlay
]);
return {

View File

@@ -73,7 +73,8 @@ export async function post<T>(
ref: RefObject<WebView>,
sessionId: string,
type: string,
value: T | null = null
value: T | null = null,
waitFor = 300
) {
if (!sessionId) {
console.warn("post called without sessionId of type:", type);
@@ -85,7 +86,7 @@ export async function post<T>(
sessionId: sessionId
};
setImmediate(() => ref.current?.postMessage(JSON.stringify(message)));
const response = await getResponse(type);
const response = await getResponse(type, waitFor);
return response;
}
@@ -97,7 +98,8 @@ type WebviewResponseData = {
};
export const getResponse = async (
type: string
type: string,
waitFor = 300
): Promise<WebviewResponseData | false> => {
return new Promise((resolve) => {
const callback = (data: WebviewResponseData) => {
@@ -107,7 +109,7 @@ export const getResponse = async (
eSubscribeEvent(type, callback);
setTimeout(() => {
resolve(false);
}, 5000);
}, waitFor);
});
};

View File

@@ -35,7 +35,7 @@ import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { editorRef } from "../../utils/global-refs";
import { ProgressBar } from "./progress";
import { editorController, editorState, textInput } from "./tiptap/utils";
import { editorController, textInput } from "./tiptap/utils";
export const EditorWrapper = ({ width }) => {
const colors = useThemeStore((state) => state.colors);
const deviceMode = useSettingStore((state) => state.deviceMode);
@@ -48,7 +48,6 @@ export const EditorWrapper = ({ width }) => {
const keyboard = useKeyboard();
const onAppStateChanged = async (state) => {
if (editorState().movedAway) return;
if (state === "active") {
editorController.current.onReady();
editorController.current.overlay(false);

View File

@@ -28,7 +28,7 @@ import React, {
import { ActivityIndicator, Linking, Platform, View } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { db } from "../../common/database";
import Storage from "../../common/database/storage";
import DialogHeader from "../../components/dialog/dialog-header";

View File

@@ -39,6 +39,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store";
import { getElevation } from "../../utils";
import { SIZE } from "../../utils/size";
import { requestInAppReview } from "../../services/app-review";
const AppLock = ({ route }) => {
const colors = useThemeStore((state) => state.colors);
const appLockMode = useSettingStore((state) => state.settings.appLockMode);
@@ -151,6 +152,9 @@ const AppLock = ({ route }) => {
return;
}
SettingsService.set({ appLockMode: item.value });
if (!welcome) {
requestInAppReview();
}
}}
customStyle={{
justifyContent: "flex-start",

View File

@@ -18,12 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Clipboard from "@react-native-clipboard/clipboard";
import { LogMessage } from "@streetwriters/logger";
import { LogMessage } from "@notesnook/logger";
import { format, LogLevel, logManager } from "@notesnook/core/logger";
import React, { useEffect, useState } from "react";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import Storage from "../../common/database/storage";
import { presentDialog } from "../../components/dialog/functions";
import { IconButton } from "../../components/ui/icon-button";

View File

@@ -268,10 +268,10 @@ export const LICENSES = [
link: "https://github.com/RocketChat/rn-extensions-share"
},
{
name: "rn-fetch-blob",
name: "react-native-blob-util",
licenseType: "MIT",
author: "Joltup",
link: "https://github.com/joltup/rn-fetch-blob"
author: "RonRadtke",
link: "https://github.com/RonRadtke/react-native-blob-util"
},
{
name: "react-native-gzip",

View File

@@ -0,0 +1,47 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import InAppReview from "react-native-in-app-review";
import { DatabaseLogger } from "../common/database";
import { MMKV } from "../common/database/mmkv";
import Config from "react-native-config";
import { useUserStore } from "../stores/use-user-store";
const day_ms = 86400000;
export function requestInAppReview() {
if (Config.GITHUB_RELEASE === "true") return;
const time = MMKV.getMap<{ timestamp: number }>("requestInAppReview");
if (time?.timestamp && time?.timestamp + day_ms * 7 > Date.now()) {
return;
}
if (InAppReview.isAvailable()) {
useUserStore.getState().setShouldBlockVerifyUser(true);
InAppReview.RequestInAppReview()
.then(() => {})
.catch((error) => {
DatabaseLogger.error(error);
});
MMKV.setMap("requestInAppReview", { timestamp: Date.now() });
} else {
DatabaseLogger.error(new Error("In App Review not available"));
}
}

View File

@@ -21,7 +21,7 @@ import { Platform } from "react-native";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { presentDialog } from "../components/dialog/functions";
import { DatabaseLogger, db } from "../common/database";
import storage from "../common/database/storage";

View File

@@ -22,7 +22,7 @@ import { zipSync } from "fflate";
import { Platform } from "react-native";
import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "rn-fetch-blob";
import RNFetchBlob from "react-native-blob-util";
import { DatabaseLogger, db } from "../common/database/index";
import Storage from "../common/database/storage";
import { toTXT } from "../utils";

View File

@@ -45,6 +45,7 @@ import { sleep } from "../utils/time";
import { useRelationStore } from "../stores/use-relation-store";
import { useReminderStore } from "../stores/use-reminder-store";
import { presentDialog } from "../components/dialog/functions";
import NetInfo from "@react-native-community/netinfo";
export type Reminder = {
id: string;
@@ -193,7 +194,7 @@ const onEvent = async ({ type, detail }: Event) => {
case "Hide":
unpinQuickNote();
break;
case "ReplyInput":
case "ReplyInput": {
displayNotification({
title: "Quick note",
message: 'Tap on "Take note" to add a note.',
@@ -211,9 +212,17 @@ const onEvent = async ({ type, detail }: Event) => {
data: `<p>${input} </p>`
}
});
await db.sync(false, false);
const status = await NetInfo.fetch();
if (status.isInternetReachable) {
try {
await db.sync(false, false);
} catch (e) {
console.log(e, (e as Error).stack);
}
}
useNoteStore.getState().setNotes();
break;
}
}
}
};

View File

@@ -98,7 +98,7 @@ async function getProducts() {
}
function get() {
if (__DEV__ || Config.isTesting) return true;
if (__DEV__ || Config.isTesting === "true") return true;
return SUBSCRIPTION_STATUS.BASIC !== premiumStatus;
}
@@ -154,9 +154,11 @@ const onUserStatusCheck = async (type) => {
};
break;
case CHECK_IDS.notebookAdd:
setTimeout(() => {
eSendEvent(eOpenPremiumDialog);
}, 500);
message = {
context: "sheet",
title: "Get Notesnook Pro",
desc: "With Notesnook Pro you can create unlimited notebooks and do so much more! Get it now."
};
break;
case CHECK_IDS.vaultAdd:
message = {

View File

@@ -37,6 +37,8 @@ export interface UserStore extends State {
setLastSynced: (lastSynced: string) => void;
verifyUser: boolean;
setVerifyUser: (verified: boolean) => void;
shouldBlockVerifyUser: boolean;
setShouldBlockVerifyUser: (shouldBlockVerifyUser: boolean) => void;
}
export const useUserStore = create<UserStore>((set) => ({
@@ -52,5 +54,12 @@ export const useUserStore = create<UserStore>((set) => ({
},
setLastSynced: (lastSynced) => set({ lastSynced: lastSynced }),
setVerifyUser: (verified) => set({ verifyUser: verified }),
lastSyncStatus: SyncStatus.Never
lastSyncStatus: SyncStatus.Never,
shouldBlockVerifyUser: false,
setShouldBlockVerifyUser: (shouldBlockVerifyUser) => {
set({ shouldBlockVerifyUser });
setTimeout(() => {
set({ shouldBlockVerifyUser: false });
}, 1000);
}
}));

View File

@@ -164,7 +164,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 2045
versionCode 2049
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -276,6 +276,13 @@ android {
}
}
packagingOptions {
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->

View File

@@ -51,8 +51,7 @@
android:theme="@style/BootTheme"
android:largeHeap="true"
android:supportsRtl="false"
tools:replace="android:supportsRtl"
android:usesCleartextTraffic="true">
tools:replace="android:supportsRtl">
<receiver android:exported="false" android:name=".NoteWidget">
<intent-filter>

View File

@@ -1,6 +1,6 @@
- Sync notes created from share extension & notification quick notes without requiring to launch the app
- Fix formatting is lost when sharing notes to other apps
- Fix editor would be blank after entering the app from background
- Small bug fixes and ux improvements
- Preview PDFs & Images directly inside Notesnook."
- Improved attachments manager with support for downloading all attachments
- Assign tags to multiple notes
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -7,6 +7,15 @@ import Config from 'react-native-config';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import appJson from './app.json';
import Notifications from "../app/services/notifications";
import NetInfo from "@react-native-community/netinfo";
NetInfo.configure({
reachabilityUrl: "https://notesnook.com",
reachabilityTest: (response) => {
if (!response) return false;
return response?.status >= 200 && response?.status < 300;
}
});
Notifications.init();
const appName = appJson.name;

View File

@@ -993,7 +993,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2037;
CURRENT_PROJECT_VERSION = 2041;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1067,7 +1067,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.4.14;
MARKETING_VERSION = 2.5.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1097,7 +1097,7 @@
CODE_SIGN_ENTITLEMENTS = Notesnook/Notesnook.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2037;
CURRENT_PROJECT_VERSION = 2041;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
@@ -1170,7 +1170,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.4.14;
MARKETING_VERSION = 2.5.0;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1328,7 +1328,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2037;
CURRENT_PROJECT_VERSION = 2041;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1340,7 +1340,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.4.14;
MARKETING_VERSION = 2.5.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1370,7 +1370,7 @@
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2037;
CURRENT_PROJECT_VERSION = 2041;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1382,7 +1382,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.4.14;
MARKETING_VERSION = 2.5.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1411,7 +1411,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2037;
CURRENT_PROJECT_VERSION = 2041;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1485,7 +1485,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.4.14;
MARKETING_VERSION = 2.5.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1515,7 +1515,7 @@
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2037;
CURRENT_PROJECT_VERSION = 2041;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1589,7 +1589,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.4.14;
MARKETING_VERSION = 2.5.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -1,8 +1,11 @@
PODS:
- Base64 (1.1.2)
- BEMCheckBox (1.4.1)
- boost (1.76.0)
- callstack-repack (3.0.0):
- callstack-repack (3.2.0):
- JWTDecode (~> 3.0)
- React-Core
- SwiftyRSA
- DoubleConversion (1.1.6)
- FBLazyVector (0.69.7)
- FBReactNativeSpec (0.69.7):
@@ -15,6 +18,7 @@ PODS:
- fmt (6.2.1)
- glog (0.3.5)
- GZIP (1.3.0)
- JWTDecode (3.0.1)
- MMKV (1.2.13):
- MMKVCore (~> 1.2.13)
- MMKVCore (1.2.13)
@@ -234,9 +238,11 @@ PODS:
- React-Core
- react-native-begin-background-task (0.1.0):
- React
- react-native-config (1.4.11):
- react-native-config/App (= 1.4.11)
- react-native-config/App (1.4.11):
- react-native-blob-util (0.17.3):
- React-Core
- react-native-config (1.5.1):
- react-native-config/App (= 1.5.1)
- react-native-config/App (1.5.1):
- React-Core
- react-native-date-picker (4.2.6):
- React-Core
@@ -244,7 +250,7 @@ PODS:
- React-Core
- react-native-fingerprint-scanner (5.0.0):
- React-Core
- react-native-get-random-values (1.8.0):
- react-native-get-random-values (1.9.0):
- React-Core
- react-native-gzip (1.0.0):
- Base64
@@ -256,18 +262,22 @@ PODS:
- React-Core
- react-native-image-resizer (3.0.5):
- React-Core
- react-native-in-app-review (4.3.3):
- React-Core
- react-native-keep-awake (1.1.0):
- React-Core
- react-native-mmkv-storage (0.8.0):
- MMKV (= 1.2.13)
- React-Core
- react-native-netinfo (9.3.7):
- react-native-netinfo (9.3.10):
- React-Core
- react-native-notification-sounds (0.5.5):
- React
- react-native-orientation (3.1.3):
- React
- react-native-safe-area-context (4.4.1):
- react-native-pdf (6.6.2):
- React-Core
- react-native-safe-area-context (4.5.3):
- RCT-Folly
- RCTRequired
- RCTTypeSafety
@@ -275,7 +285,7 @@ PODS:
- ReactCommon/turbomodule/core
- react-native-sodium (1.3.0):
- React
- react-native-webview (11.23.1):
- react-native-webview (11.26.1):
- React-Core
- React-perflogger (0.69.7)
- React-RCTActionSheet (0.69.7):
@@ -345,15 +355,14 @@ PODS:
- React-perflogger (= 0.69.7)
- rn-extensions-share (2.4.0):
- React-Core
- rn-fetch-blob (0.12.0):
- RNBootSplash (4.7.1):
- React-Core
- RNBootSplash (4.3.2):
- RNCCheckbox (0.5.15):
- BEMCheckBox (~> 1.4)
- React-Core
- RNCCheckbox (0.5.12):
- RNCClipboard (1.11.2):
- React-Core
- RNCClipboard (1.11.1):
- React-Core
- RNCMaskedView (0.2.8):
- RNCMaskedView (0.2.9):
- React-Core
- RNDateTimePicker (6.6.0):
- React-Core
@@ -365,7 +374,7 @@ PODS:
- React-Core
- RNFlashList (1.4.0):
- React-Core
- RNGestureHandler (2.7.1):
- RNGestureHandler (2.10.1):
- React-Core
- RNIap (7.5.6):
- React-Core
@@ -405,21 +414,32 @@ PODS:
- React-RCTText
- ReactCommon/turbomodule/core
- Yoga
- RNScreens (3.18.2):
- RNScreens (3.20.0):
- React-Core
- React-RCTImage
- RNSecureRandom (1.0.1):
- React
- RNShare (7.9.1):
- React-Core
- RNSVG (12.4.4):
- RNSVG (12.5.1):
- React-Core
- RNTooltips (1.0.3):
- pop (~> 1.0)
- React
- SexyTooltip
- RNZipArchive (6.0.9):
- React-Core
- RNZipArchive/Core (= 6.0.9)
- SSZipArchive (~> 2.2)
- RNZipArchive/Core (6.0.9):
- React-Core
- SSZipArchive (~> 2.2)
- SexyTooltip (1.2.5):
- pop (~> 1.0)
- SSZipArchive (2.4.3)
- SwiftyRSA (1.7.0):
- SwiftyRSA/ObjC (= 1.7.0)
- SwiftyRSA/ObjC (1.7.0)
- toolbar-android (0.2.1):
- React
- Yoga (1.14.0)
@@ -451,6 +471,7 @@ DEPENDENCIES:
- react-native-actions-shortcuts (from `../../node_modules/react-native-actions-shortcuts`)
- react-native-background-actions (from `../../node_modules/react-native-background-actions`)
- react-native-begin-background-task (from `../../node_modules/react-native-begin-background-task`)
- react-native-blob-util (from `../../node_modules/react-native-blob-util`)
- react-native-config (from `../../node_modules/react-native-config`)
- react-native-date-picker (from `../../node_modules/react-native-date-picker`)
- react-native-document-picker (from `../../node_modules/react-native-document-picker`)
@@ -460,11 +481,13 @@ DEPENDENCIES:
- 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`)"
- react-native-mmkv-storage (from `../../node_modules/react-native-mmkv-storage`)
- "react-native-netinfo (from `../../node_modules/@react-native-community/netinfo`)"
- react-native-notification-sounds (from `../../node_modules/react-native-notification-sounds`)
- react-native-orientation (from `../../node_modules/react-native-orientation`)
- react-native-pdf (from `../../node_modules/react-native-pdf`)
- react-native-safe-area-context (from `../../node_modules/react-native-safe-area-context`)
- "react-native-sodium (from `../../node_modules/@ammarahmed/react-native-sodium`)"
- react-native-webview (from `../../node_modules/react-native-webview`)
@@ -481,7 +504,6 @@ DEPENDENCIES:
- React-runtimeexecutor (from `../../node_modules/react-native/ReactCommon/runtimeexecutor`)
- ReactCommon/turbomodule/core (from `../../node_modules/react-native/ReactCommon`)
- rn-extensions-share (from `../../node_modules/rn-extensions-share`)
- rn-fetch-blob (from `../../node_modules/rn-fetch-blob`)
- RNBootSplash (from `../../node_modules/react-native-bootsplash`)
- "RNCCheckbox (from `../../node_modules/@react-native-community/checkbox`)"
- "RNCClipboard (from `../../node_modules/@react-native-clipboard/clipboard`)"
@@ -502,6 +524,7 @@ DEPENDENCIES:
- RNShare (from `../../node_modules/react-native-share`)
- RNSVG (from `../../node_modules/react-native-svg`)
- RNTooltips (from `../../node_modules/react-native-tooltips`)
- RNZipArchive (from `../../node_modules/react-native-zip-archive`)
- SexyTooltip (from `https://github.com/ammarahm-ed/SexyTooltip.git`)
- "toolbar-android (from `../../node_modules/@react-native-community/toolbar-android`)"
- Yoga (from `../../node_modules/react-native/ReactCommon/yoga`)
@@ -509,11 +532,15 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- Base64
- BEMCheckBox
- fmt
- GZIP
- JWTDecode
- MMKV
- MMKVCore
- pop
- SSZipArchive
- SwiftyRSA
EXTERNAL SOURCES:
boost:
@@ -562,6 +589,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native-background-actions"
react-native-begin-background-task:
:path: "../../node_modules/react-native-begin-background-task"
react-native-blob-util:
:path: "../../node_modules/react-native-blob-util"
react-native-config:
:path: "../../node_modules/react-native-config"
react-native-date-picker:
@@ -580,6 +609,8 @@ EXTERNAL SOURCES:
: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:
:path: "../../node_modules/react-native-in-app-review"
react-native-keep-awake:
:path: "../../node_modules/@sayem314/react-native-keep-awake"
react-native-mmkv-storage:
@@ -590,6 +621,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native-notification-sounds"
react-native-orientation:
:path: "../../node_modules/react-native-orientation"
react-native-pdf:
:path: "../../node_modules/react-native-pdf"
react-native-safe-area-context:
:path: "../../node_modules/react-native-safe-area-context"
react-native-sodium:
@@ -622,8 +655,6 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native/ReactCommon"
rn-extensions-share:
:path: "../../node_modules/rn-extensions-share"
rn-fetch-blob:
:path: "../../node_modules/rn-fetch-blob"
RNBootSplash:
:path: "../../node_modules/react-native-bootsplash"
RNCCheckbox:
@@ -664,6 +695,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native-svg"
RNTooltips:
:path: "../../node_modules/react-native-tooltips"
RNZipArchive:
:path: "../../node_modules/react-native-zip-archive"
SexyTooltip:
:git: https://github.com/ammarahm-ed/SexyTooltip.git
toolbar-android:
@@ -678,14 +711,16 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
Base64: cecfb41a004124895a7bcee567a89bae5a89d49b
BEMCheckBox: 5ba6e37ade3d3657b36caecc35c8b75c6c2b1a4e
boost: a7c83b31436843459a1961bfd74b96033dc77234
callstack-repack: 9e5425dfffeda7ea87b71729c4097141c7d8ce1c
callstack-repack: 3e48a96824e0e0411ae1f48749a2ab103aa62a3a
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 6b7f5692909b4300d50e7359cdefbcd09dd30faa
FBReactNativeSpec: f53cf57758c70c6bfba5230b739cd1071e7a6824
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a
GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3
JWTDecode: 2eed97c2fa46ccaf3049a787004eedf0be474a87
MMKV: aac95d817a100479445633f2b3ed8961b4ac5043
MMKVCore: 3388952ded307e41b3ed8a05892736a236ed1b8e
pop: d582054913807fd11fd50bfe6a539d91c7e1a55a
@@ -706,23 +741,26 @@ SPEC CHECKSUMS:
react-native-actions-shortcuts: 5d9cf0c9c308333dfcc1e05c3f9afa8c428e2533
react-native-background-actions: 2c251c986f23347f9c1722f05fd296938f60edb1
react-native-begin-background-task: 3b889e07458afc5822a7277cf9cbc7cd077e39ee
react-native-config: bcafda5b4c51491ee1b0e1d0c4e3905bc7b56c1b
react-native-blob-util: 99f4d79189252f597fe0d810c57a3733b1b1dea6
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
react-native-date-picker: 93e43b3084cea595b4d68b1405d6d99849663bd6
react-native-document-picker: ec07866a30707f23660c0f3ae591d669d3e89096
react-native-fingerprint-scanner: be63e626b31fb951780a5fac5328b065a61a3d6e
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-gzip: 02f9968afa759e189f0414d41f8f4a951a86b4f1
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: acbee258db16483744910f0da3ace39eb9ab47fd
react-native-mmkv-storage: 8ba3c0216a6df283ece11205b442a3e435aec4e5
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
react-native-notification-sounds: da78c828fe1bcbb92d8b505d5261890ed315ff39
react-native-orientation: f1caf84d65f1a4fd4511a18f2b924e634ad7a628
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
react-native-safe-area-context: b8979f5eda6ed5903d4dbc885be3846ea3daa753
react-native-sodium: 1681828855ec18fa952f4557cd595bf048cf5c32
react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
React-perflogger: 8e832d4e21fdfa613033c76d58d7e617341e804b
React-RCTActionSheet: 9ca778182a9523991bff6381045885b6e808bb73
React-RCTAnimation: 9ced26ad20b96e532ac791a8ab92a7b1ce2266b8
@@ -736,28 +774,30 @@ SPEC CHECKSUMS:
React-runtimeexecutor: 65cd2782a57e1d59a68aa5d504edf94278578e41
ReactCommon: 1e783348b9aa73ae68236271df972ba898560a95
rn-extensions-share: 3f0ecce20dfbca1f0358deb4ebfb9ee121a6d92a
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNBootSplash: 5f346163977573d6b2aeba1b25df9d2245c0d73c
RNCCheckbox: ed1b4ca295475b41e7251ebae046360a703b6eb5
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
RNCMaskedView: bc0170f389056201c82a55e242e5d90070e18e5a
RNBootSplash: 3f3f7f82efe2addbfe7ddeda20877ff4d579cd81
RNCCheckbox: 43bcc6493611468af0e19f19f029dab3da8561c4
RNCClipboard: 3f0451a8100393908bea5c5c5b16f96d45f30bfc
RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6
RNDateTimePicker: 818672afa85519722533d017b832ed09539d9ddb
RNDeviceInfo: aad3c663b25752a52bf8fce93f2354001dd185aa
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
RNFlashList: 399bf6a0db68f594ad2c86aaff3ea39564f39f8a
RNGestureHandler: b7a872907ee289ada902127f2554fa1d2c076122
RNGestureHandler: 42ec7c28dd02d540ed6c9159c57a98ff016492dc
RNIap: d248609d1b8937e63bd904e865c318e9b1457eff
RNKeychain: 840f8e6f13be0576202aefcdffd26a4f54bfe7b5
RNNotifee: 2ae3c18196e6f307fa62ae5c8e5305dea03ff147
RNPrivacySnapshot: 8eaf571478a353f2e5184f5c803164f22428b023
RNReanimated: f1b109fb8341505ace9d7d2eedd150da1686716b
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
RNShare: a5dc3b9c53ddc73e155b8cd9a94c70c91913c43c
RNSVG: ecd661f380a07ba690c9c5929c475a44f432d674
RNSVG: d7d7bc8229af3842c9cfc3a723c815a52cdd1105
RNTooltips: 5424d4bf0b3d441104127943b1115cc7f0616b1f
RNZipArchive: 68a0c6db4b1c103f846f1559622050df254a3ade
SexyTooltip: 5c9b4dec52bfb317938cb0488efd9da3717bb6fd
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6
toolbar-android: 2a73856e98b750d7e71ce4644d3f41cc98211719
Yoga: 0b84a956f7393ef1f37f3bb213c516184e4a689d

View File

@@ -5,7 +5,7 @@
"main": "index.js",
"license": "GPL-3.0-or-later",
"dependencies": {
"@callstack/repack": "^3.0.0",
"@callstack/repack": "^3.2.0",
"@react-native-clipboard/clipboard": "^1.9.0",
"@react-native-community/checkbox": "^0.5.8",
"@react-native-community/netinfo": "^9.3.7",
@@ -20,7 +20,7 @@
"react-native": "0.69.7",
"react-native-background-actions": "^2.6.6",
"react-native-begin-background-task": "https://github.com/blockfirm/react-native-begin-background-task.git",
"react-native-bootsplash": "^4.1.4",
"react-native-bootsplash": "4.1.4",
"react-native-config": "^1.4.6",
"react-native-device-info": "^8.4.1",
"react-native-document-picker": "^7.1.1",
@@ -46,10 +46,8 @@
"@ammarahmed/react-native-sodium": "1.3.0",
"react-native-svg": "^12.3.0",
"react-native-tooltips": "^1.0.3",
"react-native-vector-icons": "^9.0.0",
"react-native-webview": "^11.14.1",
"rn-extensions-share": "^2.4.0",
"rn-fetch-blob": "^0.12.0",
"react-native-gzip":"1.0.0",
"@shopify/flash-list":"1.4.0",
"@ammarahmed/notifee-react-native": "7.4.4",
@@ -59,7 +57,12 @@
"react-native-notification-sounds": "0.5.5",
"@bam.tech/react-native-image-resizer": "3.0.5",
"react-native-navigation-bar-color": "2.0.2",
"react-native-actions-shortcuts": "^1.0.1"
"react-native-actions-shortcuts": "^1.0.1",
"react-native-in-app-review": "4.3.3",
"react-native-zip-archive": "6.0.9",
"react-native-vector-icons": "9.2.0",
"react-native-pdf": "6.6.2",
"react-native-blob-util": "0.17.3"
},
"devDependencies": {
"@babel/core": "^7.12.9",
@@ -98,6 +101,7 @@
"react-test-renderer": "18.0.0",
"terser-webpack-plugin": "^5.3.5",
"ts-jest": "^28.0.7",
"webpack": "^5.74.0"
"webpack": "^5.74.0",
"react-refresh": "0.14.0"
}
}

View File

@@ -22,6 +22,11 @@ if (isGithubRelease) {
android:null
}
}
config.dependencies["react-native-in-app-review"] = {
platforms: {
android: null
}
}
}
module.exports = config;

View File

@@ -1,6 +1,6 @@
- Sync notes created from share extension & notification quick notes in background
- Fix formatting is lost when sharing notes to other apps
- Fix editor would be blank after entering the app from background sometimes
- Small bug fixes and UX improvements
- Preview PDFs & Images directly inside Notesnook."
- Improved attachments manager with support for downloading all attachments
- Assign tags to multiple notes
- Bug fixes and performance improvements
Thank you for using Notesnook!

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "2.4.14",
"version": "2.5.0",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -25,13 +25,15 @@
"devDependencies": {
"patch-package": "^6.4.7",
"typescript": "^4.8.2",
"otplib": "12.0.1"
"otplib": "12.0.1",
"react-refresh": "0.14.0"
},
"dependencies": {
"react": "18.0.0",
"react-native": "0.69.7",
"@notesnook/core": "*",
"@notesnook/editor": "*",
"@notesnook/editor-mobile": "*"
"@notesnook/editor-mobile": "*",
"@notesnook/logger": "*"
}
}
}

View File

@@ -1,103 +0,0 @@
diff --git a/node_modules/rn-fetch-blob/android/build.gradle b/node_modules/rn-fetch-blob/android/build.gradle
index a4ca7a4..4fd3cfa 100644
--- a/node_modules/rn-fetch-blob/android/build.gradle
+++ b/node_modules/rn-fetch-blob/android/build.gradle
@@ -41,6 +41,7 @@ android {
dependencies {
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
- //compile 'com.squareup.okhttp3:okhttp:+'
+ implementation 'com.squareup.okhttp3:okhttp:3.4.1'
+
//{RNFetchBlob_PRE_0.28_DEPDENDENCY}
}
diff --git a/node_modules/rn-fetch-blob/react-native.config.js b/node_modules/rn-fetch-blob/react-native.config.js
deleted file mode 100644
index 03c61b6..0000000
--- a/node_modules/rn-fetch-blob/react-native.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = {
- dependency: {
- hooks: {
- prelink: 'node ./node_modules/rn-fetch-blob/scripts/prelink.js',
- },
- },
-};
diff --git a/node_modules/rn-fetch-blob/scripts/prelink.js b/node_modules/rn-fetch-blob/scripts/prelink.js
deleted file mode 100644
index e2c3ac4..0000000
--- a/node_modules/rn-fetch-blob/scripts/prelink.js
+++ /dev/null
@@ -1,71 +0,0 @@
-try {
- var fs = require('fs');
- var glob = require('glob');
- var addAndroidPermissions = process.env.RNFB_ANDROID_PERMISSIONS == 'true';
- var MANIFEST_PATH = glob.sync(process.cwd() + '/android/app/src/main/**/AndroidManifest.xml')[0];
- var PACKAGE_JSON = process.cwd() + '/package.json';
- var package = JSON.parse(fs.readFileSync(PACKAGE_JSON));
- var APP_NAME = package.name;
- var PACKAGE_GRADLE = process.cwd() + '/node_modules/rn-fetch-blob/android/build.gradle'
- var VERSION = checkVersion();
-
- console.log('RNFetchBlob detected app version => ' + VERSION);
-
- if(VERSION < 0.28) {
- console.log('You project version is '+ VERSION + ' which may not compatible to rn-fetch-blob 7.0+, please consider upgrade your application template to react-native 0.27+.')
- // add OkHttp3 dependency fo pre 0.28 project
- var main = fs.readFileSync(PACKAGE_GRADLE);
- console.log('adding OkHttp3 dependency to pre 0.28 project .. ')
- main = String(main).replace('//{RNFetchBlob_PRE_0.28_DEPDENDENCY}', "compile 'com.squareup.okhttp3:okhttp:3.4.1'");
- fs.writeFileSync(PACKAGE_GRADLE, main);
- console.log('adding OkHttp3 dependency to pre 0.28 project .. ok')
- }
-
- console.log('Add Android permissions => ' + (addAndroidPermissions == "true"))
-
- if(addAndroidPermissions) {
-
- // set file access permission for Android < 6.0
- fs.readFile(MANIFEST_PATH, function(err, data) {
-
- if(err)
- console.log('failed to locate AndroidManifest.xml file, you may have to add file access permission manually.');
- else {
-
- console.log('RNFetchBlob patching AndroidManifest.xml .. ');
- // append fs permission
- data = String(data).replace(
- '<uses-permission android:name="android.permission.INTERNET" />',
- '<uses-permission android:name="android.permission.INTERNET" />\n <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> '
- )
- // append DOWNLOAD_COMPLETE intent permission
- data = String(data).replace(
- '<category android:name="android.intent.category.LAUNCHER" />',
- '<category android:name="android.intent.category.LAUNCHER" />\n <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>'
- )
- fs.writeFileSync(MANIFEST_PATH, data);
- console.log('RNFetchBlob patching AndroidManifest.xml .. ok');
-
- }
-
- })
- }
- else {
- console.log(
- '\033[95mrn-fetch-blob \033[97mwill not automatically add Android permissions after \033[92m0.9.4 '+
- '\033[97mplease run the following command if you want to add default permissions :\n\n' +
- '\033[96m\tRNFB_ANDROID_PERMISSIONS=true react-native link \n')
- }
-
- function checkVersion() {
- console.log('RNFetchBlob checking app version ..');
- return parseFloat(/\d\.\d+(?=\.)/.exec(package.dependencies['react-native']));
- }
-
-} catch(err) {
- console.log(
- '\033[95mrn-fetch-blob\033[97m link \033[91mFAILED \033[97m\nCould not automatically link package :'+
- err.stack +
- 'please follow the instructions to manually link the library : ' +
- '\033[4mhttps://github.com/joltup/rn-fetch-blob/wiki/Manually-Link-Package\n')
-}

View File

@@ -48,6 +48,7 @@ import { eOnLoadNote } from "../app/utils/events";
import { Editor } from "./editor";
import { Search } from "./search";
import { initDatabase, useShareStore } from "./store";
import NetInfo from "@react-native-community/netinfo";
const getLinkPreview = (url) => {
return getPreviewData(url, 5000);
};
@@ -355,7 +356,14 @@ const ShareView = ({ quicknote = false }) => {
}
}
}
await db.sync(false, false);
const status = await NetInfo.fetch();
if (status.isInternetReachable) {
try {
await db.sync(false, false);
} catch (e) {
console.log(e, e.stack);
}
}
await Storage.write("notesAddedFromIntent", "added");
close();
setLoading(false);

View File

@@ -256,6 +256,23 @@ test("select notes using Shift+Click upwards", async ({ page }, info) => {
expect(await notesList[0].isFocused()).toBeTruthy();
});
test("using Shift+Click when no notes are selected should not crash the app", async ({
page
}, info) => {
info.setTimeout(60 * 1000);
const { notes } = await populateList(page, 5);
await page.reload();
const note = await notes.findNote({ title: "Test note 3" });
await page.keyboard.down("Shift");
await note?.click();
await page.keyboard.up("Shift");
expect(await notes.isEmpty()).toBeFalsy();
});
test("Ctrl+Click to select/unselect notes", async ({ page }, info) => {
info.setTimeout(60 * 1000);
const { notesList, notes } = await populateList(page, 10);

View File

@@ -0,0 +1,46 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./bootstrap";
import { test } from "vitest";
import { Base64DecoderStream } from "../src/utils/streams/base64-decoder-stream";
import { consumeReadableStream } from "../src/utils/stream";
import { createReadStream, readFileSync } from "fs";
import { Readable } from "stream";
import path from "path";
test("streamed base64 decoder should output same as non-streamed", async (t) => {
const expected = readFileSync(
path.join(__dirname, "..", "__e2e__", "data", "importer-data.zip"),
"base64"
);
const fileStream = Readable.toWeb(
createReadStream(
path.join(__dirname, "..", "__e2e__", "data", "importer-data.zip")
)
) as ReadableStream<Uint8Array>;
t.expect(
(
await consumeReadableStream(
fileStream.pipeThrough(new Base64DecoderStream("base64"))
)
).join("")
).toBe(expected);
});

View File

@@ -0,0 +1,34 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
TransformStream,
ReadableStream,
WritableStream
} from "node:stream/web";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
globalThis.TransformStream = TransformStream;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
globalThis.ReadableStream = ReadableStream;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
globalThis.WritableStream = WritableStream;

View File

@@ -0,0 +1,45 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./bootstrap";
import { test } from "vitest";
import { ChunkedStream } from "../src/utils/streams/chunked-stream";
import { Readable } from "stream";
import { createReadStream } from "fs";
import { consumeReadableStream } from "../src/utils/stream";
import { xxhash64 } from "hash-wasm";
import path from "path";
const CHUNK_SIZE = 512 * 1024;
test("chunked stream should create equal sized chunks", async (t) => {
const chunks = await consumeReadableStream(
(
Readable.toWeb(
createReadStream(
path.join(__dirname, "..", "__e2e__", "data", "importer-data.zip")
)
) as ReadableStream<Uint8Array>
).pipeThrough(new ChunkedStream(CHUNK_SIZE))
);
t.expect(await Promise.all(chunks.map((a) => xxhash64(a)))).toMatchObject([
"6234b76401d9eb97",
"338834da3f6500b2"
]);
});

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "2.4.9",
"version": "2.5.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "2.4.9",
"version": "2.5.0",
"dependencies": {
"diary": "^0.3.1",
"electron-updater": "^5.3.0",

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.4.9",
"version": "2.5.0",
"appAppleId": "1544027013",
"private": true,
"main": "./build/electron.js",

View File

@@ -23,6 +23,7 @@ import { createReadStream } from "fs";
import { extname, normalize } from "path";
import { logger } from "./logger";
import { URL } from "url";
import { Blob } from "buffer";
const FILE_NOT_FOUND = -6;
const BASE_PATH = isDevelopment() ? "../public" : "";
@@ -67,7 +68,31 @@ function registerProtocol() {
mimeType: extensionToMimeType[fileExtension]
});
} else {
protocol.uninterceptProtocol(PROTOCOL);
var response;
try {
const body = await getBody(request);
response = await fetch(request.url, {
...request,
body,
headers: {
...request.headers
// origin: `${PROTOCOL}://${HOSTNAME}/`
},
referrer: request.referrer,
redirect: "manual"
});
} catch (e) {
console.error(e);
logger.error(`Error sending request to `, request.url, "Error: ", e);
callback({ statusCode: 400 });
return;
}
callback({
statusCode: response.status,
data: response.body,
headers: Object.fromEntries(response.headers.entries()),
mimeType: response.headers.get("Content-Type")
});
}
}
);
@@ -85,5 +110,29 @@ function shouldInterceptRequest(url) {
return shouldIntercept && !bypassedRoutes.includes(url.pathname);
}
/**
*
* @param {Electron.ProtocolRequest} request
*/
async function getBody(request) {
/**
* @type {Electron.Session}
*/
const session = globalThis?.window?.webContents?.session;
const blobParts = [];
if (!request.uploadData || !request.uploadData.length) return null;
for (let data of request.uploadData) {
if (data.type === "rawData") {
blobParts.push(new Uint8Array(data.bytes));
} else if (session && data.type === "blob") {
const buffer = await session.getBlobData(data.blobUUID);
blobParts.push(new Uint8Array(buffer));
}
}
const blob = new Blob(blobParts);
return await blob.arrayBuffer();
}
const PROTOCOL_URL = `${PROTOCOL}://${HOSTNAME}/`;
export { registerProtocol, PROTOCOL_URL };

19331
apps/web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "2.4.9",
"version": "2.5.0",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -27,12 +27,14 @@
"@notesnook/streamable-fs": "*",
"@notesnook/theme": "*",
"@notesnook/web-clipper": "*",
"@react-pdf-viewer/core": "^3.12.0",
"@react-pdf-viewer/toolbar": "^3.12.0",
"@tanstack/react-virtual": "^3.0.0-beta.18",
"@theme-ui/components": "^0.14.7",
"@theme-ui/core": "^0.14.7",
"allotment": "^1.12.1",
"async-mutex": "^0.3.2",
"axios": "^0.21.4",
"axios": "^1.3.4",
"clipboard-polyfill": "^3.0.3",
"comlink": "^4.3.1",
"cronosjs": "^1.7.1",
@@ -48,6 +50,7 @@
"localforage-driver-memory": "^1.0.5",
"mac-scrollbar": "^0.10.3",
"marked": "^4.1.0",
"pdfjs-dist": "^3.6.172",
"phone": "^3.1.14",
"platform": "^1.3.6",
"print-js": "^1.6.0",
@@ -85,6 +88,7 @@
"env-cmd": "^10.1.0",
"file-loader": "^6.2.0",
"find-process": "^1.4.4",
"happy-dom": "^8.9.0",
"ip": "^1.1.8",
"lorem-ipsum": "^2.0.4",
"otplib": "^12.0.1",
@@ -94,6 +98,7 @@
"react-dom": "17.0.2",
"source-map-explorer": "^2.5.2",
"typescript": "^4.8.2",
"vitest": "^0.29.8",
"webpack-bundle-analyzer": "^4.8.0",
"worker-loader": "^3.0.8"
},

22
apps/web/public/pdf.worker.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,154 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-restricted-globals */
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
const map = new Map();
// This should be called once per download
// Each event has a dataChannel that the data will be piped through
self.onmessage = (event) => {
// We send a heartbeat every x second to keep the
// service worker alive if a transferable stream is not sent
if (event.data === "ping") {
return;
}
const data = event.data;
const downloadUrl =
data.url ||
self.registration.scope +
Math.random() +
"/" +
(typeof data === "string" ? data : data.filename);
const port = event.ports[0];
const metadata = new Array(3); // [stream, data, port]
metadata[1] = data;
metadata[2] = port;
if (event.data.transferringReadable) {
port.onmessage = (evt) => {
port.onmessage = null;
metadata[0] = evt.data.readableStream;
};
} else {
metadata[0] = createStream(port);
}
map.set(downloadUrl, metadata);
port.postMessage({ download: downloadUrl });
};
function createStream(port) {
// ReadableStream is only supported by chrome 52
return new ReadableStream({
start(controller) {
// When we receive data on the messageChannel, we write
port.onmessage = ({ data }) => {
if (data === "end") {
return controller.close();
}
if (data === "abort") {
controller.error("Aborted the download");
return;
}
controller.enqueue(data);
};
},
cancel(reason) {
console.log("user aborted", reason);
port.postMessage({ abort: true });
}
});
}
self.onfetch = (event) => {
const url = event.request.url;
// this only works for Firefox
if (url.endsWith("/ping")) {
return event.respondWith(new Response("pong"));
}
const metadata = map.get(url);
if (!metadata) return null;
const [stream, data, port] = metadata;
map.delete(url);
// Not comfortable letting any user control all headers
// so we only copy over the length & disposition
const responseHeaders = new Headers({
"Content-Type": "application/octet-stream; charset=utf-8",
// To be on the safe side, The link can be opened in a iframe.
// but octet-stream should stop it.
"Content-Security-Policy": "default-src 'none'",
"X-Content-Security-Policy": "default-src 'none'",
"X-WebKit-CSP": "default-src 'none'",
"X-XSS-Protection": "1; mode=block"
});
let headers = new Headers(data.headers || {});
if (headers.has("Content-Length")) {
responseHeaders.set("Content-Length", headers.get("Content-Length"));
}
if (headers.has("Content-Disposition")) {
responseHeaders.set(
"Content-Disposition",
headers.get("Content-Disposition")
);
}
// data, data.filename and size should not be used anymore
if (data.size) {
console.warn("Depricated");
responseHeaders.set("Content-Length", data.size);
}
let fileName = typeof data === "string" ? data : data.filename;
if (fileName) {
console.warn("Depricated");
// Make filename RFC5987 compatible
fileName = encodeURIComponent(fileName)
.replace(/['()]/g, escape)
.replace(/\*/g, "%2A");
responseHeaders.set(
"Content-Disposition",
"attachment; filename*=UTF-8''" + fileName
);
}
event.respondWith(new Response(stream, { headers: responseHeaders }));
port.postMessage({ debug: "Download started" });
};

View File

@@ -45,11 +45,8 @@ import { isTesting } from "./utils/platform";
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
import { showToast } from "./utils/toast";
import { interruptedOnboarding } from "./components/dialogs/onboarding-dialog";
import { WebExtensionRelay } from "./utils/web-extension-relay";
import { hashNavigate } from "./navigation";
const relay = new WebExtensionRelay();
export default function AppEffects({ setShow }) {
const refreshNavItems = useStore((store) => store.refreshNavItems);
const updateLastSynced = useStore((store) => store.updateLastSynced);
@@ -103,7 +100,6 @@ export default function AppEffects({ setShow }) {
await showOnboardingDialog(interruptedOnboarding());
await showFeatureDialog("highlights");
await scheduleBackups();
relay.connect();
})();
return () => {

View File

@@ -1,3 +1,10 @@
:root {
--focus-border: var(--primary);
--separator-border: var(--border);
--sash-size: 10px;
--sash-hover-size: 4px;
}
/* open-sans-regular - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
@@ -95,6 +102,8 @@
format("truetype");
}
.rpv-core__text-layer,
.rpv-core__text-layer *,
.selectable,
.selectable *,
input,
@@ -120,6 +129,17 @@ textarea,
-webkit-tap-highlight-color: transparent;
}
.rpv-core__text-layer-text::selection {
background-color: var(--dimPrimary) !important;
color: transparent;
}
.rpv-core__text-layer-text::-moz-selection {
/* Code for Firefox */
background-color: var(--dimPrimary) !important;
color: transparent;
}
*::-moz-focus-inner {
border: 0;
}
@@ -165,13 +185,6 @@ textarea,
width: 1px !important;
}
:root {
--focus-border: var(--primary);
--separator-border: var(--border);
--sash-size: 10px;
--sash-hover-size: 4px;
}
.route#settings,
#mainRouteContainer {
overflow: hidden;

View File

@@ -25,7 +25,6 @@ import useTablet from "./hooks/use-tablet";
import { LazyMotion, domAnimation } from "framer-motion";
import useDatabase from "./hooks/use-database";
import { Allotment, LayoutPriority } from "allotment";
import "allotment/dist/style.css";
import Config from "./utils/config";
import { useStore } from "./stores/app-store";
import { Toaster } from "react-hot-toast";
@@ -35,6 +34,9 @@ import StatusBar from "./components/status-bar";
import { EditorLoader } from "./components/loaders/editor-loader";
import { FlexScrollContainer } from "./components/scroll-container";
import CachedRouter from "./components/cached-router";
import { WebExtensionRelay } from "./utils/web-extension-relay";
new WebExtensionRelay();
const GlobalMenuWrapper = React.lazy(() =>
import("./components/global-menu-wrapper")

View File

@@ -20,8 +20,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import FS from "../interfaces/fs";
import { db } from "./db";
export async function downloadAttachment(hash) {
const attachment = db.attachments.attachment(hash);
async function download(hash: string) {
const attachment = db.attachments?.attachment(hash);
if (!attachment) return;
const downloadResult = await db.fs.downloadFile(
attachment.metadata.hash,
@@ -31,9 +31,17 @@ export async function downloadAttachment(hash) {
);
if (!downloadResult) throw new Error("Failed to download file.");
const key = await db.attachments.decryptKey(attachment.key);
const key = await db.attachments?.decryptKey(attachment.key);
if (!key) throw new Error("Invalid key for attachment.");
return { key, attachment };
}
export async function saveAttachment(hash: string) {
const response = await download(hash);
if (!response) return;
const { attachment, key } = response;
await FS.saveFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
@@ -43,23 +51,51 @@ export async function downloadAttachment(hash) {
});
}
export async function checkAttachment(hash) {
const attachment = db.attachments.attachment(hash);
type OutputTypeToReturnType = {
blob: Blob;
base64: string;
text: string;
};
export async function downloadAttachment<
TType extends "blob" | "base64" | "text",
TOutputType = OutputTypeToReturnType[TType]
>(hash: string, type: TType): Promise<TOutputType | undefined> {
const response = await download(hash);
if (!response) return;
const { attachment, key } = response;
if (type === "base64" || type === "text")
return (await db.attachments?.read(hash, type)) as TOutputType;
const blob = await FS.decryptFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
});
if (!blob) return;
return blob as TOutputType;
}
export async function checkAttachment(hash: string) {
const attachment = db.attachments?.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {
const size = await FS.getUploadedFileSize(hash);
if (size <= 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e.message };
return { failed: e instanceof Error ? e.message : "Unknown error." };
}
return { success: true };
}
const ABYTES = 17;
export function getTotalSize(attachments) {
export function getTotalSize(attachments: any[]) {
let size = 0;
for (let attachment of attachments) {
for (const attachment of attachments) {
size += attachment.length + ABYTES;
}
return size;

View File

@@ -88,6 +88,12 @@ export function closeOpenedDialog() {
dialogs.forEach((elem) => elem.remove());
}
export function showAddTagsDialog(noteIds: string[]) {
return showDialog("AddTagsDialog", (Dialog, perform) => (
<Dialog onClose={(res) => perform(res)} noteIds={noteIds} />
));
}
export function showAddNotebookDialog() {
return showDialog("AddNotebookDialog", (Dialog, perform) => (
<Dialog
@@ -211,8 +217,9 @@ export function showError(title: string, message: string) {
export function showMultiDeleteConfirmation(length: number) {
return confirm({
title: `Delete ${length} items?`,
message: `These items will be **kept in your Trash for ${db.settings?.getTrashCleanupInterval() || 7
} days** after which they will be permanently deleted.`,
message: `These items will be **kept in your Trash for ${
db.settings?.getTrashCleanupInterval() || 7
} days** after which they will be permanently deleted.`,
positiveButtonText: "Yes",
negativeButtonText: "No"
});
@@ -331,6 +338,12 @@ export function showMoveNoteDialog(noteIds: string[]) {
));
}
export function showBillingHistoryDialog() {
return showDialog("BillingHistoryDialog", (Dialog, perform) => (
<Dialog onClose={(res: boolean) => perform(res)} />
));
}
function getDialogData(type: string) {
switch (type) {
case "create_vault":

View File

@@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/db";
import { showPasswordDialog } from "../common/dialog-controller";
import { db } from "./db";
import { showPasswordDialog } from "./dialog-controller";
import { showToast } from "../utils/toast";
class Vault {

View File

@@ -1,250 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Box, Flex, Text } from "@theme-ui/components";
import { formatBytes } from "../../utils/filename";
import ListItem from "../list-item";
import * as Icon from "../icons";
import { downloadAttachment } from "../../common/attachments";
import { reuploadAttachment } from "../editor/picker";
import { store } from "../../stores/attachment-store";
import { db } from "../../common/db";
import { Multiselect } from "../../common/multi-select";
import {
closeOpenedDialog,
showAttachmentsDialog,
showPromptDialog
} from "../../common/dialog-controller";
import { hashNavigate } from "../../navigation";
import { showToast } from "../../utils/toast";
const workStatusMap = {
recheck: "Rechecking...",
delete: "Deleting..."
};
function Attachment({ item, isCompact, index }) {
const attachment = item;
const status = attachment.status;
return (
<ListItem
selectable
isCompact={isCompact}
item={{ ...attachment, title: attachment.metadata.filename }}
title={attachment.metadata.filename}
body={
attachment.failed && <Text variant={"error"}>{attachment.failed}</Text>
}
menu={{
items: menuItems,
extraData: { attachment }
}}
onClick={() => {
if (isCompact) showAttachmentsDialog();
}}
footer={
isCompact ? (
<Flex
sx={{
fontSize: "subBody",
color: "fontTertiary",
alignItems: "center"
}}
>
{status ? (
<Text variant="subBody" title={`${status.type}ing`}>
{formatBytes(status.loaded, 1)}/{formatBytes(status.total, 1)}
</Text>
) : (
<Text mr={1}>{formatBytes(attachment.length)}</Text>
)}
{attachment.failed && (
<Icon.AttachmentError
sx={{ flexShrink: 0 }}
color={"error"}
size={13}
title={attachment.failed}
/>
)}
{attachment.isDeleting ? (
<Icon.Loading
sx={{ flexShrink: 0 }}
size={13}
title={"Deleting.."}
/>
) : attachment.dateUploaded ? (
<Icon.DoubleCheckmark
sx={{ flexShrink: 0 }}
color={"primary"}
size={13}
title={"Uploaded"}
/>
) : (
<Icon.Checkmark
sx={{ flexShrink: 0 }}
color={"icon"}
size={13}
title={"Waiting for upload"}
/>
)}
</Flex>
) : attachment.working ? (
<Flex>
<Icon.Loading size={13} />
<Text variant={"subBody"} ml={1}>
{workStatusMap[attachment.working]}
</Text>
</Flex>
) : (
<Flex sx={{ flexDirection: "column" }}>
{status && (
<Flex sx={{ flexDirection: "column" }}>
<Text variant="subBody">
{formatBytes(status.loaded, 1)}/{formatBytes(status.total, 1)}{" "}
({status.type}ing)
</Text>
<Box
sx={{
my: 1,
bg: "primary",
height: "2px",
width: `${status.progress}%`
}}
/>
</Flex>
)}
<Flex
sx={{
fontSize: "subBody",
color: "fontTertiary",
alignItems: "center"
}}
>
<Text mr={1}>{formatBytes(attachment.length)}</Text>
<Text mr={1}>{attachment.metadata.type}</Text>
{attachment.noteIds && (
<Text mr={1}>{attachment.noteIds.length} notes</Text>
)}
{attachment.metadata.hash && (
<Text className="selectable" mr={0}>
{attachment.metadata.hash}
</Text>
)}
</Flex>
</Flex>
)
}
index={index}
/>
);
}
export default Attachment;
const menuItems = [
{
key: "notes",
title: "Notes",
icon: Icon.References,
items: ({ attachment }) =>
attachment.noteIds.reduce((prev, curr) => {
const note = db.notes.note(curr);
if (!note)
prev.push({
key: curr,
title: `Note with id ${curr}`,
onClick: () => showToast("error", "This note does not exist.")
});
else
prev.push({
key: note.id,
title: note.title,
onClick: () => {
hashNavigate(`/notes/${curr}/edit`);
closeOpenedDialog();
}
});
return prev;
}, [])
},
{
key: "recheck",
title: () => "Recheck",
icon: Icon.DoubleCheckmark,
disabled: ({ attachment }) =>
!attachment.dateUploaded ? "This attachment is not uploaded yet." : false,
onClick: async ({ items }) => {
await store.recheck(items.map((i) => i.metadata.hash));
},
multiSelect: true
},
{
key: "rename",
title: () => "Rename",
icon: Icon.Rename,
onClick: async ({ attachment }) => {
const newName = await showPromptDialog({
title: "Rename attachment",
description: attachment.metadata.filename,
defaultValue: attachment.metadata.filename
});
if (!newName) return;
await store.rename(attachment.metadata.hash, newName);
}
},
{
key: "download",
title: ({ attachment }) =>
attachment.status?.type === "download" ? "Cancel download" : "Download",
icon: Icon.Download,
disabled: ({ attachment }) =>
!attachment.dateUploaded ? "This attachment is not uploaded yet." : false,
onClick: async ({ attachment }) => {
const isDownloading = attachment.status?.type === "download";
if (isDownloading) {
await db.fs.cancel(attachment.metadata.hash, "download");
} else await downloadAttachment(attachment.metadata.hash);
}
},
{
key: "reupload",
title: ({ attachment }) =>
attachment.status?.type === "upload" ? "Cancel upload" : "Reupload",
icon: Icon.Reupload,
onClick: async ({ attachment }) => {
const isDownloading = attachment.status?.type === "upload";
if (isDownloading) {
await db.fs.cancel(attachment.metadata.hash, "upload");
} else
await reuploadAttachment(
attachment.metadata.type,
attachment.metadata.hash
);
}
},
{
key: "permanent-delete",
color: "error",
iconColor: "error",
title: () => "Delete permanently",
icon: Icon.DeleteForver,
onClick: ({ items }) => Multiselect.deleteAttachments(items),
multiSelect: true
}
];

View File

@@ -0,0 +1,333 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Box, Checkbox, Flex, Label, Text } from "@theme-ui/components";
import { formatBytes } from "../../utils/filename";
import {
AttachmentError,
Checkmark,
DeleteForver,
DoubleCheckmark,
Download,
FileDocument,
FileGeneral,
FileImage,
FilePDF,
FileVideo,
FileWebClip,
Icon,
Loading,
References,
Rename,
Reupload,
Uploading
} from "../icons";
import { formatDate } from "@notesnook/core/utils/date";
import { showToast } from "../../utils/toast";
import { hashNavigate } from "../../navigation";
import {
closeOpenedDialog,
showPromptDialog
} from "../../common/dialog-controller";
import { store } from "../../stores/attachment-store";
import { db } from "../../common/db";
import { saveAttachment } from "../../common/attachments";
import { reuploadAttachment } from "../editor/picker";
import { Multiselect } from "../../common/multi-select";
import { Menu } from "../../hooks/use-menu";
import {
DocumentMimeTypes,
WebClipMimeType,
PDFMimeType
} from "@notesnook/core/utils/filename";
import { useEffect, useState } from "react";
import { AppEventManager, AppEvents } from "../../common/app-events";
const FILE_ICONS: Record<string, Icon> = {
"image/": FileImage,
"video/": FileVideo,
[WebClipMimeType]: FileWebClip
};
for (const mimeType of DocumentMimeTypes) {
FILE_ICONS[mimeType] = mimeType === PDFMimeType ? FilePDF : FileDocument;
}
function getFileIcon(type: string) {
for (const mime in FILE_ICONS) {
if (type.startsWith(mime))
return FILE_ICONS[mime as keyof typeof FILE_ICONS];
}
return FileGeneral;
}
type AttachmentProgressStatus = {
type: "download" | "upload";
loaded: number;
total: number;
};
type AttachmentProps = {
attachment: any;
isSelected?: boolean;
onSelected?: () => void;
compact?: boolean;
};
export function Attachment({
attachment,
isSelected,
onSelected,
compact
}: AttachmentProps) {
const [status, setStatus] = useState<AttachmentProgressStatus>();
useEffect(() => {
const event = AppEventManager.subscribe(
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
(progress: any) => {
if (progress.hash === attachment.metadata.hash) {
const percent = Math.round((progress.loaded / progress.total) * 100);
setStatus(
percent < 100
? {
type: progress.type,
loaded: progress.loaded,
total: progress.total
}
: undefined
);
}
}
);
return () => {
event.unsubscribe();
};
}, [attachment.metadata.hash]);
const Icon = getFileIcon(attachment.metadata.type);
return (
<Box
as="tr"
sx={{ height: 30, ":hover": { bg: "hover" } }}
onContextMenu={(e) => {
e.preventDefault();
Menu.openMenu(AttachmentMenuItems, {
attachment,
status
});
}}
onClick={onSelected}
>
{!compact && (
<td>
<Label>
<Checkbox
sx={{ width: 18, height: 18 }}
checked={isSelected}
onChange={onSelected}
/>
</Label>
</td>
)}
<td>
<Flex
sx={{
alignItems: "center",
maxWidth: compact ? 180 : "95%"
}}
>
{status ? (
status.type === "download" ? (
<Download size={16} color="primary" />
) : (
<Uploading size={16} color="primary" />
)
) : attachment.failed ? (
<AttachmentError
color={"error"}
size={16}
title={attachment.failed}
/>
) : attachment.working ? (
<Loading size={16} />
) : (
<Icon size={16} />
)}
<Text
variant="body"
sx={{
ml: 1,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>
{attachment.metadata.filename}
</Text>
</Flex>
</td>
<Text as="td" variant="body">
{attachment.isDeleting ? (
<Loading sx={{ flexShrink: 0 }} size={16} title={"Deleting.."} />
) : attachment.dateUploaded ? (
<DoubleCheckmark
sx={{ flexShrink: 0 }}
color={"primary"}
size={16}
title={"Uploaded"}
/>
) : (
<Checkmark
sx={{ flexShrink: 0 }}
color={"icon"}
size={16}
title={"Waiting for upload"}
/>
)}
</Text>
<Text as="td" variant="body" sx={{ color: status ? "primary" : "text" }}>
{status ? (
<>
{formatBytes(status.loaded, 1)}/{formatBytes(status.total, 1)}
</>
) : (
formatBytes(attachment.length, compact ? 1 : 2)
)}
</Text>
{!compact && (
<Text as="td" variant="body">
{attachment.dateUploaded
? formatDate(attachment.dateUploaded, {
dateStyle: "short",
timeStyle: "short"
})
: "-"}
</Text>
)}
</Box>
);
}
type MenuActionParams = {
attachment: any;
status: AttachmentProgressStatus;
};
type MenuItemValue<T> = T | ((options: MenuActionParams) => T);
type MenuItem = {
type?: "separator";
key: string;
title?: MenuItemValue<string>;
icon?: MenuItemValue<Icon>;
onClick?: (options: MenuActionParams) => void;
disabled?: MenuItemValue<boolean | string>;
color?: MenuItemValue<string>;
iconColor?: MenuItemValue<string>;
multiSelect?: boolean;
items?: MenuItemValue<MenuItem[]>;
};
const AttachmentMenuItems: MenuItem[] = [
{
key: "notes",
title: "Notes",
icon: References,
items: ({ attachment }) =>
(attachment.noteIds as string[]).reduce((prev, curr) => {
const note = db.notes?.note(curr);
if (!note)
prev.push({
key: curr,
title: `Note with id ${curr}`,
onClick: () => showToast("error", "This note does not exist.")
});
else
prev.push({
key: note.id,
title: note.title,
onClick: () => {
hashNavigate(`/notes/${curr}/edit`);
closeOpenedDialog();
}
});
return prev;
}, [] as MenuItem[])
},
{
key: "recheck",
title: () => "Recheck",
icon: DoubleCheckmark,
disabled: ({ attachment }) =>
!attachment.dateUploaded ? "This attachment is not uploaded yet." : false,
onClick: async ({ attachment }) => {
await store.recheck([attachment.metadata.hash]);
}
},
{
key: "rename",
title: () => "Rename",
icon: Rename,
onClick: async ({ attachment }) => {
const newName = await showPromptDialog({
title: "Rename attachment",
description: attachment.metadata.filename,
defaultValue: attachment.metadata.filename
});
if (!newName) return;
await store.rename(attachment.metadata.hash, newName);
}
},
{
key: "download",
title: ({ status }) =>
status?.type === "download" ? "Cancel download" : "Download",
icon: Download,
disabled: ({ attachment }) =>
!attachment.dateUploaded ? "This attachment is not uploaded yet." : false,
onClick: async ({ attachment, status }) => {
const isDownloading = status?.type === "download";
if (isDownloading) {
await db.fs.cancel(attachment.metadata.hash, "download");
} else await saveAttachment(attachment.metadata.hash);
}
},
{
key: "reupload",
title: ({ status }) =>
status?.type === "upload" ? "Cancel upload" : "Reupload",
icon: Reupload,
onClick: async ({ attachment, status }) => {
const isDownloading = status?.type === "upload";
if (isDownloading) {
await db.fs.cancel(attachment.metadata.hash, "upload");
} else
await reuploadAttachment(
attachment.metadata.type,
attachment.metadata.hash
);
}
},
{
key: "permanent-delete",
color: "error",
iconColor: "error",
title: () => "Delete permanently",
icon: DeleteForver,
onClick: ({ attachment }) => Multiselect.deleteAttachments([attachment])
}
];

View File

@@ -0,0 +1,220 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useCallback, useEffect, useState } from "react";
import { Flex, Text } from "@theme-ui/components";
import * as Icon from "../icons";
import { db } from "../../common/db";
import Dialog from "./dialog";
import { useStore, store } from "../../stores/tag-store";
import { store as notestore } from "../../stores/note-store";
import { Perform } from "../../common/dialog-controller";
import { FilteredList } from "../filtered-list";
type SelectedReference = {
id: string;
new: boolean;
op: "add" | "remove";
};
type Item = {
id: string;
type: "tag" | "header";
title: string;
};
type Tag = Item & { noteIds: string[] };
export type AddTagsDialogProps = {
onClose: Perform;
noteIds: string[];
};
function AddTagsDialog(props: AddTagsDialogProps) {
const { onClose, noteIds } = props;
const refreshTags = useStore((store) => store.refresh);
const tags = useStore((store) => store.tags);
useEffect(() => {
refreshTags();
}, [refreshTags]);
const [selected, setSelected] = useState<SelectedReference[]>([]);
const getAllTags = useCallback(() => {
refreshTags();
return (store.get().tags as Item[]).filter((a) => a.type !== "header");
}, [refreshTags]);
useEffect(() => {
if (!tags) return;
setSelected((s) => {
const selected = s.slice();
for (const tag of tags as Tag[]) {
if (tag.type === "header") continue;
if (selected.findIndex((a) => a.id === tag.id) > -1) continue;
if (tagHasNotes(tag, noteIds)) {
selected.push({
id: tag.id,
op: "add",
new: false
});
}
}
return selected;
});
}, [noteIds, tags, setSelected]);
return (
<Dialog
isOpen={true}
title={"Add tags"}
description={`Add tags to multiple notes at once`}
onClose={() => onClose(false)}
width={450}
positiveButton={{
text: "Done",
onClick: async () => {
for (const id of noteIds) {
for (const item of selected) {
if (item.op === "add") await db.notes?.note(id).tag(item.id);
else await db.notes?.note(id).untag(item.id);
}
}
notestore.refresh();
onClose(true);
}
}}
negativeButton={{
text: "Cancel",
onClick: () => onClose(false)
}}
>
<Flex
mt={1}
sx={{ overflowY: "hidden", flexDirection: "column" }}
data-test-id="tag-list"
>
<FilteredList
items={getAllTags}
placeholders={{
empty: "Add a new tag",
filter: "Search or add a new tag"
}}
filter={(tags, query) => db.lookup?.tags(tags, query) || []}
onCreateNewItem={async (title) => {
const tag = await db.tags?.add(title);
setSelected((selected) => [
...selected,
{ id: tag.id, new: true, op: "add" }
]);
}}
renderItem={(tag, _index) => {
const selectedTag = selected.find((item) => item.id === tag.id);
return (
<TagItem
key={tag.id}
tag={tag}
selected={selectedTag ? selectedTag.op : false}
onSelect={() => {
setSelected((selected) => {
const copy = selected.slice();
const index = copy.findIndex((item) => item.id === tag.id);
const isNew = copy[index] && copy[index].new;
if (isNew) {
copy.splice(index, 1);
} else if (index > -1) {
copy[index] = {
...copy[index],
op: copy[index].op === "add" ? "remove" : "add"
};
} else {
copy.push({ id: tag.id, new: true, op: "add" });
}
return copy;
});
}}
/>
);
}}
/>
</Flex>
</Dialog>
);
}
function TagItem(props: {
tag: Item;
selected: boolean | SelectedReference["op"];
onSelect: () => void;
}) {
const { tag, selected, onSelect } = props;
return (
<Flex
as="li"
data-test-id="tag"
sx={{
cursor: "pointer",
justifyContent: "space-between",
alignItems: "center",
bg: "bgSecondary",
borderRadius: "default",
p: 1,
background: "bgSecondary"
}}
onClick={onSelect}
>
<Flex sx={{ alignItems: "center" }}>
<SelectedCheck size={20} selected={selected} />
<Text
className="title"
data-test-id="notebook-title"
variant="subtitle"
sx={{ fontWeight: "body", color: "text" }}
>
#{tag.title}
</Text>
</Flex>
</Flex>
);
}
export default AddTagsDialog;
function SelectedCheck({
selected,
size = 20
}: {
selected: SelectedReference["op"] | boolean;
size?: number;
}) {
return selected === "add" ? (
<Icon.CheckCircleOutline size={size} sx={{ mr: 1 }} color="primary" />
) : selected === "remove" ? (
<Icon.CheckRemove size={size} sx={{ mr: 1 }} color="error" />
) : (
<Icon.CircleEmpty size={size} sx={{ mr: 1, opacity: 0.4 }} />
);
}
function tagHasNotes(tag: Tag, noteIds: string[]) {
return tag.noteIds.some((id) => noteIds.indexOf(id) > -1);
}

View File

@@ -1,68 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect } from "react";
import { Flex } from "@theme-ui/components";
import { getTotalSize } from "../../common/attachments";
import { useStore } from "../../stores/attachment-store";
import { formatBytes } from "../../utils/filename";
import Field from "../field";
import ListContainer from "../list-container";
import Dialog from "./dialog";
import Placeholder from "../placeholders";
function AttachmentsDialog({ onClose }) {
const attachments = useStore((store) => store.attachments);
const refresh = useStore((store) => store.refresh);
const filter = useStore((store) => store.filter);
useEffect(() => {
refresh();
}, [refresh]);
return (
<Dialog
isOpen={true}
width={500}
title="Attachments"
description={`${attachments.length} attachments | ${formatBytes(
getTotalSize(attachments)
)} occupied`}
onClose={onClose}
noScroll
negativeButton={{ text: "Close", onClick: onClose }}
>
<Flex px={2} sx={{ flexDirection: "column", height: 500 }}>
<Field
placeholder="Filter attachments by filename, type or hash"
sx={{ mb: 1, px: 1 }}
onChange={(e) => filter(e.target.value)}
/>
<ListContainer
header={<div />}
type="attachments"
groupType="attachments"
placeholder={<Placeholder context="attachments" />}
items={attachments}
/>
</Flex>
</Dialog>
);
}
export default AttachmentsDialog;

View File

@@ -0,0 +1,515 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState, memo, useMemo, useRef } from "react";
import {
Box,
Button,
Checkbox,
Donut,
Flex,
Input,
Label,
Text
} from "@theme-ui/components";
import { getTotalSize } from "../../common/attachments";
import { useStore, store } from "../../stores/attachment-store";
import { formatBytes } from "../../utils/filename";
import Dialog from "./dialog";
import { TableVirtuoso } from "react-virtuoso";
import {
ChevronDown,
ChevronUp,
Close,
DoubleCheckmark,
Download,
FileDocument,
FileGeneral,
FileImage,
FileVideo,
Icon,
Trash,
Unlink,
Uploading
} from "../icons";
import NavigationItem from "../navigation-menu/navigation-item";
import { pluralize } from "../../utils/string";
import { db } from "../../common/db";
import { Perform } from "../../common/dialog-controller";
import { Multiselect } from "../../common/multi-select";
import { CustomScrollbarsVirtualList } from "../list-container";
import { Attachment } from "../attachment";
import { isDocument, isImage, isVideo } from "@notesnook/core/utils/filename";
type ToolbarAction = {
title: string;
icon: Icon;
onClick: ({ selected }: { selected: any[] }) => void;
};
const TOOLBAR_ACTIONS: ToolbarAction[] = [
{
title: "Download",
icon: Download,
onClick: async ({ selected }) => {
await store.get().download(selected);
}
},
{
title: "Recheck",
icon: DoubleCheckmark,
onClick: async ({ selected }) => {
await store.recheck(selected.map((a) => a.metadata.hash));
}
},
{
title: "Delete",
icon: Trash,
onClick: ({ selected }) => Multiselect.deleteAttachments(selected)
}
];
type AttachmentsDialogProps = { onClose: Perform };
function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
const allAttachments = useStore((store) => store.attachments);
const [attachments, setAttachments] = useState<any[]>(allAttachments);
const [selected, setSelected] = useState<string[]>([]);
const [sortBy, setSortBy] = useState({ id: "name", direction: "asc" });
const currentRoute = useRef<Route>("all");
const refresh = useStore((store) => store.refresh);
useEffect(() => {
refresh();
}, [refresh]);
useEffect(() => {
setAttachments(filterAttachments(currentRoute.current, allAttachments));
}, [allAttachments]);
useEffect(() => {
setAttachments((a) => {
const attachments = a.slice();
if (sortBy.id === "name") {
attachments.sort(
sortBy.direction === "asc"
? (a, b) => a.metadata.filename.localeCompare(b.metadata.filename)
: (a, b) => b.metadata.filename.localeCompare(a.metadata.filename)
);
} else if (sortBy.id === "size") {
attachments.sort(
sortBy.direction === "asc"
? (a, b) => a.length - b.length
: (a, b) => b.length - a.length
);
} else if (sortBy.id === "dateUploaded") {
attachments.sort(
sortBy.direction === "asc"
? (a, b) => a.dateUploaded - b.dateUploaded
: (a, b) => b.dateUploaded - a.dateUploaded
);
}
return attachments;
});
}, [sortBy]);
const totalSize = useMemo(
() => getTotalSize(allAttachments),
[allAttachments]
);
return (
<Dialog
isOpen={true}
width={"70%"}
onClose={() => onClose(false)}
noScroll
sx={{ bg: "transparent" }}
>
<Flex
sx={{
height: "80vw"
}}
>
<Sidebar
totalSize={totalSize}
filter={(query) => {
setAttachments(
db.lookup?.attachments(db.attachments?.all || [], query) || []
);
}}
counts={getCounts(allAttachments)}
onRouteChange={(route) => {
currentRoute.current = route;
setSelected([]);
setAttachments(filterAttachments(route, allAttachments));
}}
/>
<Flex
variant="columnFill"
sx={{
bg: "background",
flexDirection: "column",
px: 4,
pt: 2,
overflowY: "hidden",
overflow: "hidden",
table: { width: "100%", tableLayout: "fixed" },
"tbody::before": {
content: `''`,
display: "block",
height: 5
}
}}
>
<Flex sx={{ justifyContent: "space-between" }}>
<Flex sx={{ gap: 1 }}>
{TOOLBAR_ACTIONS.map((tool) => (
<Button
variant="tool"
key={tool.title}
title={tool.title}
onClick={() =>
tool.onClick({
selected: attachments.filter(
(a) => selected.indexOf(a.id) > -1
)
})
}
disabled={!selected.length}
sx={{ bg: "transparent", p: 1 }}
>
<tool.icon size={18} />
</Button>
))}
</Flex>
{/* <Button
variant="tool"
sx={{ p: 1, display: "flex" }}
onClick={async () => {
if (!(await insertAttachment())) return;
refresh();
}}
>
<Plus size={18} />
<Text variant="body" sx={{ ml: 1 }}>
Upload
</Text>
</Button> */}
</Flex>
<TableVirtuoso
components={{
Scroller: CustomScrollbarsVirtualList,
TableRow: (props) => {
const attachment = attachments[props["data-item-index"]];
return (
<Attachment
{...props}
key={attachment.id}
attachment={attachment}
isSelected={selected.indexOf(attachment.id) > -1}
onSelected={() => {
setSelected((s) => {
const copy = s.slice();
const index = copy.indexOf(attachment.id);
if (index > -1) copy.splice(index, 1);
else copy.push(attachment.id);
return copy;
});
}}
/>
);
}
}}
style={{ height: "100%" }}
data={attachments}
fixedItemHeight={30}
defaultItemHeight={30}
fixedHeaderContent={() => (
<Box
as="tr"
sx={{
height: 40,
th: { borderBottom: "1px solid var(--border)" },
bg: "background"
}}
>
<Text
as="th"
variant="body"
sx={{
width: 24,
textAlign: "left",
fontWeight: "normal",
mb: 2
}}
>
<Label>
<Checkbox
sx={{ width: 18, height: 18 }}
onChange={(e) => {
setSelected(
e.currentTarget.checked
? attachments.map((a) => a.id)
: []
);
}}
/>
</Label>
</Text>
{[
{ id: "name", title: "Name", width: "65%" },
{ id: "status", width: "24px" },
{ id: "size", title: "Size", width: "15%" },
{ id: "dateUploaded", title: "Date uploaded", width: "20%" }
].map((column) =>
!column.title ? (
<th key={column.id} />
) : (
<Box
as="th"
key={column.id}
sx={{
width: column.width,
cursor: "pointer",
px: 1,
mb: 2,
":hover": { bg: "hover" }
}}
onClick={() => {
setSortBy((sortBy) => ({
direction:
sortBy.id === column.id &&
sortBy.direction === "asc"
? "desc"
: "asc",
id: column.id
}));
}}
>
<Flex
sx={{
alignItems: "center",
justifyContent: "space-between"
}}
>
<Text
variant="body"
sx={{ textAlign: "left", fontWeight: "normal" }}
>
{column.title}
</Text>
{sortBy.id === column.id ? (
sortBy.direction === "asc" ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)
) : null}
</Flex>
</Box>
)
)}
</Box>
)}
itemContent={() => <></>}
/>
</Flex>
</Flex>
</Dialog>
);
}
export default AttachmentsDialog;
type Route = "all" | "images" | "documents" | "videos" | "uploads" | "orphaned";
const routes: { id: Route; icon: Icon; title: string }[] = [
{
id: "all",
icon: FileGeneral,
title: "All files"
},
{
id: "images",
icon: FileImage,
title: "Images"
},
{
id: "documents",
icon: FileDocument,
title: "Documents"
},
{
id: "videos",
icon: FileVideo,
title: "Videos"
},
{
id: "uploads",
icon: Uploading,
title: "Uploads"
},
{
id: "orphaned",
icon: Unlink,
title: "Orphaned"
}
];
type SidebarProps = {
onRouteChange: (route: Route) => void;
filter: (query: string) => void;
counts: Record<Route, number>;
totalSize: number;
};
const Sidebar = memo(
function Sidebar(props: SidebarProps) {
const { onRouteChange, filter, counts, totalSize } = props;
const [route, setRoute] = useState("all");
const downloadStatus = useStore((store) => store.status);
const cancelDownload = useStore((store) => store.cancel);
const download = useStore((store) => store.download);
return (
<Flex
sx={{
flexDirection: "column",
justifyContent: "space-between",
width: 240,
bg: "bgSecondary",
"@supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none))":
{
bg: "bgTransparent",
backdropFilter: "blur(8px)"
}
}}
>
<Flex sx={{ flexDirection: "column" }}>
<Input
placeholder="Search"
sx={{ m: 2, mb: 0, width: "auto", bg: "bgSecondary", py: "7px" }}
onChange={(e) => {
setRoute(e.target.value ? "none" : "all");
if (e.target.value) filter(e.target.value);
}}
/>
{routes.map((item) => (
<NavigationItem
key={item.id}
icon={item.icon}
title={item.title}
count={counts[item.id]}
onClick={() => {
onRouteChange(item.id);
setRoute(item.id);
}}
selected={route === item.id}
/>
))}
</Flex>
<Flex sx={{ flexDirection: "column" }}>
<Flex sx={{ pl: 2, m: 2, mt: 1, justifyContent: "space-between" }}>
<Flex sx={{ flexDirection: "column" }}>
<Text variant="body">
{pluralize(counts.all, "file", "files")}
</Text>
<Text variant="subBody">{formatBytes(totalSize)}</Text>
</Flex>
<Button
variant="tool"
sx={{
bg: "transparent",
borderRadius: 100,
position: "relative",
width: 38,
height: 38
}}
title="Download all attachments"
onClick={async () => {
if (downloadStatus) {
await cancelDownload();
} else {
await download(db.attachments?.all);
}
}}
>
{downloadStatus ? <Close size={18} /> : <Download size={18} />}
{downloadStatus ? (
<Donut
value={(downloadStatus.current / downloadStatus.total) * 100}
max={100}
size={38}
sx={{
position: "absolute",
top: 0,
left: 0
}}
/>
) : null}
</Button>
</Flex>
</Flex>
</Flex>
);
},
(prev, next) =>
prev.totalSize === next.totalSize &&
prev.counts.all === next.counts.all &&
prev.counts.documents === next.counts.documents &&
prev.counts.images === next.counts.images &&
prev.counts.videos === next.counts.videos &&
prev.counts.uploads === next.counts.uploads &&
prev.counts.orphaned === next.counts.orphaned
);
function getCounts(attachments: any[]): Record<Route, number> {
const counts: Record<Route, number> = {
all: 0,
documents: 0,
images: 0,
videos: 0,
uploads: 0,
orphaned: 0
};
for (const attachment of attachments) {
counts.all++;
if (isDocument(attachment.metadata.type)) counts.documents++;
else if (isImage(attachment.metadata.type)) counts.images++;
else if (isVideo(attachment.metadata.type)) counts.videos++;
if (!attachment.dateUploaded) counts.uploads++;
if (!attachment.noteIds.length) counts.orphaned++;
}
return counts;
}
function filterAttachments(route: Route, attachments: any[]): any[] {
return route === "all"
? attachments
: route === "images"
? attachments.filter((a) => a.metadata.type.startsWith("image/"))
: route === "videos"
? attachments.filter((a) => a.metadata.type.startsWith("video/"))
: route === "documents"
? attachments.filter((a) => isDocument(a.metadata.type))
: route === "orphaned"
? attachments.filter((a) => !a.noteIds.length)
: attachments.filter((a) => !a.dateUploaded);
}

View File

@@ -0,0 +1,145 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { Perform } from "../../common/dialog-controller";
import Dialog from "./dialog";
import { db } from "../../common/db";
import { Loading } from "../icons";
import { Flex, Link, Text } from "@theme-ui/components";
import { formatDate } from "@notesnook/core/utils/date";
type Transaction = {
order_id: string;
checkout_id: string;
amount: string;
currency: string;
status: keyof typeof TransactionStatusToText;
created_at: Date;
passthrough: null;
product_id: number;
is_subscription: boolean;
is_one_off: boolean;
subscription: Subscription;
user: User;
receipt_url: string;
};
type Subscription = {
subscription_id: number;
status: string;
};
type User = {
user_id: number;
email: string;
marketing_consent: boolean;
};
const TransactionStatusToText = {
completed: "Completed",
refunded: "Refunded",
partially_refunded: "Partially refunded",
disputed: "Disputed"
};
export type BillingHistoryDialogProps = {
onClose: Perform;
};
export default function BillingHistoryDialog(props: BillingHistoryDialogProps) {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | undefined>();
useEffect(() => {
(async function () {
try {
setError(undefined);
setIsLoading(true);
const transactions = await db.subscriptions?.transactions();
if (!transactions) return;
setTransactions(transactions);
} catch (e) {
if (e instanceof Error) setError(e);
} finally {
setIsLoading(false);
}
})();
}, []);
return (
<Dialog
isOpen={true}
title={"Billing history"}
description={"View all the transactions you have made with Notesnook."}
onClose={() => props.onClose(false)}
negativeButton={{ text: "Close", onClick: () => props.onClose(false) }}
width={600}
>
{isLoading ? (
<Loading sx={{ mt: 2 }} />
) : error ? (
<Flex sx={{ bg: "errorBg", p: 1, borderRadius: "default" }}>
<Text variant="error">
{error.message}
<br />
{error.stack}
</Text>
</Flex>
) : (
<Flex sx={{ flexDirection: "column", gap: 1, mt: 1 }}>
{transactions.map((transaction) => (
<Flex
key={transaction.order_id}
sx={{
justifyContent: "space-between",
borderBottom: "1px solid var(--border)",
pb: 1
}}
>
<Flex sx={{ flexDirection: "column" }}>
<Text variant="subtitle">Order #{transaction.order_id}</Text>
<Text variant="body" sx={{ color: "fontTertiary" }}>
{formatDate(new Date(transaction.created_at).getTime())} {" "}
{TransactionStatusToText[transaction.status]}
</Text>
</Flex>
<Flex sx={{ flexDirection: "column", alignItems: "end" }}>
<Text variant="body">
{transaction.amount} {transaction.currency}
</Text>
<Link
href={transaction.receipt_url}
target="_blank"
rel="noreferer nofollow"
variant="text.subBody"
sx={{ color: "primary" }}
>
View receipt
</Link>
</Flex>
</Flex>
))}
</Flex>
)}
</Dialog>
);
}

View File

@@ -55,6 +55,8 @@ export function BuyDialog(props: BuyDialogProps) {
const theme = useTheme() as Theme;
const onApplyCoupon = useCheckoutStore((store) => store.applyCoupon);
const isCheckoutCompleted = useCheckoutStore((store) => store.isCompleted);
useEffect(() => {
return () => {
useCheckoutStore.getState().reset();
@@ -118,7 +120,7 @@ export function BuyDialog(props: BuyDialogProps) {
boxShadow: "4px 5px 18px 2px #00000038",
borderRadius: "dialog",
flexDirection: ["column", "column", "row"],
width: ["95%", "80%", "60%"],
width: ["95%", "80%", isCheckoutCompleted ? "400px" : "60%"],
maxHeight: ["95%", "80%", "80%"],
alignSelf: "center",
overflowY: ["scroll", "scroll", "hidden"]
@@ -139,7 +141,7 @@ export function BuyDialog(props: BuyDialogProps) {
flexShrink: 0,
alignItems: "center",
justifyContent: "center",
width: ["100%", "100%", 350]
width: ["100%", "100%", isCheckoutCompleted ? "100%" : 350]
}}
p={4}
py={50}
@@ -165,6 +167,9 @@ function SideBar(props: SideBarProps) {
const user = useUserStore((store) => store.user);
const couponCode = useCheckoutStore((store) => store.couponCode);
const onApplyCoupon = useCheckoutStore((store) => store.applyCoupon);
const isCheckoutCompleted = useCheckoutStore((store) => store.isCompleted);
if (isCheckoutCompleted) return <CheckoutCompleted onClose={onClose} />;
if (user && selectedPlan)
return (
@@ -230,12 +235,16 @@ function Details() {
const user = useUserStore((store) => store.user);
const selectedPlan = useCheckoutStore((state) => state.selectedPlan);
const onPriceUpdated = useCheckoutStore((state) => state.updatePrice);
const completeCheckout = useCheckoutStore((state) => state.completeCheckout);
const isCheckoutCompleted = useCheckoutStore((store) => store.isCompleted);
const couponCode = useCheckoutStore((store) => store.couponCode);
const setIsApplyingCoupon = useCheckoutStore(
(store) => store.setIsApplyingCoupon
);
const theme = useThemeStore((store) => store.theme);
if (isCheckoutCompleted) return null;
if (selectedPlan && user)
return (
<PaddleCheckout
@@ -243,6 +252,7 @@ function Details() {
theme={theme}
user={user}
coupon={couponCode}
onCompleted={completeCheckout}
onCouponApplied={() => setIsApplyingCoupon(true)}
onPriceUpdated={(pricingInfo) => {
onPriceUpdated(pricingInfo);
@@ -396,6 +406,31 @@ function AlreadyPremium(props: AlreadyPremiumProps) {
);
}
function CheckoutCompleted(props: { onClose: () => void }) {
const { onClose } = props;
return (
<>
<Rocket width={200} />
<Text variant="heading" mt={4} sx={{ textAlign: "center" }}>
Thank you!
</Text>
<Text variant="body" mt={1} sx={{ textAlign: "center" }}>
You have successfully subscribed to Notesnook Pro.
</Text>
<Button
variant="primary"
mt={2}
sx={{ borderRadius: 100, px: 6 }}
onClick={onClose}
data-test-id="see-all-plans"
>
Continue
</Button>
</>
);
}
type SelectedPlanProps = {
plan: Plan;
pricingInfo: PricingInfo | undefined;

View File

@@ -33,13 +33,25 @@ import {
// const isDev = false; // process.env.NODE_ENV === "development";
// const VENDOR_ID = isDev ? 1506 : 128190;
const PADDLE_ORIGIN = "https://buy.paddle.com";
const PADDLE_ORIGIN =
process.env.NODE_ENV === "development"
? "https://sandbox-buy.paddle.com"
: "https://buy.paddle.com";
const CHECKOUT_CREATE_ORIGIN =
process.env.NODE_ENV === "development"
? "https://sandbox-create-checkout.paddle.com"
: "https://create-checkout.paddle.com";
const CHECKOUT_SERVICE_ORIGIN =
process.env.NODE_ENV === "development"
? "https://sandbox-checkout-service.paddle.com"
: "https://checkout-service.paddle.com";
const SUBSCRIBED_EVENTS: PaddleEvents[] = [
PaddleEvents["Checkout.Loaded"],
PaddleEvents["Checkout.Coupon.Applied"],
PaddleEvents["Checkout.Coupon.Remove"],
PaddleEvents["Checkout.Location.Submit"]
PaddleEvents["Checkout.Location.Submit"],
PaddleEvents["Checkout.Complete"]
];
type PaddleCheckoutProps = {
@@ -48,10 +60,11 @@ type PaddleCheckoutProps = {
plan: Plan;
onPriceUpdated?: (pricingInfo: PricingInfo) => void;
onCouponApplied?: () => void;
onCompleted?: () => void;
coupon?: string;
};
export function PaddleCheckout(props: PaddleCheckoutProps) {
const { plan, onPriceUpdated, coupon, onCouponApplied } = props;
const { plan, onPriceUpdated, coupon, onCouponApplied, onCompleted } = props;
const [sourceUrl, setSourceUrl] = useState<string>();
const [isLoading, setIsLoading] = useState(true);
const [checkoutId, setCheckoutId] = useState<string>();
@@ -61,7 +74,7 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
const reloadCheckout = useCallback(() => {
if (!checkoutRef.current) return;
setIsLoading(true);
checkoutRef.current.src = `https://buy.paddle.com/checkout/?checkout_id=${checkoutId}&display_mode=inline&apple_pay_enabled=false`;
checkoutRef.current.src = `${PADDLE_ORIGIN}/checkout/?checkout_id=${checkoutId}&display_mode=inline&apple_pay_enabled=false`;
}, [checkoutId]);
const updatePrice = useCallback(
@@ -96,6 +109,10 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
)
return;
if (event_name === PaddleEvents["Checkout.Complete"]) {
onCompleted && onCompleted();
return;
}
if (event_name === PaddleEvents["Checkout.Loaded"]) setIsLoading(false);
const pricingInfo = await updatePrice(checkout.id);
@@ -108,7 +125,7 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
return () => {
window.removeEventListener("message", onMessage);
};
}, [onPriceUpdated, updatePrice, plan]);
}, [onPriceUpdated, updatePrice, plan, onCompleted]);
useEffect(() => {
if (
@@ -173,7 +190,7 @@ export function PaddleCheckout(props: PaddleCheckoutProps) {
async function getCheckoutURL(params: PaddleCheckoutProps) {
const { plan, theme, user } = params;
const BASE_URL = `https://create-checkout.paddle.com/checkout/product/${plan.id}`;
const BASE_URL = `${CHECKOUT_CREATE_ORIGIN}/checkout/product/${plan.id}`;
const queryParams = new URLSearchParams();
queryParams.set("product", plan.id);
queryParams.set("passthrough", JSON.stringify({ userId: user.id }));
@@ -227,7 +244,7 @@ async function applyCoupon(
checkoutId: string,
couponCode: string
): Promise<CheckoutData | false> {
const url = ` https://checkout-service.paddle.com/checkout/${checkoutId}/coupon`;
const url = ` ${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}/coupon`;
const body = { data: { coupon_code: couponCode } };
const headers = new Headers();
headers.set("content-type", "application/json");
@@ -246,7 +263,7 @@ async function applyCoupon(
}
async function removeCoupon(checkoutId: string): Promise<CheckoutData | false> {
const url = ` https://checkout-service.paddle.com/checkout/${checkoutId}/coupon`;
const url = ` ${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}/coupon`;
const response = await fetch(url, {
method: "DELETE"
@@ -260,7 +277,7 @@ async function removeCoupon(checkoutId: string): Promise<CheckoutData | false> {
}
async function sendCheckoutEvent(checkoutId: string, eventName: PaddleEvents) {
const url = ` https://checkout-service.paddle.com/checkout/${checkoutId}/event`;
const url = ` ${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}/event`;
const body = { data: { event_name: eventName } };
const headers = new Headers();
headers.set("content-type", "application/json");
@@ -275,7 +292,7 @@ async function sendCheckoutEvent(checkoutId: string, eventName: PaddleEvents) {
async function getCheckoutData(
checkoutId: string
): Promise<CheckoutData | undefined> {
const url = `https://checkout-service.paddle.com/checkout/${checkoutId}`;
const url = `${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}`;
const response = await fetch(url);
if (!response.ok) return undefined;
const json = (await response.json()) as CheckoutDataResponse;

View File

@@ -32,7 +32,7 @@ export const DEFAULT_PLANS: Plan[] = [
country: "PK",
currency: "USD",
discount: 0,
id: "648884",
id: process.env.NODE_ENV === "development" ? "9822" : "648884",
price: { gross: 4.49, net: 0, tax: 0 }
},
{
@@ -40,7 +40,7 @@ export const DEFAULT_PLANS: Plan[] = [
country: "PK",
currency: "USD",
discount: 0,
id: "658759",
id: process.env.NODE_ENV === "development" ? "50305" : "658759",
price: { gross: 49.99, net: 0, tax: 0 }
}
];
@@ -52,7 +52,8 @@ export const PLAN_METADATA: Record<Period, PlanMetadata> = {
let CACHED_PLANS: Plan[];
export async function getPlans(): Promise<Plan[] | null> {
if (isTesting()) return DEFAULT_PLANS;
if (isTesting() || process.env.NODE_ENV === "development")
return DEFAULT_PLANS;
if (CACHED_PLANS) return CACHED_PLANS;
const url = `https://notesnook.com/api/v1/prices/products/web`;

View File

@@ -22,6 +22,8 @@ import create from "zustand";
import produce from "immer";
interface ICheckoutStore {
isCompleted: boolean;
completeCheckout: () => void;
selectedPlan?: Plan;
selectPlan: (plan?: Plan) => void;
pricingInfo?: PricingInfo;
@@ -33,10 +35,17 @@ interface ICheckoutStore {
reset: () => void;
}
export const useCheckoutStore = create<ICheckoutStore>((set) => ({
isCompleted: false,
selectedPlan: undefined,
pricingInfo: undefined,
couponCode: undefined,
isApplyingCoupon: false,
completeCheckout: () =>
set(
produce((state: ICheckoutStore) => {
state.isCompleted = true;
})
),
selectPlan: (plan) =>
set(
produce((state: ICheckoutStore) => {

View File

@@ -22,6 +22,7 @@ import { Flex, Text, Button, ButtonProps } from "@theme-ui/components";
import * as Icon from "../icons";
import ReactModal from "react-modal";
import { FlexScrollContainer } from "../scroll-container";
import { SxProp } from "@theme-ui/core";
ReactModal.setAppElement("#root");
@@ -32,7 +33,7 @@ type DialogButtonProps = ButtonProps & {
loading?: boolean;
};
type DialogProps = {
type DialogProps = SxProp & {
isOpen?: boolean;
onClose?: (
event?: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>
@@ -47,6 +48,7 @@ type DialogProps = {
positiveButton?: DialogButtonProps | null;
negativeButton?: DialogButtonProps | null;
footer?: React.Component;
noScroll?: boolean;
};
function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
@@ -94,7 +96,9 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
position: "relative",
overflow: "hidden",
boxShadow: "4px 5px 18px 2px #00000038",
borderRadius: "dialog"
borderRadius: "dialog",
...props.sx
}}
>
{props.showCloseButton && (
@@ -111,34 +115,42 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
onClick={props.onClose}
/>
)}
<Flex sx={{ flexDirection: "column" }} p={4} pb={0}>
<Text
variant="heading"
sx={{
fontSize: "subheading",
textAlign: props.textAlignment || "left",
color: "text"
}}
>
{props.title}
</Text>
{props.description && (
<Text
variant="body"
sx={{
textAlign: props.textAlignment || "left",
color: "fontTertiary"
}}
>
{props.description}
</Text>
)}
</Flex>
<Flex variant="columnFill" sx={{ overflowY: "hidden" }} my={1}>
<FlexScrollContainer style={{ paddingRight: 20, paddingLeft: 20 }}>
{props.children}
</FlexScrollContainer>
</Flex>
{props.title || props.description ? (
<Flex sx={{ flexDirection: "column" }} p={4} pb={0}>
{props.title && (
<Text
variant="heading"
sx={{
fontSize: "subheading",
textAlign: props.textAlignment || "left",
color: "text"
}}
>
{props.title}
</Text>
)}
{props.description && (
<Text
variant="body"
sx={{
textAlign: props.textAlignment || "left",
color: "fontTertiary"
}}
>
{props.description}
</Text>
)}
</Flex>
) : null}
{props.noScroll ? (
<>{props.children}</>
) : (
<Flex variant="columnFill" sx={{ overflowY: "hidden" }} my={1}>
<FlexScrollContainer style={{ paddingRight: 20, paddingLeft: 20 }}>
{props.children}
</FlexScrollContainer>
</Flex>
)}
{(props.positiveButton || props.negativeButton) && (
<Flex

View File

@@ -22,7 +22,7 @@ import Dialog from "./dialog";
import { getHomeRoute, hardNavigate } from "../../navigation";
import { appVersion } from "../../utils/version";
import Config from "../../utils/config";
import { isDesktop, isTesting } from "../../utils/platform";
import { isTesting } from "../../utils/platform";
import { useEffect } from "react";
import { ArrowRight, Checkmark, Icon, Warn } from "../icons";
@@ -89,9 +89,23 @@ const features: Record<FeatureKeys, Feature> = {
)
}
]
: isDesktop()
? []
: [],
: [
{
title: "Attachments preview",
subtitle:
"You can now preview PDFs & Images directly inside Notesnook."
},
{
title: "New attachments manager",
subtitle:
"The new attachments manager makes is much easier to view & interact with your attachments. It also allows you to download all (or some) of your attachments."
},
{
title: "Assign tags to multiple notes",
subtitle:
"You can now easily tag multiple notes without first opening them."
}
],
cta: {
title: "Got it",
icon: Checkmark,

View File

@@ -57,6 +57,10 @@ const EmailChangeDialog = React.lazy(() => import("./email-change-dialog"));
const LanguageSelectorDialog = React.lazy(
() => import("./language-selector-dialog")
);
const BillingHistoryDialog = React.lazy(
() => import("./billing-history-dialog")
);
const AddTagsDialog = React.lazy(() => import("./add-tags-dialog"));
export const Dialogs = {
AddNotebookDialog,
@@ -85,5 +89,7 @@ export const Dialogs = {
AddReminderDialog,
ReminderPreviewDialog,
EmailChangeDialog,
LanguageSelectorDialog
LanguageSelectorDialog,
BillingHistoryDialog,
AddTagsDialog
};

View File

@@ -17,12 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Box, Button, Flex, Input, Text } from "@theme-ui/components";
import * as Icon from "../icons";
import { db } from "../../common/db";
import Dialog from "./dialog";
import Field from "../field";
import { useStore, store } from "../../stores/notebook-store";
import { store as notestore } from "../../stores/note-store";
import { Perform } from "../../common/dialog-controller";
@@ -30,6 +29,7 @@ import { showToast } from "../../utils/toast";
import { pluralize } from "../../utils/string";
import { isMac } from "../../utils/platform";
import create from "zustand";
import { FilteredList } from "../filtered-list";
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
type NotebookReference = {
@@ -59,7 +59,7 @@ interface ISelectionStore {
export const useSelectionStore = create<ISelectionStore>((set) => ({
selected: [],
isMultiselect: false,
setSelected: (selected) => set({ selected }),
setSelected: (selected) => set({ selected: selected.slice() }),
setIsMultiselect: (isMultiselect) => set({ isMultiselect })
}));
@@ -193,7 +193,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
sx={{ overflowY: "hidden", flexDirection: "column" }}
data-test-id="notebook-list"
>
<FilteredTree
<FilteredList
placeholders={{
empty: "Add a new notebook",
filter: "Search or add a new notebook"
@@ -423,127 +423,6 @@ function TopicItem(props: { topic: Topic }) {
export default MoveDialog;
type FilteredTreeProps<T extends Item> = {
placeholders: { filter: string; empty: string };
items: () => T[];
filter: (items: T[], query: string) => T[];
onCreateNewItem: (title: string) => Promise<void>;
renderItem: (
item: T,
index: number,
refresh: () => void,
isSearching: boolean
) => JSX.Element;
};
function FilteredTree<T extends Item>(props: FilteredTreeProps<T>) {
const {
items: _items,
filter,
onCreateNewItem,
placeholders,
renderItem
} = props;
const [items, setItems] = useState<T[]>([]);
const [query, setQuery] = useState<string>();
const noItemsFound = items.length <= 0 && query && query.length > 0;
const inputRef = useRef<HTMLInputElement>(null);
const refresh = useCallback(() => {
setItems(_items());
}, [_items]);
useEffect(() => {
refresh();
}, [refresh]);
const _filter = useCallback(
(query) => {
setItems(() => {
const items = _items();
if (!query) {
return items;
}
return filter(items, query);
});
setQuery(query);
},
[_items, filter]
);
const _createNewItem = useCallback(
async (title) => {
await onCreateNewItem(title);
refresh();
setQuery(undefined);
if (inputRef.current) inputRef.current.value = "";
},
[inputRef, refresh, onCreateNewItem]
);
return (
<>
<Field
inputRef={inputRef}
data-test-id={"filter-input"}
autoFocus
placeholder={
items.length <= 0 ? placeholders.empty : placeholders.filter
}
onChange={(e: ChangeEvent) =>
_filter((e.target as HTMLInputElement).value)
}
onKeyUp={async (e: KeyboardEvent) => {
if (e.key === "Enter" && noItemsFound) {
await _createNewItem(query);
}
}}
action={
items.length <= 0
? {
icon: Icon.Plus,
onClick: async () => await _createNewItem(query)
}
: { icon: Icon.Search, onClick: () => _filter(query) }
}
/>
<Flex
as="ul"
mt={1}
sx={{
overflowY: "hidden",
listStyle: "none",
m: 0,
p: 0,
gap: 1,
display: "flex",
flexDirection: "column"
}}
>
{noItemsFound && (
<Button
variant={"secondary"}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
py: 2
}}
onClick={async () => {
await _createNewItem(query);
}}
>
<Text variant={"body"}>{`Add "${query}"`}</Text>
<Icon.Plus size={16} color="primary" />
</Button>
)}
{items.map((item, index) => renderItem(item, index, refresh, !!query))}
</Flex>
</>
);
}
function SelectedCheck({
item,
size = 20

View File

@@ -72,7 +72,7 @@ function Header({ readonly }) {
}
export default Header;
function Autosuggest({
export function Autosuggest({
sessionId,
filter,
onRemove,
@@ -200,6 +200,8 @@ function Autosuggest({
if (e.key === "ArrowDown" && !text) onOpenMenu(defaultItems.slice());
e.preventDefault();
} else if (e.key === "Tab") {
closeMenu();
}
}}
/>

View File

@@ -24,7 +24,8 @@ import {
useRef,
PropsWithChildren
} from "react";
import { Box, Button, Flex, Text } from "@theme-ui/components";
import ReactDOM from "react-dom";
import { Box, Button, Flex, Progress, Text } from "@theme-ui/components";
import Properties from "../properties";
import { useStore, store as editorstore } from "../../stores/editor-store";
import {
@@ -40,7 +41,7 @@ import Header from "./header";
import { Attachment } from "../icons";
import { useEditorInstance } from "./context";
import { attachFile, AttachmentProgress, insertAttachment } from "./picker";
import { downloadAttachment } from "../../common/attachments";
import { saveAttachment, downloadAttachment } from "../../common/attachments";
import { EV, EVENTS } from "@notesnook/core/common";
import { db } from "../../common/db";
import useMobile from "../../hooks/use-mobile";
@@ -49,6 +50,11 @@ import useTablet from "../../hooks/use-tablet";
import Config from "../../utils/config";
import { AnimatedFlex } from "../animated";
import { EditorLoader } from "../loaders/editor-loader";
import { Lightbox } from "../lightbox";
import ThemeProviderWrapper from "../theme-provider";
import { Allotment } from "allotment";
import { PdfPreview } from "../pdf-preview";
import { showToast } from "../../utils/toast";
type PreviewSession = {
content: { data: string; type: string };
@@ -56,6 +62,11 @@ type PreviewSession = {
dateEdited: number;
};
type DocumentPreview = {
url?: string;
hash: string;
};
function onEditorChange(noteId: string, sessionId: string, content: string) {
if (!content) return;
@@ -79,7 +90,9 @@ export default function EditorManager({
// stored in refs. Update this value to trigger an
// update.
const [timestamp, setTimestamp] = useState<number>(0);
const lastSavedTime = useRef<number>(0);
const [docPreview, setDocPreview] = useState<DocumentPreview>();
const previewSession = useRef<PreviewSession>();
const [dropRef, overlayRef] = useDragOverlay();
@@ -100,7 +113,7 @@ export default function EditorManager({
async (item?: Record<string, string | number>) => {
if (
!item ||
lastSavedTime.current >= item.dateEdited ||
lastSavedTime.current >= (item.dateEdited as number) ||
isPreviewSession ||
!appstore.get().isRealtimeSyncEnabled
)
@@ -185,48 +198,126 @@ export default function EditorManager({
openSession(noteId);
}, [noteId]);
return (
<Allotment
proportionalLayout={true}
onDragEnd={(sizes) => {
Config.set("editor:panesize", sizes[1]);
}}
>
<Allotment.Pane className="editor-pane">
<Flex
ref={dropRef}
id="editorContainer"
sx={{
position: "relative",
alignSelf: "stretch",
overflow: "hidden",
flex: 1,
flexDirection: "column"
}}
>
{previewSession.current && (
<PreviewModeNotice
{...previewSession.current}
onDiscard={() => openSession(noteId)}
/>
)}
<Editor
nonce={timestamp}
content={
previewSession.current?.content?.data ||
editorstore.get().session?.content?.data
}
onPreviewDocument={(url) => setDocPreview(url)}
onContentChange={() => (lastSavedTime.current = Date.now())}
options={{
readonly: isReadonly || isPreviewSession,
onRequestFocus: () => toggleProperties(false),
onLoadMedia: loadMedia,
focusMode: isFocusMode,
isMobile: isMobile || isTablet
}}
/>
{arePropertiesVisible && (
<Properties
onOpenPreviewSession={async (session: PreviewSession) => {
previewSession.current = session;
setTimestamp(Date.now());
}}
/>
)}
<DropZone overlayRef={overlayRef} />
</Flex>
</Allotment.Pane>
{docPreview && (
<Allotment.Pane
minSize={450}
preferredSize={Config.get("editor:panesize", 500)}
>
{docPreview.url ? (
<Flex
id="editorSidebar"
sx={{
flexDirection: "column",
overflow: "hidden",
borderLeft: "1px solid var(--border)",
height: "100%"
}}
>
<PdfPreview
fileUrl={docPreview.url}
hash={docPreview.hash}
onClose={() => setDocPreview(undefined)}
/>
</Flex>
) : (
<DownloadAttachmentProgress hash={docPreview.hash} />
)}
</Allotment.Pane>
)}
</Allotment>
);
}
type DownloadAttachmentProgressProps = {
hash: string;
};
function DownloadAttachmentProgress(props: DownloadAttachmentProgressProps) {
const { hash } = props;
const [progress, setProgress] = useState(0);
useEffect(() => {
const event = AppEventManager.subscribe(
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
(progress: AttachmentProgress) => {
if (progress.hash === hash) {
setProgress(Math.round((progress.loaded / progress.total) * 100));
}
}
);
return () => {
event.unsubscribe();
};
}, [hash]);
return (
<Flex
ref={dropRef}
id="editorContainer"
sx={{
position: "relative",
alignSelf: "stretch",
overflow: "hidden",
flex: 1,
height: "100%",
alignItems: "center",
justifyContent: "center",
flexDirection: "column"
}}
>
{previewSession.current && (
<PreviewModeNotice
{...previewSession.current}
onDiscard={() => openSession(noteId)}
/>
)}
<Editor
nonce={timestamp}
content={
previewSession.current?.content?.data ||
editorstore.get().session?.content?.data
}
onContentChange={() => (lastSavedTime.current = Date.now())}
options={{
readonly: isReadonly || isPreviewSession,
onRequestFocus: () => toggleProperties(false),
onLoadMedia: loadMedia,
focusMode: isFocusMode,
isMobile: isMobile || isTablet
}}
<Text variant="title">Downloading attachment ({progress}%)</Text>
<Progress
value={progress}
max={100}
sx={{ width: ["90%", "35%"], mt: 1 }}
/>
{arePropertiesVisible && (
<Properties
onOpenPreviewSession={async (session: PreviewSession) => {
previewSession.current = session;
setTimestamp(Date.now());
}}
/>
)}
<DropZone overlayRef={overlayRef} />
</Flex>
);
}
@@ -244,9 +335,10 @@ type EditorProps = {
nonce?: number;
options?: EditorOptions;
onContentChange?: () => void;
onPreviewDocument?: (preview: DocumentPreview) => void;
};
export function Editor(props: EditorProps) {
const { content, nonce, options, onContentChange } = props;
const { content, nonce, options, onContentChange, onPreviewDocument } = props;
const { readonly, headless, onLoadMedia, isMobile } = options || {
headless: false,
readonly: false,
@@ -314,9 +406,35 @@ export function Editor(props: EditorProps) {
}}
onContentChange={onContentChange}
onChange={onEditorChange}
onDownloadAttachment={(attachment) =>
downloadAttachment(attachment.hash)
}
onDownloadAttachment={(attachment) => saveAttachment(attachment.hash)}
onPreviewAttachment={async ({ hash, dataurl }) => {
const attachment = db.attachments?.attachment(hash);
if (attachment && attachment.metadata.type.startsWith("image/")) {
const container = document.getElementById("dialogContainer");
if (!(container instanceof HTMLElement)) return;
dataurl = dataurl || (await downloadAttachment(hash, "base64"));
if (!dataurl)
return showToast("error", "This image cannot be previewed.");
ReactDOM.render(
<ThemeProviderWrapper>
<Lightbox
image={dataurl}
onClose={() => {
ReactDOM.unmountComponentAtNode(container);
}}
/>
</ThemeProviderWrapper>,
container
);
} else if (attachment && onPreviewDocument) {
onPreviewDocument({ hash });
const blob = await downloadAttachment(hash, "blob");
if (!blob) return;
onPreviewDocument({ url: URL.createObjectURL(blob), hash });
}
}}
onInsertAttachment={(type) => {
const mime = type === "file" ? "*/*" : "image/*";
insertAttachment(mime).then((file) => {
@@ -408,7 +526,6 @@ function EditorChrome(
</AnimatedFlex>
</Flex>
</FlexScrollContainer>
{isMobile && (
<Box
id="editorToolbar"

View File

@@ -155,7 +155,7 @@ export type AttachmentProgress = {
export type Attachment = {
hash: string;
filename: string;
type: string;
mime: string;
size: number;
dataurl?: string;
};
@@ -206,7 +206,7 @@ async function addAttachment(
return {
hash: hash,
filename: file.name,
type: file.type,
mime: file.type,
size: file.size,
dataurl
};

View File

@@ -64,6 +64,7 @@ type TipTapProps = {
onContentChange?: () => void;
onInsertAttachment?: (type: AttachmentType) => void;
onDownloadAttachment?: (attachment: Attachment) => void;
onPreviewAttachment?: (attachment: Attachment) => void;
onAttachFile?: (file: File) => void;
onFocus?: () => void;
content?: string;
@@ -109,6 +110,7 @@ function TipTap(props: TipTapProps) {
onChange,
onInsertAttachment,
onDownloadAttachment,
onPreviewAttachment,
onAttachFile,
onContentChange,
onFocus = () => {},
@@ -253,6 +255,10 @@ function TipTap(props: TipTapProps) {
onDownloadAttachment?.(attachment);
return true;
},
onPreviewAttachment(_editor, attachment) {
onPreviewAttachment?.(attachment);
return true;
},
onOpenLink: (url) => {
window.open(url, "_blank");
return true;

View File

@@ -0,0 +1,151 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
import Field from "../field";
import { Plus, Search } from "../icons";
import { Button, Flex, Text } from "@theme-ui/components";
type FilterableItem = {
id: string;
title: string;
};
type FilteredListProps<T extends FilterableItem> = {
placeholders: { filter: string; empty: string };
items: () => T[];
filter: (items: T[], query: string) => T[];
onCreateNewItem: (title: string) => Promise<void>;
renderItem: (
item: T,
index: number,
refresh: () => void,
isSearching: boolean
) => JSX.Element;
};
export function FilteredList<T extends FilterableItem>(
props: FilteredListProps<T>
) {
const {
items: _items,
filter,
onCreateNewItem,
placeholders,
renderItem
} = props;
const [items, setItems] = useState<T[]>([]);
const [query, setQuery] = useState<string>();
const noItemsFound = items.length <= 0 && query && query.length > 0;
const inputRef = useRef<HTMLInputElement>(null);
const refresh = useCallback(() => {
setItems(_items());
}, [_items]);
useEffect(() => {
refresh();
}, [refresh]);
const _filter = useCallback(
(query) => {
setItems(() => {
const items = _items();
if (!query) {
return items;
}
return filter(items, query);
});
setQuery(query);
},
[_items, filter]
);
const _createNewItem = useCallback(
async (title) => {
await onCreateNewItem(title);
refresh();
setQuery(undefined);
if (inputRef.current) inputRef.current.value = "";
},
[inputRef, refresh, onCreateNewItem]
);
return (
<>
<Field
inputRef={inputRef}
data-test-id={"filter-input"}
autoFocus
placeholder={
items.length <= 0 ? placeholders.empty : placeholders.filter
}
onChange={(e: ChangeEvent) =>
_filter((e.target as HTMLInputElement).value)
}
onKeyUp={async (e: KeyboardEvent) => {
if (e.key === "Enter" && noItemsFound) {
await _createNewItem(query);
}
}}
action={
items.length <= 0
? {
icon: Plus,
onClick: async () => await _createNewItem(query)
}
: { icon: Search, onClick: () => _filter(query) }
}
/>
<Flex
as="ul"
mt={1}
sx={{
overflowY: "hidden",
listStyle: "none",
m: 0,
p: 0,
gap: 1,
display: "flex",
flexDirection: "column"
}}
>
{noItemsFound && (
<Button
variant={"secondary"}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
py: 2
}}
onClick={async () => {
await _createNewItem(query);
}}
>
<Text variant={"body"}>{`Add "${query}"`}</Text>
<Plus size={16} color="primary" />
</Button>
)}
{items.map((item, index) => renderItem(item, index, refresh, !!query))}
</Flex>
</>
);
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState } from "react";
import { memo } from "react";
import MDIIcon from "@mdi/react";
import {
mdiPlus,
@@ -191,7 +191,17 @@ import {
mdiNoteMultipleOutline,
mdiBookMultipleOutline,
mdiArrowTopRight,
mdiBookmarkRemoveOutline
mdiBookmarkRemoveOutline,
mdiFileImageOutline,
mdiFileDocumentOutline,
mdiFileVideoOutline,
mdiWeb,
mdiUploadOutline,
mdiLinkOff,
mdiMagnifyPlusOutline,
mdiMagnifyMinusOutline,
mdiRotateRight,
mdiRotateLeft
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -205,7 +215,7 @@ type MDIIconWrapperProps = {
color?: keyof Theme["colors"];
rotate?: boolean;
};
function MDIIconWrapper({
function _MDIIconWrapper({
title,
path,
size = 24,
@@ -237,12 +247,11 @@ function MDIIconWrapper({
/>
);
}
const MDIIconWrapper = memo(_MDIIconWrapper, () => true);
export type IconProps = FlexProps &
MotionProps &
Omit<MDIIconWrapperProps, "path"> & {
hoverColor?: keyof Theme["colors"];
};
Omit<MDIIconWrapperProps, "path">;
export type Icon = {
(props: IconProps): JSX.Element;
@@ -251,7 +260,6 @@ export type Icon = {
function createIcon(path: string, rotate = false) {
const NNIcon: Icon = function Icon(props) {
const [isHovering, setIsHovering] = useState(false);
const { sx, rotate: _rotate = rotate, size, ...restProps } = props;
return (
<Flex
@@ -262,17 +270,13 @@ function createIcon(path: string, rotate = false) {
alignItems: "center",
flexShrink: 0
}}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<MDIIconWrapper
title={props.title}
path={path}
rotate={_rotate}
size={size}
color={
props.hoverColor && isHovering ? props.hoverColor : props.color
}
color={props.color}
/>
</Flex>
);
@@ -455,6 +459,7 @@ export const Reupload = createIcon(mdiProgressUpload);
export const Rename = createIcon(mdiFormTextbox);
export const Upload = createIcon(mdiCloudOffOutline);
export const Uploaded = createIcon(mdiCloudCheckOutline);
export const Uploading = createIcon(mdiUploadOutline);
export const References = createIcon(mdiVectorLink);
export const Codeblock = createIcon(mdiCodeBraces);
export const Resize = createIcon(mdiArrowCollapseHorizontal);
@@ -479,3 +484,18 @@ export const EditorFullWidth = createIcon(
`M4 20q-.825 0-1.412-.587Q2 18.825 2 18V6q0-.825.588-1.412Q3.175 4 4 4h16q.825 0 1.413.588Q22 5.175 22 6v12q0 .825-.587 1.413Q20.825 20 20 20Zm0-2h1V6H4v12Zm3 0h10V6H7Zm12 0h1V6h-1ZM7 6v12Z`
);
export const Suggestion = createIcon(mdiLightbulbOnOutline);
export const FileImage = createIcon(mdiFileImageOutline);
export const FilePDF = createIcon(
`M14 2l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8m4 18V9h-5V4H6v16h12m-7.08-7.69c-.24-.77-.77-3.23.63-3.27c1.4-.04.48 3.12.48 3.12c.39 1.49 2.02 2.56 2.02 2.56c.5-.15 3.35-.48 2.95 1c-.43 1.48-3.5.09-3.5.09c-1.95.14-3.41.66-3.41.66c-1.13 2.11-2.45 3.03-2.99 2.14c-.67-1.11 2.13-2.54 2.13-2.54c1.45-2.35 1.67-3.72 1.69-3.76m.65.84c-.4 1.3-1.2 2.69-1.2 2.69c.85-.34 2.71-.73 2.71-.73c-1.14-1-1.49-1.95-1.51-1.96m3.14 2.17s1.75.65 1.79.39c.07-.27-1.33-.51-1.79-.39m-5.66 1.49c-.77.3-1.51 1.58-1.33 1.58c.18.01.91-.6 1.33-1.58m2.52-5.55c0-.05.43-1.68 0-1.73c-.3-.03-.01 1.69 0 1.73z`
);
export const FileDocument = createIcon(mdiFileDocumentOutline);
export const FileVideo = createIcon(mdiFileVideoOutline);
export const FileGeneral = createIcon(mdiFileOutline);
export const FileWebClip = createIcon(mdiWeb);
export const Unlink = createIcon(mdiLinkOff);
export const ZoomIn = createIcon(mdiMagnifyPlusOutline);
export const ZoomOut = createIcon(mdiMagnifyMinusOutline);
export const RotateCW = createIcon(mdiRotateRight);
export const RotateACW = createIcon(mdiRotateLeft);
export const Reset = createIcon(mdiRestore);

View File

@@ -0,0 +1,416 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, Image } from "@theme-ui/components";
import React from "react";
import {
Close,
Icon,
Loading,
Reset,
RotateACW,
RotateCW,
ZoomIn,
ZoomOut
} from "../icons";
const DEFAULT_ZOOM_STEP = 0.3;
const DEFAULT_LARGE_ZOOM = 4;
function getXY(e: React.MouseEvent | React.TouchEvent) {
let x = 0;
let y = 0;
if ("touches" in e && e.touches.length) {
x = e.touches[0].pageX;
y = e.touches[0].pageY;
} else if ("pageX" in e && "pageY" in e) {
x = e.pageX;
y = e.pageY;
}
return { x, y };
}
type Image = { url: string; title?: string };
export type LightboxProps = {
image?: string;
// title?: string;
zoomStep?: number;
images?: Image[];
startIndex?: number;
keyboardInteraction?: boolean;
doubleClickZoom?: number;
// showTitle?: boolean;
// buttonAlign?: "flex-end" | "flex-start" | "center";
allowZoom?: boolean;
allowReset?: boolean;
allowRotate?: boolean;
onNavigateImage?: (index: number) => void;
clickOutsideToExit?: boolean;
onClose?: (e: React.MouseEvent | KeyboardEvent) => void;
};
export class Lightbox extends React.Component<LightboxProps> {
initX = 0;
initY = 0;
lastX = 0;
lastY = 0;
_cont = React.createRef<HTMLDivElement>();
state = {
x: 0,
y: 0,
zoom: 1,
rotate: 0,
loading: true,
moving: false,
current: this.props?.startIndex ?? 0,
multi: this.props?.images?.length ? true : false
};
createTransform = (x: number, y: number, zoom: number, rotate: number) =>
`translate3d(${x}px,${y}px,0px) scale(${zoom}) rotate(${rotate}deg)`;
stopSideEffect = (
e: React.KeyboardEvent | React.MouseEvent | KeyboardEvent | MouseEvent
) => e.stopPropagation();
getCurrentImage = () => {
if (!this.state.multi) return this.props.image ?? "";
return this.props.images?.[this.state.current]?.url ?? "";
};
resetZoom = () => this.setState({ x: 0, y: 0, zoom: 1 });
shockZoom = (e: React.MouseEvent) => {
const {
zoomStep = DEFAULT_ZOOM_STEP,
allowZoom = true,
doubleClickZoom = DEFAULT_LARGE_ZOOM
} = this.props;
if (!allowZoom || !doubleClickZoom) return false;
this.stopSideEffect(e);
if (this.state.zoom > 1) return this.resetZoom();
const _z =
(zoomStep < 1 ? Math.ceil(doubleClickZoom / zoomStep) : zoomStep) *
zoomStep;
const _xy = getXY(e);
const _cbr = this._cont.current?.getBoundingClientRect?.();
if (!_cbr) return false;
const _ccx = _cbr.x + _cbr.width / 2;
const _ccy = _cbr.y + _cbr.height / 2;
const x = (_xy.x - _ccx) * -1 * _z;
const y = (_xy.y - _ccy) * -1 * _z;
this.setState({ x, y, zoom: _z });
};
navigateImage = (
direction: "next" | "prev",
e: React.KeyboardEvent | React.MouseEvent | KeyboardEvent | MouseEvent
) => {
if (!this.props.images) return;
this.stopSideEffect(e);
let current = 0;
switch (direction) {
case "next":
current = this.state.current + 1;
break;
case "prev":
current = this.state.current - 1;
break;
}
if (current >= this.props.images.length) current = 0;
else if (current < 0) current = this.props.images.length - 1;
this.setState({ current, x: 0, y: 0, zoom: 1, rotate: 0, loading: true });
if (typeof this.props.onNavigateImage === "function") {
this.props.onNavigateImage(current);
}
};
startMove = (e: React.MouseEvent | React.TouchEvent) => {
if (this.state.zoom <= 1) return false;
this.setState({ moving: true });
const xy = getXY(e);
this.initX = xy.x - this.lastX;
this.initY = xy.y - this.lastY;
};
duringMove = (e: React.MouseEvent | React.TouchEvent) => {
if (!this.state.moving) return false;
const xy = getXY(e);
this.lastX = xy.x - this.initX;
this.lastY = xy.y - this.initY;
this.setState({
x: xy.x - this.initX,
y: xy.y - this.initY
});
};
endMove = () => this.setState({ moving: false });
applyZoom = (type: "in" | "out" | "reset") => {
const { zoomStep = DEFAULT_ZOOM_STEP } = this.props;
switch (type) {
case "in":
this.setState({ zoom: this.state.zoom + zoomStep });
break;
case "out": {
const newZoom = this.state.zoom - zoomStep;
if (newZoom < 1) break;
else if (newZoom === 1) this.setState({ x: 0, y: 0, zoom: 1 });
else this.setState({ zoom: newZoom });
break;
}
case "reset":
this.resetZoom();
break;
}
};
applyRotate = (type: "cw" | "acw") => {
switch (type) {
case "cw":
this.setState({ rotate: this.state.rotate + 90 });
break;
case "acw":
this.setState({ rotate: this.state.rotate - 90 });
break;
}
};
reset = (e: React.MouseEvent | KeyboardEvent) => {
this.stopSideEffect(e);
this.setState({ x: 0, y: 0, zoom: 1, rotate: 0 });
};
exit = (e: React.MouseEvent | KeyboardEvent) => {
if (typeof this.props.onClose === "function") return this.props.onClose(e);
console.error(
"No Exit function passed on prop: onClose. Clicking the close button will do nothing"
);
};
shouldShowReset = () =>
this.state.x ||
this.state.y ||
this.state.zoom !== 1 ||
this.state.rotate !== 0;
canvasClick = (e: React.MouseEvent) => {
const { clickOutsideToExit = true } = this.props;
if (clickOutsideToExit && this.state.zoom <= 1) return this.exit(e);
};
keyboardNavigation = (e: KeyboardEvent) => {
const { allowZoom = true, allowReset = true } = this.props;
const { multi, x, y, zoom } = this.state;
switch (e.key) {
case "ArrowLeft":
if (multi && zoom === 1) this.navigateImage("prev", e);
else if (zoom > 1) this.setState({ x: x - 20 });
break;
case "ArrowRight":
if (multi && zoom === 1) this.navigateImage("next", e);
else if (zoom > 1) this.setState({ x: x + 20 });
break;
case "ArrowUp":
if (zoom > 1) this.setState({ y: y + 20 });
break;
case "ArrowDown":
if (zoom > 1) this.setState({ y: y - 20 });
break;
case "+":
if (allowZoom) this.applyZoom("in");
break;
case "-":
if (allowZoom) this.applyZoom("out");
break;
case "Escape":
if (allowReset && this.shouldShowReset()) this.reset(e);
else this.exit(e);
break;
}
};
componentDidMount() {
document.body.classList.add("lb-open-lightbox");
const { keyboardInteraction = true } = this.props;
if (keyboardInteraction)
document.addEventListener("keyup", this.keyboardNavigation);
}
componentWillUnmount() {
document.body.classList.remove("lb-open-lightbox");
const { keyboardInteraction = true } = this.props;
if (keyboardInteraction)
document.removeEventListener("keyup", this.keyboardNavigation);
}
render() {
const image = this.getCurrentImage();
if (!image) {
console.warn("Not showing lightbox because no image(s) was supplied");
return null;
}
const {
allowZoom = true,
allowRotate = true,
allowReset = true,
onClose
} = this.props;
const { x, y, zoom, rotate, multi, loading, moving } = this.state;
const _reset = allowReset && this.shouldShowReset();
const tools: {
title: string;
icon: Icon;
enabled: boolean;
onClick: (e: React.MouseEvent) => void;
hidden?: boolean;
hideOnMobile?: boolean;
}[] = [
{
title: "Reset",
icon: Reset,
enabled: true,
hidden: !allowReset,
onClick: (e) => this.reset(e)
},
{
title: "Rotate left",
icon: RotateACW,
enabled: true,
hidden: !allowRotate,
onClick: () => this.applyRotate("acw")
},
{
title: "Rotate rigth",
icon: RotateCW,
enabled: true,
hidden: !allowRotate,
onClick: () => this.applyRotate("cw")
},
{
title: "Zoom out",
icon: ZoomOut,
enabled: zoom > 1,
hidden: !allowZoom,
onClick: () => this.applyZoom("out")
},
{
title: "Zoom in",
icon: ZoomIn,
enabled: true,
hidden: !allowZoom,
onClick: () => this.applyZoom("in")
},
{
title: "Close",
icon: Close,
enabled: !!onClose,
onClick: (e) => this.exit(e)
}
];
return (
<Flex
sx={{
zIndex: 50000,
position: "fixed",
left: 0,
top: 0,
width: "100%",
height: "100%",
bg: "#000000a1",
flexDirection: "column"
}}
>
<Flex
sx={{
justifyContent: "flex-end",
zIndex: 10
}}
>
<Flex
bg="bgSecondary"
sx={{
borderRadius: "0px 0px 0px 5px",
overflow: "hidden",
alignItems: "center",
justifyContent: "flex-end"
}}
>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
variant="tool"
bg="transparent"
title={tool.title}
key={tool.title}
sx={{
borderRadius: 0,
display: [
tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
color: tool.enabled ? "text" : "disabled",
cursor: tool.enabled ? "pointer" : "not-allowed",
flexDirection: "row",
flexShrink: 0,
alignItems: "center"
}}
onClick={tool.onClick}
>
<tool.icon
size={18}
color={tool.enabled ? "text" : "disabled"}
/>
</Button>
))}
</Flex>
</Flex>
<Flex
sx={{
flex: 1,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
maxWidth: "100%",
maxHeight: "100%",
position: "relative"
}}
ref={this._cont}
onClick={(e) => this.canvasClick(e)}
>
{loading ? <Loading color="static" size={60} /> : null}
<Image
draggable="false"
sx={{
transform: this.createTransform(x, y, zoom, rotate),
cursor: zoom > 1 ? "grab" : "unset",
transition: moving ? "none" : "all 0.1s",
maxWidth: "80vw",
maxHeight: "80vh",
minWidth: "100px",
minHeight: "100px",
backgroundSize: "50px",
transformOrigin: "center center"
}}
onMouseDown={(e) => this.startMove(e)}
onTouchStart={(e) => this.startMove(e)}
onMouseMove={(e) => this.duringMove(e)}
onTouchMove={(e) => this.duringMove(e)}
onMouseUp={() => this.endMove()}
onMouseLeave={() => this.endMove()}
onTouchEnd={() => this.endMove()}
onClick={(e) => this.stopSideEffect(e)}
onDoubleClick={(e) => this.shockZoom(e)}
onLoad={() => this.setState({ loading: false })}
src={image}
/>
</Flex>
</Flex>
);
}
}

View File

@@ -39,19 +39,20 @@ import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigat
import { AnimatedFlex } from "../animated";
import { domAnimation, LazyMotion } from "framer-motion";
const CustomScrollbarsVirtualList = forwardRef<HTMLDivElement, ScrollerProps>(
function CustomScrollbarsVirtualList(props, ref) {
return (
<ScrollContainer
{...props}
forwardedRef={(sRef) => {
if (typeof ref === "function") ref(sRef);
else if (ref) ref.current = sRef;
}}
/>
);
}
);
export const CustomScrollbarsVirtualList = forwardRef<
HTMLDivElement,
ScrollerProps
>(function CustomScrollbarsVirtualList(props, ref) {
return (
<ScrollContainer
{...props}
forwardedRef={(sRef) => {
if (typeof ref === "function") ref(sRef);
else if (ref) ref.current = sRef;
}}
/>
);
});
type ListContainerProps = {
type: keyof typeof ListProfiles;
@@ -113,7 +114,7 @@ function ListContainer(props: ListContainerProps) {
element.focus()
);
},
skip: (index) => items[index].type === "header",
skip: (index) => !items[index] || items[index].type === "header",
open: (index) => {
const item = items[index];
if (!item || !listRef.current) return;

View File

@@ -23,7 +23,6 @@ import Notebook from "../notebook";
import Tag from "../tag";
import Topic from "../topic";
import TrashItem from "../trash-item";
import Attachment from "../attachment";
import { db } from "../../common/db";
import { getTotalNotes } from "../../common";
import Reminder from "../reminder";
@@ -111,10 +110,6 @@ const TrashProfile: ItemWrapper = ({ index, item, type }) => (
<TrashItem index={index} item={item} date={getDate(item, type)} />
);
const AttachmentProfile: ItemWrapper = ({ index, item }) => (
<Attachment index={index} item={item} isCompact={false} />
);
export const ListProfiles = {
home: NotesProfile,
notebooks: NotebooksProfile,
@@ -122,8 +117,7 @@ export const ListProfiles = {
reminders: RemindersProfile,
tags: TagsProfile,
topics: TopicsProfile,
trash: TrashProfile,
attachments: AttachmentProfile
trash: TrashProfile
} as const;
function getTags(item: Item) {
@@ -191,8 +185,6 @@ function getReminder(noteId: string) {
}
function getDate(item: Item, groupType: keyof typeof ListProfiles) {
if (groupType === "attachments") return item.dateCreated;
const sortBy = db.settings?.getGroupOptions(groupType).sortBy;
switch (sortBy) {
case "dateEdited":

View File

@@ -70,11 +70,11 @@ function ListItem(props) {
const isMenuTarget = target && target === listItemRef.current;
const isSelected = useSelectionStore((store) => {
const inInSelection =
const isInSelection =
store.selectedItems.findIndex((item) => props.item.id === item.id) > -1;
return isFocused
? store.selectedItems.length > 1 && inInSelection
: inInSelection;
? store.selectedItems.length > 1 && isInSelection
: isInSelection;
});
return (

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