Compare commits

..

63 Commits

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

* mobile: fix image and file uploads

---------

Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-04 23:02:07 +05:00
Abdullah Atta
0f5ce465c3 web: add logging related to attachments 2024-05-04 22:51:01 +05:00
Abdullah Atta
647ff7ae0e web: prevent overwriting uploaded attachments 2024-05-04 22:51:01 +05:00
Abdullah Atta
45a552adef web: handle errors during uploaded file size check 2024-05-04 22:51:01 +05:00
Abdullah Atta
8e207d787d web: delete file before reuploading 2024-05-04 22:51:01 +05:00
Abdullah Atta
00754593ac web: fix attachment not getting marked as failed on error 2024-05-04 22:51:01 +05:00
Abdullah Atta
b22770f1ae core: do not reset attachment dateUploaded when marking as failed 2024-05-04 22:51:01 +05:00
Abdullah Atta
523da4e91f core: only delete attachment file (not the metadata) on sync conflict 2024-05-04 22:51:01 +05:00
Abdullah Atta
de6c8154a3 web: fix password recovery not working 2024-05-04 22:50:44 +05:00
Abdullah Atta
9ef32e8081 web: do not hide attachment manager for logged out user 2024-05-01 20:14:20 +05:00
Abdullah Atta
5f53593a26 web: do not throw if backup doesn't contain .nnbackup file 2024-05-01 20:12:23 +05:00
Abdullah Atta
1f404944d9 web: re-enable service worker 2024-05-01 16:38:09 +05:00
Ammar Ahmed
23dca36475 mobile: release v3.0.1 2024-05-01 16:34:01 +05:00
Ammar Ahmed
3dd8e20e9a mobile: add logging when loading attachments 2024-05-01 16:29:37 +05:00
Abdullah Atta
8f412f9487 editor: fix padding of react node views on mobile 2024-05-01 16:29:12 +05:00
Abdullah Atta
bf790219db web: bump version to 3.0.1 2024-05-01 16:16:08 +05:00
Ammar Ahmed
c6af310b2d mobile: add notebook search 2024-05-01 16:10:26 +05:00
Abdullah Atta
8d89b5dca9 web: bring back notebook search in move note dialog 2024-05-01 16:10:26 +05:00
Abdullah Atta
fa35244607 editor: update test snapshosts 2024-05-01 16:09:06 +05:00
Abdullah Atta
d96815fa36 ci: remove beta publish workflows 2024-05-01 15:23:43 +05:00
Abdullah Atta
4a9f5b1d8b core: remove .only from trash tests 2024-05-01 15:19:57 +05:00
Ammar Ahmed
43fac79011 mobile: fix moving notebook on ios 2024-05-01 15:18:55 +05:00
Ammar Ahmed
f2c62c8d8b mobile: fix moving notebook on ios 2024-05-01 15:18:55 +05:00
Ammar Ahmed
70c9dd0083 mobile: reduce font size in notebook header 2024-05-01 15:15:37 +05:00
Ammar Ahmed
ae6d8c5f2b mobile: hide breadcrumbs in top level notebooks 2024-05-01 15:15:37 +05:00
Abdullah Atta
38e2250fd1 web: log attachment on failure 2024-05-01 14:55:49 +05:00
Ammar Ahmed
17e2c5a8a2 desktop: fix app icon size for macos 2024-05-01 14:53:32 +05:00
Ammar Ahmed
2e226f0f72 mobile: fix cannot read property 'sortDirection' of undefined 2024-05-01 13:34:48 +05:00
Ammar Ahmed
167c54578b mobile: fix scrolling broken in reminder -> repeat -> month 2024-05-01 13:34:37 +05:00
Abdullah Atta
fb656af5ca web: sort tags by title in editor header 2024-05-01 13:34:20 +05:00
Ammar Ahmed
ddfab272d2 common: sort tags by title in resolved items 2024-05-01 13:34:20 +05:00
Ammar Ahmed
28958311c1 mobile: fix share extention crash on sharing from some apps 2024-05-01 13:34:06 +05:00
Ammar Ahmed
f1b8275b4b mobile: add support for setting note title form quick note widget 2024-05-01 13:34:06 +05:00
Ammar Ahmed
c8c75d40dd mobile: fix theme search returns empty results 2024-05-01 13:33:50 +05:00
Ammar Ahmed
bace90808d mobile: fix footer size for notebook screen 2024-05-01 13:33:31 +05:00
Ammar Ahmed
fe7c7e8db4 mobile: fix notebook-sheet position 2024-05-01 13:32:02 +05:00
Ammar Ahmed
519a569366 mobile: fix notebook sheet flicker 2024-05-01 13:32:02 +05:00
Ammar Ahmed
89d602a40f mobile: do not lock app when requesting permission 2024-05-01 13:31:38 +05:00
Abdullah Atta
a8db65b5af editor: fix performance issues due to using portals for react node views 2024-05-01 13:29:55 +05:00
Ammar Ahmed
716239abfd mobile: fix selection header 2024-05-01 13:21:31 +05:00
Ammar Ahmed
62dd5a5416 mobile: fix vault unlock with biometrics 2024-05-01 13:19:00 +05:00
Ammar Ahmed
96558c5df5 mobile: fix search bar position on ios 2024-05-01 13:18:21 +05:00
Ammar Ahmed
45997dd2e4 editor: fix scroll into view should bring heading to top 2024-05-01 13:16:54 +05:00
Ammar Ahmed
bde190f01c mobile: render settings icon if no profile pic is set 2024-05-01 13:15:24 +05:00
Ammar Ahmed
042c97688f mobile: add quick theme switch button 2024-05-01 13:15:24 +05:00
Ammar Ahmed
95b5114070 mobile: fix confusing settings/sync button 2024-05-01 13:15:24 +05:00
Ammar Ahmed
20742e2a1c mobile: fix back navigation from editor (#4867)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-01 13:13:44 +05:00
96 changed files with 1297 additions and 1279 deletions

View File

@@ -1,69 +0,0 @@
name: Publish @notesnook/web Beta
on:
workflow_dispatch:
push:
branches:
- master
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
- "apps/desktop/**"
- "apps/vericrypt/**"
- "servers/**"
- "packages/clipper/**"
- "extensions/web-clipper/**"
pull_request:
types:
- closed
- ready_for_review
- reopened
- opened
- synchronize
branches:
- master
- "!v3-beta"
paths-ignore:
- "apps/mobile/**"
- "packages/editor-mobile/**"
- "apps/desktop/**"
- "apps/vericrypt/**"
- "servers/**"
- "packages/clipper/**"
- "extensions/web-clipper/**"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Generate beta build
run: npm run build:beta:web
- name: Publish to Cloudflare Pages
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
id: pages
with:
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-project-name: notesnook-beta-app
directory: ./apps/web/build
github-token: ${{ secrets.GITHUB_TOKEN }}
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}

View File

@@ -1,32 +0,0 @@
name: Publish @notesnook/web v3 Beta
on:
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Build
run: npm run build:web
- name: Publish to Cloudflare Pages
run: npx --yes wrangler pages deploy --project-name=notesnook-v3-beta ./apps/web/build/

Binary file not shown.

View File

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

View File

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

View File

@@ -218,12 +218,7 @@ export default async function downloadAttachment(
}
if (options.base64 || options.text) {
console.log(
"starting decrypt base64 file...",
options.base64,
options.text,
attachment.hash
);
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
options.base64 ? "base64" : "text"
@@ -292,7 +287,7 @@ export default async function downloadAttachment(
return fileUri;
} catch (e) {
console.log("download attachment error: ", e);
DatabaseLogger.error(e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)

View File

@@ -22,21 +22,24 @@ import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { DatabaseLogger, db } from "../database";
import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io";
export async function downloadFile(filename, data, cancelToken) {
if (!data) return false;
if (!data) {
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
return false;
}
console.log("Downloading", filename);
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
let { url, headers } = data;
let path = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
console.log("Exists already", filename);
DatabaseLogger.log(`File Exists already: ${filename}`);
return true;
}
@@ -44,13 +47,24 @@ export async function downloadFile(filename, data, cancelToken) {
method: "GET",
headers
});
if (!res.ok)
if (!res.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
);
throw new Error(`${res.status}: Unable to resolve download url`);
}
const downloadUrl = await res.text();
if (!downloadUrl) throw new Error("Unable to resolve download url");
if (!downloadUrl) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error("Unable to resolve download url");
}
let totalSize = 0;
console.log("Download starting");
DatabaseLogger.log(`Download starting: ${filename}`);
let request = RNFetchBlob.config({
path: path,
IOSBackgroundTask: true
@@ -61,13 +75,15 @@ export async function downloadFile(filename, data, cancelToken) {
.getState()
.setProgress(0, total, filename, recieved, "download");
totalSize = total;
console.log("downloading: ", recieved, total);
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
DatabaseLogger.log(`Download cancelled: ${filename}`);
};
let response = await request;
await fileCheck(response, totalSize);
let status = response.info().status;
@@ -91,7 +107,10 @@ export async function downloadFile(filename, data, cancelToken) {
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("Download file error:", e, url, headers);
DatabaseLogger.error(e, {
url,
headers
});
return false;
}
}
@@ -100,18 +119,17 @@ export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
return 0;
DatabaseLogger.error(e);
return -1;
}
}
@@ -125,7 +143,9 @@ export async function checkAttachment(hash) {
try {
const size = await getUploadedFileSize(hash);
if (size <= 0) return { failed: "File length is 0." };
if (size === -1) return { success: true };
if (size === 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e?.message };
}

View File

@@ -21,12 +21,12 @@ import Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { IOS_APPGROUPID } from "../../utils/constants";
import { db } from "../database";
import { DatabaseLogger, db } from "../database";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
console.log("Read encrypted file...");
DatabaseLogger.log("Read encrypted file...");
let path = `${cacheDir}/${filename}`;
try {
@@ -34,8 +34,6 @@ export async function readEncrypted(filename, key, cipherData) {
return false;
}
const attachment = await db.attachments.attachment(filename);
console.log("decrypting....");
let output = await Sodium.decryptFile(
key,
{
@@ -45,12 +43,13 @@ export async function readEncrypted(filename, key, cipherData) {
},
cipherData.outputType === "base64" ? "base64" : "text"
);
console.log("file decrypted...", attachment?.mimeType);
DatabaseLogger.log("File decrypted...");
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
console.log("readEncrypted", e);
DatabaseLogger.error(e);
return false;
}
}
@@ -128,7 +127,7 @@ export async function clearFileStorage() {
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
console.log("Cache directory created");
DatabaseLogger.log("Cache directory created");
}
}
@@ -188,6 +187,9 @@ export async function exists(filename) {
);
if (stat.size !== expectedFileSize) {
DatabaseLogger.log(
`File size mismatch: ${filename}, expected: ${expectedFileSize}, actual: ${stat.size}`
);
RNFetchBlob.fs
.unlink(existsInAppGroup ? appGroupPath : path)
.catch(console.log);

View File

@@ -25,6 +25,7 @@ import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
import { createCacheDir } from "./io";
import { getUploadedFileSize } from "./download";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
@@ -33,6 +34,18 @@ export async function uploadFile(filename, data, cancelToken) {
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
DatabaseLogger.log("Upload verification failed.");
return false;
}
const isUploaded = uploadedFileSize !== 0;
if (isUploaded) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, {
method: "PUT",
headers
@@ -50,7 +63,15 @@ export async function uploadFile(filename, data, cancelToken) {
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(uploadFilePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
let request = RNFetchBlob.config({

View File

@@ -56,6 +56,7 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import SheetProvider from "../sheet-provider";
const Actions = ({
attachment,
@@ -218,6 +219,7 @@ const Actions = ({
}}
>
<Dialog context={contextId} />
<SheetProvider context={contextId} />
<View
style={{
borderBottomWidth: 1,
@@ -238,36 +240,20 @@ const Actions = ({
style={{
flexDirection: "row",
marginBottom: 10,
paddingHorizontal: 12
paddingHorizontal: 12,
marginTop: 6,
gap: 10
}}
>
<Paragraph
size={SIZE.xs}
style={{
marginRight: 10
}}
color={colors.secondary.paragraph}
>
{attachment.type}
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{attachment.mimeType}
</Paragraph>
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{formatBytes(attachment.size)}
</Paragraph>
{notes.length ? (
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs}
color={colors.secondary.paragraph}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{notes.length} note
{notes.length > 1 ? "s" : ""}
</Paragraph>
@@ -366,7 +352,7 @@ const Actions = ({
{failed ? (
<Notice
type="alert"
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
size="small"
/>
) : null}

View File

@@ -177,7 +177,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
case "audio":
items = note
? db.attachments.ofNote(note.id, "audio")
: db.attachments.all;
: db.attachments.audios;
break;
case "documents":
items = note

View File

@@ -24,7 +24,7 @@ import {
setAppLockVerificationCipher,
validateAppLockPassword
} from "../../../common/database/encryption";
import BiometicService from "../../../services/biometrics";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
@@ -323,7 +323,7 @@ export const AppLockPassword = () => {
SettingsService.setProperty("appLockHasPasswordSecurity", false);
if (
!(await BiometicService.isBiometryAvailable()) ||
!(await BiometricService.isBiometryAvailable()) ||
SettingsService.getProperty("biometricsAuthEnabled") === false
) {
SettingsService.setProperty("appLockEnabled", false);

View File

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

View File

@@ -128,7 +128,7 @@ export class VaultDialog extends Component {
: this.state.copyNote
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.goToEditor
? "Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault."
? "Unlock note to open it in editor."
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
}

View File

@@ -33,6 +33,7 @@ import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
import { useNavigation } from "@react-navigation/native";
type HeaderRightButton = {
title: string;
@@ -66,6 +67,8 @@ export const Header = ({
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const navigation = useNavigation();
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [borderHidden, setBorderHidden] = useState(true);

View File

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

View File

@@ -19,6 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { View } from "react-native";
import React from "react";
export const Footer = () => {
return <View style={{ height: 150 }} />;
export const Footer = ({ height = 150 }) => {
return <View style={{ height: height }} />;
};

View File

@@ -95,7 +95,7 @@ export const NotebookHeader = ({
style={{
flexShrink: 1
}}
size={SIZE.xxl}
size={SIZE.lg}
>
{notebook.title}
</Heading>
@@ -114,7 +114,7 @@ export const NotebookHeader = ({
width: 40,
height: 40
}}
type={isPinnedToMenu ? "secondary" : "secondary"}
type="transparent"
color={isPinnedToMenu ? colors.primary.accent : colors.primary.icon}
size={SIZE.lg}
/>
@@ -123,7 +123,7 @@ export const NotebookHeader = ({
onPress={onEditNotebook}
tooltipText="Edit this notebook"
name="pencil"
type="secondary"
type="transparent"
color={colors.primary.icon}
style={{
width: 40,
@@ -134,7 +134,7 @@ export const NotebookHeader = ({
</View>
{notebook.description ? (
<Paragraph size={SIZE.md} color={colors.primary.paragraph}>
<Paragraph size={SIZE.sm} color={colors.primary.paragraph}>
{notebook.description}
</Paragraph>
) : null}

View File

@@ -192,7 +192,9 @@ export default function List(props: ListProps) {
/>
) : null
}
ListFooterComponent={<Footer />}
ListFooterComponent={
<Footer height={props.renderedInRoute === "Notebook" ? 300 : 150} />
}
ListHeaderComponent={
<>
{props.CustomLisHeader ? (

View File

@@ -19,14 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import React, { useEffect, useRef } from "react";
import {
BackHandler,
NativeEventSubscription,
Platform,
View
} from "react-native";
import Animated, { FadeInUp } from "react-native-reanimated";
import Menu from "react-native-reanimated-material-menu/src/Menu";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
@@ -42,9 +42,10 @@ import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import ManageTagsSheet from "../sheets/manage-tags";
import { MoveNotebookSheet } from "../sheets/move-notebook";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import { MoveNotebookSheet } from "../sheets/move-notebook";
export const SelectionHeader = React.memo(
({
@@ -58,6 +59,8 @@ export const SelectionHeader = React.memo(
type?: ItemType;
renderedInRoute?: string;
}) => {
const menuRef = useRef<Menu>(null);
const { colors: contextMenuColors } = useThemeColors("contextMenu");
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore(
@@ -132,8 +135,7 @@ export const SelectionHeader = React.memo(
}, [clearSelection, selectionMode]);
return selectionMode !== type || focusedRouteId !== id ? null : (
<Animated.View
entering={FadeInUp}
<View
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
@@ -143,8 +145,7 @@ export const SelectionHeader = React.memo(
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
paddingHorizontal: 12
}}
>
<View
@@ -164,18 +165,15 @@ export const SelectionHeader = React.memo(
borderRadius: 100,
marginRight: 10
}}
type="secondary"
onPress={() => {
clearSelection();
}}
size={SIZE.xl}
color={colors.primary.icon}
name="close"
/>
<View
style={{
backgroundColor: colors.secondary.background,
height: 40,
borderRadius: 100,
paddingHorizontal: 16,
@@ -184,11 +182,12 @@ export const SelectionHeader = React.memo(
alignItems: "center"
}}
>
<Heading size={SIZE.md} color={colors.primary.accent}>
<Heading size={SIZE.lg} color={colors.primary.paragraph}>
{selectedItemsList.length}
</Heading>
</View>
</View>
<View
style={{
flexDirection: "row",
@@ -211,159 +210,142 @@ export const SelectionHeader = React.memo(
allSelected ? colors.primary.accent : colors.primary.paragraph
}
name="select-all"
size={SIZE.xl}
/>
{renderedInRoute === "Notebooks" && selectedItemsList.length ? (
<IconButton
{selectedItemsList.length ? (
<Menu
ref={menuRef}
animationDuration={200}
style={{
marginLeft: 10
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: -20
}}
onPress={async () => {
const ids = selectedItemsList;
const notebooks = await db.notebooks.all.items(ids);
MoveNotebookSheet.present(notebooks);
onRequestClose={() => {
menuRef.current?.hide();
}}
tooltipText="Move notebooks"
tooltipPosition={1}
name="arrow-right-bold-box-outline"
size={SIZE.xl}
/>
) : null}
{type !== "note" || !selectedItemsList.length ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
style={{
marginLeft: 10
}}
color={colors.primary.icon}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
}}
tooltipText="Export"
tooltipPosition={4}
style={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="export"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
MoveNoteSheet.present();
}}
style={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
tooltipPosition={4}
color={colors.primary.paragraph}
name="plus"
size={SIZE.xl}
/>
</>
)}
{renderedInRoute === "Notebook" && selectedItemsList.length ? (
<IconButton
onPress={async () => {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
}}
style={{
marginLeft: 10
}}
tooltipText={`Remove from Notebook`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
name="minus"
size={SIZE.xl}
/>
) : null}
{focusedRouteId === "Favorites" && selectedItemsList.length ? (
<IconButton
onPress={addToFavorite}
style={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.primary.paragraph}
name="star-off"
size={SIZE.xl}
/>
) : null}
{type === "trash" || !selectedItemsList.length ? null : (
<IconButton
style={{
marginLeft: 10
}}
onPress={() => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
}}
tooltipText="Move to trash"
tooltipPosition={1}
color={colors.primary.paragraph}
name="delete"
size={SIZE.xl}
/>
)}
{type === "trash" && selectedItemsList.length ? (
<>
<IconButton
style={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
style={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
anchor={
<IconButton
onPress={() => {
menuRef.current?.show();
}}
name="dots-vertical"
color={colors.primary.paragraph}
/>
}
>
{[
{
title:
selectedItemsList.length > 1
? "Move notebooks"
: "Move notebook",
onPress: async () => {
const ids = selectedItemsList;
const notebooks = await db.notebooks.all.items(ids);
MoveNotebookSheet.present(notebooks);
},
visible: renderedInRoute === "Notebooks",
icon: "arrow-right-bold-box-outline"
},
{
title: "Manage tags",
onPress: async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"
},
{
title: "Export",
onPress: async () => {
await sleep(100);
ExportNotesSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "export"
},
{
title: "Link notebook",
onPress: async () => {
await sleep(100);
MoveNoteSheet.present();
},
visible: type === "note",
icon: "plus"
},
{
title: "Unlink notebook",
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
},
visible: renderedInRoute === "Notebook",
icon: "minus"
},
{
title: "Unfavorite",
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: `Move to trash`,
onPress: async () => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
},
visible: type !== "trash",
icon: "delete"
},
{
title: `Restore`,
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: `Delete`,
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
}
].map((item) =>
!item.visible ? null : (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0
}}
type="plain"
buttonType={{
text: contextMenuColors.primary.paragraph
}}
icon={item.icon}
key={item.title}
title={item.title}
onPress={async () => {
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();
}}
/>
)
)}
</Menu>
) : null}
</View>
</Animated.View>
</View>
);
}
);

View File

@@ -45,14 +45,16 @@ export const AddNotebookSheet = ({
notebook,
parentNotebook,
close,
showMoveNotesOnComplete
showMoveNotesOnComplete,
defaultTitle
}: {
notebook?: Notebook;
parentNotebook?: Notebook;
close?: (didAddNotebook: boolean) => void;
showMoveNotesOnComplete: boolean;
defaultTitle?: string;
}) => {
const title = useRef(notebook?.title);
const title = useRef(notebook?.title || defaultTitle);
const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null);
const descriptionInput = useRef<TextInput>(null);
@@ -139,7 +141,7 @@ export const AddNotebookSheet = ({
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : ""}
defaultValue={notebook ? notebook.title : title.current}
/>
<Input
@@ -174,7 +176,8 @@ AddNotebookSheet.present = (
parentNotebook?: Notebook,
context?: string,
onClose?: (didAddNotebook: boolean) => void,
showMoveNotesOnComplete = true
showMoveNotesOnComplete = true,
defaultTitle?: string
) => {
presentSheet({
context: context,
@@ -187,6 +190,7 @@ AddNotebookSheet.present = (
onClose?.(didAddNotebook);
}}
showMoveNotesOnComplete={showMoveNotesOnComplete || false}
defaultTitle={defaultTitle}
/>
)
});

View File

@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Note } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback, useEffect } from "react";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import {
ActivityIndicator,
Keyboard,
@@ -47,6 +53,8 @@ import Paragraph from "../../ui/typography/paragraph";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
import { AddNotebookSheet } from "../add-notebook";
import Input from "../../ui/input";
import { presentDialog } from "../../dialog/functions";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
@@ -93,7 +101,11 @@ const MoveNoteSheet = ({
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const [notebooks, loading] = useNotebooks();
const [rootNotebooks, loading] = useNotebooks();
const searchQuery = useRef("");
const searchTimer = useRef<NodeJS.Timeout>();
const [notebooks, setNotebooks] = useState(rootNotebooks);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
@@ -103,6 +115,12 @@ const MoveNoteSheet = ({
(state) => state.multiSelect
);
useEffect(() => {
if (!loading) {
setNotebooks(rootNotebooks);
}
}, [loading, rootNotebooks]);
useEffect(() => {
const items = note ? [note.id] : selectedItemsList;
updateInitialSelectionState(items);
@@ -199,7 +217,6 @@ const MoveNoteSheet = ({
{hasSelected() ? (
<IconButton
name="restore"
type="secondaryAccented"
color={colors.primary.icon}
onPress={() => {
const items = note ? [note.id] : selectedItemsList;
@@ -239,33 +256,48 @@ const MoveNoteSheet = ({
style={{
width: "100%"
}}
keyboardShouldPersistTaps="handled"
ListHeaderComponent={
<View
style={{
paddingHorizontal: 12,
width: "100%"
width: "100%",
paddingTop: 12
}}
>
<Button
title="Add new notebook"
style={{
alignSelf: "flex-start",
paddingHorizontal: 12,
justifyContent: "space-between"
<Input
placeholder="Search notebooks...."
button={{
icon: "plus",
onPress: () => {
AddNotebookSheet.present(
undefined,
undefined,
"link-notebooks",
undefined,
false,
searchQuery.current
);
},
color: colors.primary.icon
}}
onPress={() => {
AddNotebookSheet.present(
undefined,
undefined,
"link-notebooks",
undefined,
false
);
onChangeText={(value) => {
searchQuery.current = value;
if (!searchQuery.current) {
setNotebooks(rootNotebooks);
return;
}
searchTimer.current = setTimeout(() => {
db.lookup
.notebooks(searchQuery.current)
.sorted()
.then((result) => {
if (searchQuery.current === value) {
setNotebooks(result);
}
});
}, 300);
}}
icon="plus"
iconPosition="right"
type="secondaryAccented"
width="100%"
/>
</View>
}

View File

@@ -96,10 +96,12 @@ export default function Migrate() {
});
setLoading(true);
await sleep(1);
const { error } = await BackupService.run(false, "local");
const { error, report } = await BackupService.run(false, "local");
if (error) {
ToastManager.error(error, "Backup failed");
reportError(error);
if (report) {
reportError(error);
}
setLoading(false);
return;
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import create from "zustand";
@@ -72,6 +72,20 @@ export const MoveNotebookSheet = ({
}) => {
const [notebooks] = useNotebooks();
const { colors } = useThemeColors();
const [moveToTop, setMoveToTop] = useState(false);
useEffect(() => {
(async () => {
for (const notebook of selectedNotebooks) {
const root = await findRootNotebookId(notebook.id);
if (root !== notebook.id) {
setMoveToTop(true);
return;
}
}
})();
}, [selectedNotebooks]);
const renderItem = useCallback(
({ index }: { index: number }) => {
return (
@@ -93,6 +107,7 @@ export const MoveNotebookSheet = ({
: selectedNotebooks[0].title
} to ${selectedNotebook.title}?`,
positiveText: "Move",
context: "move-notebook",
positivePress: async () => {
for (const notebook of selectedNotebooks) {
const parent = await getParentNotebookId(notebook.id);
@@ -157,36 +172,38 @@ export const MoveNotebookSheet = ({
paddingHorizontal: 12
}}
>
<Button
title="Move to top"
style={{
alignSelf: "flex-start",
width: "100%",
justifyContent: "space-between"
}}
icon="arrow-up-bold"
iconPosition="right"
type="secondaryAccented"
onPress={async () => {
for (const notebook of selectedNotebooks) {
const parent = await getParentNotebookId(notebook.id);
const root = await findRootNotebookId(notebook.id);
if (root !== notebook.id) {
await db.relations.unlink(
{
type: "notebook",
id: parent
},
notebook
);
eSendEvent(eOnNotebookUpdated, parent);
eSendEvent(eOnNotebookUpdated, notebook.id);
{moveToTop ? (
<Button
title="Move to top"
style={{
alignSelf: "flex-start",
width: "100%",
justifyContent: "space-between"
}}
icon="arrow-up-bold"
iconPosition="right"
type="secondaryAccented"
onPress={async () => {
for (const notebook of selectedNotebooks) {
const parent = await getParentNotebookId(notebook.id);
const root = await findRootNotebookId(notebook.id);
if (root !== notebook.id) {
await db.relations.unlink(
{
type: "notebook",
id: parent
},
notebook
);
eSendEvent(eOnNotebookUpdated, parent);
eSendEvent(eOnNotebookUpdated, notebook.id);
}
}
}
useNotebookStore.getState().refresh();
close?.();
}}
/>
useNotebookStore.getState().refresh();
close?.();
}}
/>
) : null}
</View>
}
ListEmptyComponent={

View File

@@ -19,13 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { RefreshControl, View, useWindowDimensions } from "react-native";
import {
Platform,
RefreshControl,
View,
useWindowDimensions
} from "react-native";
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Config from "react-native-config";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create from "zustand";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { MMKV } from "../../../common/database/mmkv";
import { useNotebook } from "../../../hooks/use-notebook";
import NotebookScreen from "../../../screens/notebook";
@@ -43,9 +49,8 @@ import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import Sort from "../sort";
import { MoveNotebookSheet } from "../move-notebook";
import { db } from "../../../common/database";
import Sort from "../sort";
const useItemSelectionStore = createItemSelectionStore(true, false);
@@ -63,7 +68,8 @@ class NotebookSheetConfig {
}
static get(item: ConfigItem) {
return MMKV.getInt(NotebookSheetConfig.makeId(item)) || 0;
const value = MMKV.getInt(NotebookSheetConfig.makeId(item));
return typeof value === "number" ? value : 0;
}
static set(item: ConfigItem, index = 0) {
@@ -126,11 +132,6 @@ export const NotebookSheet = () => {
if (!focusedRouteId) return;
const nextRoot = await findRootNotebookId(focusedRouteId);
if (nextRoot !== currentItem.current) {
console.log(
"NotebookSheet.useEffect.canShow",
"Root changed to",
nextRoot
);
useItemSelectionStore.setState({
enabled: false,
selection: {}
@@ -145,11 +146,8 @@ export const NotebookSheet = () => {
if (ref.current?.isOpen()) {
ref.current?.snapToIndex(snapPoint);
} else {
setTimeout(() => {
ref.current?.show(snapPoint);
}, 150);
ref.current?.show(snapPoint);
}
console.log("NotebookSheet.useEffect.didShow", focusedRouteId);
setRoot(nextRoot);
onRequestUpdate();
});
@@ -162,7 +160,7 @@ export const NotebookSheet = () => {
ref.current?.hide();
}
}
}, [canShow, onRequestUpdate, focusedRouteId]);
}, [canShow, focusedRouteId]);
return (
<ActionSheet
@@ -198,7 +196,11 @@ export const NotebookSheet = () => {
backgroundColor: colors.secondary.background
}}
keyboardHandlerEnabled={false}
snapPoints={Config.isTesting === "true" ? [100] : [20, 100]}
snapPoints={
Config.isTesting === "true"
? [100]
: [Platform.OS === "android" ? 15 : 10, 100]
}
initialSnapIndex={1}
backgroundInteractionEnabled
gestureEnabled
@@ -237,7 +239,7 @@ export const NotebookSheet = () => {
>
<Icon
name="notebook-plus"
color={colors.primary.accent}
color={colors.primary.icon}
size={SIZE.xxl}
/>
</View>

View File

@@ -18,7 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useRef, useState } from "react";
import { Platform, TextInput, View } from "react-native";
import {
Platform,
TextInput,
View,
ScrollView as RNScrollView
} from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import {
@@ -364,7 +369,7 @@ export default function ReminderSheet({
))}
</View>
<ScrollView showsHorizontalScrollIndicator={false} horizontal>
<RNScrollView showsHorizontalScrollIndicator={false} horizontal>
{recurringMode === RecurringModes.Daily ||
recurringMode === RecurringModes.Year
? null
@@ -425,7 +430,7 @@ export default function ReminderSheet({
}}
/>
))}
</ScrollView>
</RNScrollView>
</View>
) : null}

View File

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

View File

@@ -50,7 +50,7 @@ const Sort = ({ type, screen }) => {
const setOrderBy = async () => {
let _groupOptions = {
...groupOptions,
sortDirection: groupOptions.sortDirection === "asc" ? "desc" : "asc"
sortDirection: groupOptions?.sortDirection === "asc" ? "desc" : "asc"
};
if (type === "topics") {
_groupOptions.groupBy = "none";
@@ -85,7 +85,7 @@ const Sort = ({ type, screen }) => {
<Button
title={
groupOptions.sortDirection === "asc"
groupOptions?.sortDirection === "asc"
? groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
? "A - Z"
@@ -100,7 +100,7 @@ const Sort = ({ type, screen }) => {
: "New - Old"
}
icon={
groupOptions.sortDirection === "asc"
groupOptions?.sortDirection === "asc"
? "sort-ascending"
: "sort-descending"
}
@@ -229,7 +229,7 @@ const Sort = ({ type, screen }) => {
if (item === "abc") {
_groupOptions.sortBy = "title";
_groupOptions.sortDirection = "asc";
_groupOptions.sortDirection = "desc";
}
updateGroupOptions(_groupOptions);

View File

@@ -20,29 +20,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import { useNetInfo } from "@react-native-community/netinfo";
import React from "react";
import {
ActivityIndicator,
Image,
Platform,
TouchableOpacity,
View
} from "react-native";
import { ActivityIndicator, Image, Platform, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useSyncProgress from "../../hooks/use-sync-progress";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
import { useThemeStore } from "../../stores/use-theme-store";
import { SyncStatus, useUserStore } from "../../stores/use-user-store";
import { eOpenLoginDialog } from "../../utils/events";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import { TimeSince } from "../ui/time-since";
import Paragraph from "../ui/typography/paragraph";
import Navigation from "../../services/navigation";
export const UserStatus = () => {
const { colors } = useThemeColors();
const { colors, isDark } = useThemeColors();
const user = useUserStore((state) => state.user);
const syncing = useUserStore((state) => state.syncing);
const lastSyncStatus = useUserStore((state) => state.lastSyncStatus);
@@ -97,7 +93,16 @@ export const UserStatus = () => {
borderRadius: 100
}}
/>
) : null}
) : (
<Icon
name="cog-outline"
size={SIZE.lg - 2}
color={colors.secondary.icon}
style={{
paddingLeft: 8
}}
/>
)}
<View
style={{
@@ -110,14 +115,8 @@ export const UserStatus = () => {
size={SIZE.sm}
color={colors.primary.heading}
>
{!user
? "Login to sync your notes."
: lastSyncStatus === SyncStatus.Failed
? "Sync failed, tap to retry"
: syncing
? `Syncing ${progress ? `(${progress.current})` : ""}`
: !userProfile?.fullName
? "Tap to sync"
{!user || !userProfile?.fullName
? "Settings"
: userProfile.fullName}
</Paragraph>
@@ -132,16 +131,20 @@ export const UserStatus = () => {
"Not logged in"
) : lastSynced && lastSynced !== "Never" ? (
<>
{lastSyncStatus === SyncStatus.Failed
{syncing
? `Syncing ${progress ? `(${progress.current})` : ""}`
: lastSyncStatus === SyncStatus.Failed
? "Sync failed"
: "Synced"}{" "}
<TimeSince
style={{
fontSize: SIZE.xs,
color: colors.secondary.paragraph
}}
time={lastSynced}
/>
{!syncing ? (
<TimeSince
style={{
fontSize: SIZE.xs,
color: colors.secondary.paragraph
}}
time={lastSynced}
/>
) : null}
{isOffline ? " (offline)" : ""}
</>
) : (
@@ -149,7 +152,7 @@ export const UserStatus = () => {
)}{" "}
<Icon
name="checkbox-blank-circle"
size={11}
size={9}
allowFontScaling
color={
!user || lastSyncStatus === SyncStatus.Failed
@@ -162,56 +165,80 @@ export const UserStatus = () => {
</Paragraph>
</View>
<Pressable
<View
style={{
borderRadius: 100,
width: 40,
height: 40
}}
hitSlop={{
top: 10,
bottom: 10,
left: 20
}}
onPress={() => {
if (user) {
Sync.run();
} else {
tabBarRef.current?.closeDrawer();
eSendEvent(eOpenLoginDialog);
}
flexDirection: "row",
gap: 0
}}
>
{user ? (
syncing ? (
<ActivityIndicator
color={colors.primary.accent}
size={SIZE.xl}
/>
) : lastSyncStatus === SyncStatus.Failed ? (
<Icon
color={colors.error.icon}
name="sync-alert"
size={SIZE.lg}
allowFontScaling
/>
<IconButton
hitSlop={{
top: 10,
bottom: 10
}}
onPress={() => {
useThemeStore.getState().setColorScheme();
}}
name="theme-light-dark"
color={isDark ? colors.primary.accent : colors.primary.icon}
size={SIZE.lg}
style={{
borderRadius: 100,
width: 40,
height: 40
}}
/>
<Pressable
style={{
borderRadius: 100,
width: 40,
height: 40
}}
hitSlop={{
top: 10,
bottom: 10
}}
onPress={() => {
if (user) {
Sync.run();
} else {
tabBarRef.current?.closeDrawer();
eSendEvent(eOpenLoginDialog);
}
}}
>
{user ? (
syncing ? (
<ActivityIndicator
color={colors.primary.accent}
size={SIZE.xl}
/>
) : lastSyncStatus === SyncStatus.Failed ? (
<Icon
color={colors.error.icon}
name="sync-alert"
size={SIZE.lg}
allowFontScaling
/>
) : (
<Icon
allowFontScaling
color={colors.primary.icon}
name="sync"
size={SIZE.lg}
/>
)
) : (
<Icon
allowFontScaling
color={colors.primary.accent}
name="sync"
size={SIZE.lg}
name="login"
/>
)
) : (
<Icon
allowFontScaling
color={colors.primary.accent}
size={SIZE.lg}
name="login"
/>
)}
</Pressable>
)}
</Pressable>
</View>
</Pressable>
</View>
</View>

View File

@@ -19,35 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [
{
title: "Bi-directional note linking",
body: "Now you can link notes to each other in both directions."
},
{
title: "Tabs",
body: "Tabs allow you to have multiple notes open at the same time. You can switch between them easily."
},
{
title: "Nested notebooks",
body: "You can now create notebooks inside other notebooks."
},
{
title: "At-rest encryption",
body: "Your notes are now encrypted when stored on the device."
},
{
title: "Material You themed icon",
body: "The app now has a Material You themed icon that changes color based on the wallpaper.",
platform: "android"
},
{
title: "New note quick settings tile",
body: "You can now add a new note tile to quickly create notes from quick settings panel.",
platform: "android"
},
{
title: "And so much more...",
body: "V3 packs so much that it's hard to put everything here, check out our blog at Notesnook V3 is here, and it's packed with new features and improvements such as bi-directional note linking, better syncing, at-rest encryption, nested notebooks, editor tabs, better app lock and so much more. https://blog.notesnook.com/introducing-notesnook-v3."
}
];
export const features: FeatureType[] = [];

View File

@@ -85,7 +85,6 @@ import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants";
import {
eClearEditor,
eCloseSheet,
eLoginSessionExpired,
eOnLoadNote,
@@ -532,12 +531,6 @@ export const useAppEvents = () => {
//@ts-ignore
globalThis["IS_SHARE_EXTENSION"] = false;
} else {
const id = useTabStore.getState().getCurrentNoteId();
const note = id ? await db.notes.note(id) : undefined;
const locked = note && (await db.vaults.itemExists(note));
if (locked && SettingsService.canLockAppInBackground()) {
eSendEvent(eClearEditor);
}
await saveEditorState();
if (
SettingsService.canLockAppInBackground() &&

View File

@@ -71,12 +71,10 @@ export const useDBItem = <T extends keyof ItemTypeKey>(
if (typeof itemId === "string" && itemId !== itemIdRef.current) return;
if (!isValidIdOrIndex(idOrIndex)) return;
console.log("useDBItem.onUpdateItem", idOrIndex, type);
if (items && typeof idOrIndex === "number") {
items.item(idOrIndex).then((item) => {
setItem(item.item);
itemIdRef.current = item.item.id;
itemIdRef.current = item.item?.id;
});
} else {
if (!(db as any)[type + "s"][type]) {

View File

@@ -36,7 +36,6 @@ export const useNotebook = (
const onRequestUpdate = React.useCallback(() => {
if (!item?.id) return;
console.log("useNotebook.onRequestUpdate", item?.id, Date.now());
const selector = db.relations.from(
{
@@ -59,7 +58,6 @@ export const useNotebook = (
useEffect(() => {
if (nestedNotebooks) {
console.log("useNotebook.useEffect.onRequestUpdate");
onRequestUpdate();
}
}, [item?.id, onRequestUpdate, nestedNotebooks]);

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect } from "react";
import BiometicService from "../services/biometrics";
import BiometricService from "../services/biometrics";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { db } from "../common/database";
@@ -39,8 +39,8 @@ export const useVaultStatus = () => {
const checkVaultStatus = useCallback(() => {
db.vault?.exists().then(async (exists) => {
const available = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
const available = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (
VaultStatusCache.exists === exists &&
VaultStatusCache.biometryEnrolled === fingerprint &&

View File

@@ -32,7 +32,7 @@ import WebView from "react-native-webview";
import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import BiometicService from "../../services/biometrics";
import BiometricService from "../../services/biometrics";
import {
ToastManager,
eSendEvent,
@@ -208,8 +208,8 @@ const useLockedNoteHandler = () => {
useEffect(() => {
(async () => {
const biometry = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
useTabStore.setState({
biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint
@@ -223,9 +223,9 @@ const useLockedNoteHandler = () => {
try {
if (!tabRef.current?.noteLocked || !tabRef.current) return;
console.log("Trying to unlock with biometrics...");
const credentials = await BiometicService.getCredentials(
const credentials = await BiometricService.getCredentials(
"Unlock note",
"Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault."
"Unlock note to open it in editor."
);
if (credentials && credentials?.password && tabRef.current.noteId) {
@@ -233,6 +233,7 @@ const useLockedNoteHandler = () => {
tabRef.current.noteId,
credentials?.password
);
eSendEvent(eOnLoadNote, {
item: note
});
@@ -269,7 +270,7 @@ const useLockedNoteHandler = () => {
try {
const unlocked = await db.vault.unlock(password);
if (!unlocked) throw new Error("Incorrect vault password");
await BiometicService.storeCredentials(password);
await BiometricService.storeCredentials(password);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: "Biometric unlocking enabled!",
@@ -278,8 +279,8 @@ const useLockedNoteHandler = () => {
context: "global"
});
const biometry = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
useTabStore.setState({
biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint

View File

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

View File

@@ -450,7 +450,7 @@ export const useEditorEvents = (
break;
case EventTypes.filepicker:
editorState().isAwaitingResult = true;
const { pick } = require("./picker.js").default;
const { pick } = require("./picker").default;
pick({
type: editorMessage.value,
noteId: noteId,
@@ -469,12 +469,10 @@ export const useEditorEvents = (
case EventTypes.getAttachmentData: {
const attachment = (editorMessage.value as any)
.attachment as Attachment;
?.attachment as Attachment;
console.log(
"Getting attachment data:",
attachment.hash,
attachment.type
DatabaseLogger.log(
`Getting attachment data: ${attachment?.hash} ${attachment?.type}`
);
downloadAttachment(attachment.hash, true, {
base64: attachment.type === "image",
@@ -494,8 +492,8 @@ export const useEditorEvents = (
data
});
})
.catch(() => {
console.log("Error downloading attachment data");
.catch((e) => {
DatabaseLogger.error(e);
editor.postMessage(EditorEvents.attachmentData, {
resolverId: (editorMessage.value as any).resolverId,
data: undefined

View File

@@ -32,6 +32,7 @@ import { openEditor } from "../notes/common";
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const [notes, loading] = useNotes();
const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => {
Navigation.routeNeedsUpdate(

View File

@@ -16,40 +16,37 @@ 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 { resolveItems } from "@notesnook/common";
import { VirtualizedGrouping } from "@notesnook/core";
import { Note, Notebook } from "@notesnook/core/dist/types";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import List from "../../components/list";
import { NotebookHeader } from "../../components/list-items/headers/notebook-header";
import SelectionHeader from "../../components/selection-header";
import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { eSendEvent, eSubscribeEvent } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import useNavigationStore, {
NotebookScreenParams
} from "../../stores/use-navigation-store";
import { eUpdateNotebookRoute } from "../../utils/events";
import { findRootNotebookId } from "../../utils/notebooks";
import { openEditor, setOnFirstSave } from "../notes/common";
import SelectionHeader from "../../components/selection-header";
import Paragraph from "../../components/ui/typography/paragraph";
import { View } from "react-native";
import { SIZE } from "../../utils/size";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import { resolveItems } from "@notesnook/common";
import { openEditor, setOnFirstSave } from "../notes/common";
const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const params = useRef<NotebookScreenParams>(route?.params);
const [loading, setLoading] = useState(true);
const updateOnFocus = useRef(false);
const [breadcrumbs, setBreadcrumbs] = useState<
{
id: string;
@@ -59,11 +56,17 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
useNavigationFocus(navigation, {
onFocus: () => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
if (updateOnFocus.current) {
onRequestUpdate();
updateOnFocus.current = false;
} else {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
}
syncWithNavigation();
return false;
},
onBlur: () => {
updateOnFocus.current = false;
setOnFirstSave(null);
return false;
}
@@ -79,6 +82,15 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const onRequestUpdate = React.useCallback(
async (data?: NotebookScreenParams) => {
if (
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
!data
) {
updateOnFocus.current = true;
return;
}
if (data?.item?.id && params.current.item?.id !== data?.item?.id) {
const nextRootNotebookId = await findRootNotebookId(data?.item?.id);
const currentNotebookRoot = await findRootNotebookId(
@@ -104,7 +116,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
if (notebook) {
const breadcrumbs = await db.notebooks.breadcrumbs(notebook.id);
setBreadcrumbs(breadcrumbs);
setBreadcrumbs(breadcrumbs.slice(0, breadcrumbs.length - 1));
params.current.item = notebook;
const notes = await db.relations
.from(notebook, "note")
@@ -122,10 +134,10 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
);
useEffect(() => {
onRequestUpdate();
eSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate);
onRequestUpdate(params.current);
const sub = eSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate);
return () => {
eUnSubscribeEvent(eUpdateNotebookRoute, onRequestUpdate);
sub?.unsubscribe();
};
}, [onRequestUpdate]);
@@ -167,7 +179,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
onPressDefaultRightButton={openEditor}
/>
{breadcrumbs ? (
{breadcrumbs && breadcrumbs.length > 0 ? (
<View
style={{
width: "100%",

View File

@@ -81,16 +81,21 @@ const NotesPage = ({
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined;
const updateOnFocus = useRef(false);
const isFocused = useNavigationFocus(navigation, {
onFocus: (prev) => {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
if (updateOnFocus.current) {
onRequestUpdate();
updateOnFocus.current = false;
} else {
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
}
syncWithNavigation();
if (focusControl) return !prev.current;
return false;
},
onBlur: () => {
updateOnFocus.current = false;
setOnFirstSave(null);
return false;
},
@@ -99,7 +104,6 @@ const NotesPage = ({
const syncWithNavigation = React.useCallback(() => {
const { item } = params.current;
useNavigationStore
.getState()
.setFocusedRouteId(params?.current?.item?.id || route.name);
@@ -113,6 +117,14 @@ const NotesPage = ({
const onRequestUpdate = React.useCallback(
async (data?: NotesScreenParams) => {
if (
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
!data
) {
updateOnFocus.current = false;
return;
}
const isNew = data && data?.item?.id !== params.current?.item?.id;
if (data) params.current = data;

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react";
import { View } from "react-native";
import { Platform, View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { IconButton } from "../../components/ui/icon-button";
import Navigation from "../../services/navigation";
@@ -49,8 +49,8 @@ export const SearchBar = ({
return selectionMode && isFocused ? null : (
<View
style={{
height: 50 + insets.top,
paddingTop: insets.top,
height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: Platform.OS === "ios" ? 0 : insets.top,
flexDirection: "row",
alignItems: "center",
flexShrink: 1,

View File

@@ -26,7 +26,7 @@ import { Pressable } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import BiometicService from "../../services/biometrics";
import BiometricService from "../../services/biometrics";
import { ToastManager, presentSheet } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
@@ -119,7 +119,7 @@ const AppLock = () => {
type={appLockMode === item.value ? "secondary" : "transparent"}
onPress={async () => {
if (
!(await BiometicService.isBiometryAvailable()) &&
!(await BiometricService.isBiometryAvailable()) &&
!useUserStore.getState().user &&
item.value !== modes[0].value &&
!SettingsService.getProperty("appLockHasPasswordSecurity")
@@ -139,7 +139,7 @@ const AppLock = () => {
) &&
item.value !== modes[0].value
) {
const verified = await BiometicService.validateUser(
const verified = await BiometricService.validateUser(
"Verify it's you"
);
if (verified) {

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { db } from "../../common/database";
import { validateAppLockPassword } from "../../common/database/encryption";
import { presentDialog } from "../../components/dialog/functions";
import BiometicService from "../../services/biometrics";
import BiometricService from "../../services/biometrics";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
@@ -100,9 +100,9 @@ export async function verifyUserWithApplock() {
}
});
} else {
BiometicService.isBiometryAvailable().then((available) => {
BiometricService.isBiometryAvailable().then((available) => {
if (available) {
BiometicService.validateUser("Verify it's you").then((verified) => {
BiometricService.validateUser("Verify it's you").then((verified) => {
resolve(verified);
});
} else if (useUserStore.getState().user) {

View File

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

View File

@@ -38,7 +38,7 @@ import { Update } from "../../components/sheets/update";
import { VaultStatusType, useVaultStatus } from "../../hooks/use-vault-status";
import { BackgroundSync } from "../../services/background-sync";
import BackupService from "../../services/backup";
import BiometicService from "../../services/biometrics";
import BiometricService from "../../services/biometrics";
import {
ToastManager,
eSendEvent,
@@ -349,7 +349,7 @@ export const settingsGroups: SettingSection[] = [
await db.user?.logout();
setLoginMessage();
await PremiumService.setPremiumStatus();
await BiometicService.resetCredentials();
await BiometricService.resetCredentials();
MMKV.clearStore();
clearAllStores();
refreshAllStores();
@@ -394,7 +394,7 @@ export const settingsGroups: SettingSection[] = [
if (verified) {
eSendEvent("settings-loading", true);
await db.user?.deleteUser(value);
await BiometicService.resetCredentials();
await BiometricService.resetCredentials();
SettingsService.set({
introCompleted: true
});
@@ -789,7 +789,7 @@ export const settingsGroups: SettingSection[] = [
}
},
{
id: "biometic-unlock",
id: "biometric-unlock",
type: "switch",
name: "Biometric unlocking",
icon: "fingerprint",
@@ -859,13 +859,13 @@ export const settingsGroups: SettingSection[] = [
if (!SettingsService.getProperty("appLockEnabled")) {
if (
!SettingsService.getProperty("appLockHasPasswordSecurity") &&
(await BiometicService.isBiometryAvailable())
(await BiometricService.isBiometryAvailable())
) {
SettingsService.setProperty("biometricsAuthEnabled", true);
}
if (
!(await BiometicService.isBiometryAvailable()) &&
!(await BiometricService.isBiometryAvailable()) &&
!SettingsService.getProperty("appLockHasPasswordSecurity")
) {
ToastManager.show({

View File

@@ -41,7 +41,7 @@ import {
TouchableOpacity,
View
} from "react-native";
import { db } from "../../common/database";
import { DatabaseLogger, db } from "../../common/database";
import SheetProvider from "../../components/sheet-provider";
import { Button } from "../../components/ui/button";
import Input from "../../components/ui/input";
@@ -326,11 +326,10 @@ function ThemeSelector() {
return page.themes;
})
.flat()
.filter(
(theme) =>
(!searchQuery || searchQuery === "") &&
darkTheme.id !== theme.id &&
lightTheme.id !== theme.id
.filter((theme) =>
searchQuery && searchQuery !== ""
? true
: darkTheme.id !== theme.id && lightTheme.id !== theme.id
) || []
);
}
@@ -546,7 +545,7 @@ const ThemeSetter = ({
context: "global"
});
} catch (e) {
console.log("Error", e);
DatabaseLogger.error(e);
}
setTimeout(() => {

View File

@@ -31,7 +31,6 @@ import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
import { useUserStore } from "../stores/use-user-store";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -152,13 +151,14 @@ async function updateNextBackupTime() {
/**
* @param {boolean=} progress
* @param {string=} context
* @returns {Promise<{path?: string, error?: Error}}>
* @returns {Promise<{path?: string, error?: Error, report?: boolean}}>
*/
async function run(progress = false, context) {
let androidBackupDirectory = await checkBackupDirExists(false, context);
if (!androidBackupDirectory)
return {
error: new Error("Backup directory not selected")
error: new Error("Backup directory not selected"),
report: false
};
if (progress) {
@@ -259,11 +259,12 @@ async function run(progress = false, context) {
return run(progress, context);
}
DatabaseLogger.error(e, "Backup failed");
DatabaseLogger.error(e);
await sleep(300);
progress && eSendEvent(eCloseSheet);
return {
error: e
error: e,
report: true
};
}
}

View File

@@ -179,7 +179,7 @@ async function validateUser(title: string, description?: string) {
}
}
const BiometicService = {
const BiometricService = {
isBiometryAvailable,
enableFingerprintAuth,
isFingerprintAuthEnabled,
@@ -190,4 +190,4 @@ const BiometicService = {
validateUser
};
export default BiometicService;
export default BiometricService;

View File

@@ -40,7 +40,7 @@ import { basename, dirname, join } from "pathe";
import downloadAttachment from "../common/filesystem/download-attachment";
import { presentDialog } from "../components/dialog/functions";
import { useSettingStore } from "../stores/use-setting-store";
import BiometicService from "./biometrics";
import BiometricService from "./biometrics";
import { ToastManager } from "./event-manager";
import { cacheDir } from "../common/filesystem/utils";
@@ -186,10 +186,10 @@ async function exportAs(
}
async function unlockVault() {
const biometry = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (biometry && fingerprint) {
const credentials = await BiometicService.getCredentials(
const credentials = await BiometricService.getCredentials(
"Unlock vault",
"Unlock vault to export locked notes"
);

View File

@@ -49,6 +49,8 @@ import { DDS } from "./device-detection";
import { eSendEvent } from "./event-manager";
import Navigation from "./navigation";
import SettingsService from "./settings";
import { useUserStore } from "../stores/use-user-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
let pinned: DisplayedNotification[] = [];
@@ -139,6 +141,7 @@ const onEvent = async ({ type, detail }: Event) => {
}
editorState().movedAway = false;
const noteId = notification?.id;
console.log("NOTE ID", noteId);
loadNote(noteId as string, true);
}
@@ -407,18 +410,31 @@ async function loadNote(id: string, jump: boolean) {
if (!DDS.isTab && jump) {
tabBarRef.current?.goToPage(1);
}
eSendEvent("loadingNote", note);
setTimeout(
() => {
eSendEvent(eOnLoadNote, {
item: note
});
if (!jump && !DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
},
tabBarRef?.current ? 0 : 2000
MMKV.setString(
"appState",
JSON.stringify({
editing: true,
movedAway: false,
timestamp: Date.now()
})
);
const isLocked = await db.vaults.itemExists({
type: "note",
id: id
});
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
useTabStore.getState().focusTab(tab);
} else {
useTabStore.getState().focusPreviewTab(id, {
noteId: id,
readonly: note.readonly,
noteLocked: isLocked
});
}
}
async function getChannelId(id: "silent" | "vibrate" | "urgent" | "default") {
@@ -476,7 +492,15 @@ async function displayNotification({
reply_button_text?: string;
id?: string;
}) {
if (!(await checkAndRequestPermissions())) return;
useUserStore.setState({
disableAppLockRequests: true
});
const permission = await checkAndRequestPermissions();
useUserStore.setState({
disableAppLockRequests: false
});
if (!permission) return;
try {
await notifee.displayNotification({
@@ -846,7 +870,16 @@ async function remove(id: string) {
}
async function pinQuickNote(launch: boolean) {
if (!(await checkAndRequestPermissions())) return;
useUserStore.setState({
disableAppLockRequests: true
});
const permission = await checkAndRequestPermissions();
useUserStore.setState({
disableAppLockRequests: false
});
if (!permission) {
return;
}
get().then((items) => {
const notification = items.filter((n) => n.id === "notesnook_note_input");
if (notification && launch) {

View File

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

View File

@@ -1,3 +1,3 @@
Notesnook V3 is here, and it's packed with new features and improvements such as bi-directional note linking, better syncing, at-rest encryption, nested notebooks, editor tabs, better app lock and so much more.
- Bug fixes and performance improvements
Check out our blogpost to learn about what's new at https://blog.notesnook.com/introducing-notesnook-v3.
Thank you for using Notesnook!

View File

@@ -21,9 +21,9 @@
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */; };
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */; };
656835812BB29A9800144BAB /* OpenSans-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 656835802BB29A8300144BAB /* OpenSans-Italic.ttf */; };
656DD2AB2B1891DF00A362EA /* (null) in Resources */ = {isa = PBXBuildFile; };
656DD2AC2B1891DF00A362EA /* (null) in Resources */ = {isa = PBXBuildFile; };
656DD2AD2B1891DF00A362EA /* (null) in Resources */ = {isa = PBXBuildFile; };
656DD2AB2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
656DD2AC2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
656DD2AD2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
6593E4A3281C345400492C50 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6593E4A2281C345400492C50 /* AppDelegate.mm */; };
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */ = {isa = PBXBuildFile; fileRef = 659BE46625E11A5100E05671 /* notesnook-text.png */; };
65AA857925E6DDEC00772A01 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65AA857825E6DDEC00772A01 /* WidgetKit.framework */; };
@@ -586,9 +586,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
656DD2AB2B1891DF00A362EA /* (null) in Resources */,
656DD2AC2B1891DF00A362EA /* (null) in Resources */,
656DD2AD2B1891DF00A362EA /* (null) in Resources */,
656DD2AB2B1891DF00A362EA /* BuildFile in Resources */,
656DD2AC2B1891DF00A362EA /* BuildFile in Resources */,
656DD2AD2B1891DF00A362EA /* BuildFile in Resources */,
65C400DF2A80B6B600AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
65C149872A61151B005C40F1 /* extension.bundle in Resources */,
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */,
@@ -1015,7 +1015,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2099;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1089,7 +1089,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1120,7 +1120,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2099;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1194,7 +1194,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1353,7 +1353,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2099;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1365,7 +1365,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1396,7 +1396,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2099;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1409,7 +1409,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1439,7 +1439,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2099;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1513,7 +1513,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1544,7 +1544,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2097;
CURRENT_PROJECT_VERSION = 2099;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1619,7 +1619,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.0;
MARKETING_VERSION = 3.0.2;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -319,8 +319,6 @@ PODS:
- React-Core
- react-native-html-to-pdf-lite (0.9.1):
- React
- react-native-image-picker (4.1.2):
- React-Core
- react-native-image-resizer (3.0.5):
- React-Core
- react-native-in-app-review (4.3.3):
@@ -345,7 +343,7 @@ PODS:
- React-Core
- react-native-safe-area-context (4.9.0):
- React-Core
- react-native-share-extension (2.5.6):
- react-native-share-extension (3.0.0):
- React
- react-native-sodium (1.5.4):
- React
@@ -597,7 +595,6 @@ DEPENDENCIES:
- react-native-get-random-values (from `../../node_modules/react-native-get-random-values`)
- react-native-gzip (from `../../node_modules/react-native-gzip`)
- react-native-html-to-pdf-lite (from `../../node_modules/react-native-html-to-pdf-lite`)
- react-native-image-picker (from `../../node_modules/react-native-image-picker`)
- "react-native-image-resizer (from `../../node_modules/@bam.tech/react-native-image-resizer`)"
- react-native-in-app-review (from `../../node_modules/react-native-in-app-review`)
- "react-native-keep-awake (from `../../node_modules/@sayem314/react-native-keep-awake`)"
@@ -736,8 +733,6 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native-gzip"
react-native-html-to-pdf-lite:
:path: "../../node_modules/react-native-html-to-pdf-lite"
react-native-image-picker:
:path: "../../node_modules/react-native-image-picker"
react-native-image-resizer:
:path: "../../node_modules/@bam.tech/react-native-image-resizer"
react-native-in-app-review:
@@ -899,7 +894,6 @@ SPEC CHECKSUMS:
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-gzip: c5e87ee9e359f02350e3a2ee52eb35eddc398868
react-native-html-to-pdf-lite: 21bfb169bf4cbcd7bec9f736975ee1b3f5292d4a
react-native-image-picker: 9c8a2687b69300ad9e95cec5d38f35ab9d32467d
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4
react-native-keep-awake: caee3ff89eaa21dfe29010f0d143566874a04441
@@ -910,7 +904,7 @@ SPEC CHECKSUMS:
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
react-native-quick-sqlite: e0e23b749382a85e4b57146f753de737a6c3a9e1
react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b
react-native-share-extension: df66a2ee48a62277d79898375e2142bde0782063
react-native-share-extension: faed334b1ddf165f1e576fcabd3dc1c9e748bfa9
react-native-sodium: 955bb0dc3ea05f8ea06d5e96cb89d1be7b5d7681
react-native-theme-switch-animation: 220f883f7be290e79f2ab022093ed1a7a5929e6d
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
@@ -964,4 +958,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
COCOAPODS: 1.12.1
COCOAPODS: 1.14.2

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@ammarahmed/notifee-react-native": "7.4.7",
"@ammarahmed/react-native-share-extension": "^2.5.9",
"@ammarahmed/react-native-share-extension": "^3.0.0",
"@ammarahmed/react-native-sodium": "1.5.4",
"@bam.tech/react-native-image-resizer": "3.0.5",
"@callstack/repack": "^3.2.0",
@@ -41,7 +41,6 @@
"react-native-gzip": "1.1.0",
"react-native-html-to-pdf-lite": "^0.9.1",
"react-native-iap": "12.11.0",
"react-native-image-picker": "4.1.2",
"react-native-in-app-review": "4.3.3",
"react-native-keychain": "4.0.5",
"react-native-mmkv-storage": "^0.10.0-alpha.12",

View File

@@ -7162,7 +7162,7 @@
},
"../../packages/editor-mobile/node_modules/@types/prop-types": {
"version": "15.7.11",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/q": {
@@ -7182,7 +7182,7 @@
},
"../../packages/editor-mobile/node_modules/@types/react": {
"version": "18.2.39",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -7213,7 +7213,7 @@
},
"../../packages/editor-mobile/node_modules/@types/scheduler": {
"version": "0.16.8",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/semver": {
@@ -12071,7 +12071,7 @@
},
"../../packages/editor-mobile/node_modules/immer": {
"version": "9.0.21",
"dev": true,
"devOptional": true,
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -22460,7 +22460,6 @@
},
"../../packages/editor/node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
"license": "MIT"
},
"../../packages/editor/node_modules/json-parse-even-better-errors": {
@@ -22516,7 +22515,6 @@
},
"../../packages/editor/node_modules/loose-envify": {
"version": "1.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -23038,7 +23036,6 @@
},
"../../packages/editor/node_modules/react": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -23057,7 +23054,6 @@
},
"../../packages/editor/node_modules/react-dom": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
@@ -23177,7 +23173,6 @@
},
"../../packages/editor/node_modules/scheduler": {
"version": "0.23.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -28443,7 +28438,7 @@
"@ammarahmed/notifee-react-native": "7.4.7",
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-share-extension": "^2.5.9",
"@ammarahmed/react-native-share-extension": "^3.0.0",
"@ammarahmed/react-native-sodium": "1.5.4",
"@bam.tech/react-native-image-resizer": "3.0.5",
"@callstack/repack": "^3.2.0",
@@ -28478,7 +28473,6 @@
"react-native-html-to-pdf-lite": "^0.9.1",
"react-native-iap": "12.11.0",
"react-native-image-crop-picker": "^0.40.2",
"react-native-image-picker": "4.1.2",
"react-native-in-app-review": "4.3.3",
"react-native-keychain": "4.0.5",
"react-native-mmkv-storage": "^0.10.0-alpha.12",
@@ -28596,9 +28590,9 @@
}
},
"node_modules/@ammarahmed/react-native-share-extension": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.5.9.tgz",
"integrity": "sha512-IEtlh/YCWggcse88yqSX5M+rTyEhu5xHTcje30QbVKtJwtWofbqjzL7UO4EIXKpyFh5u4/CDaY5Yy9kQS1yTjA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-3.0.0.tgz",
"integrity": "sha512-pXf/8Zt46yPfJZ4FMlhI+Bgh/yHllNPiOeb9rqTwGJLuWYA7E6QFNuZ4n4smK7rJZrmhOYV12gFUzFtodgC/8w==",
"dependencies": {
"react-native": "^0.63.1"
}
@@ -30896,7 +30890,6 @@
},
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30910,7 +30903,6 @@
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31057,7 +31049,6 @@
},
"node_modules/@babel/plugin-proposal-private-property-in-object": {
"version": "7.21.0-placeholder-for-preset-env.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -31068,7 +31059,6 @@
},
"node_modules/@babel/plugin-proposal-unicode-property-regex": {
"version": "7.18.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -31114,7 +31104,6 @@
},
"node_modules/@babel/plugin-syntax-class-static-block": {
"version": "7.14.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -31151,7 +31140,6 @@
},
"node_modules/@babel/plugin-syntax-export-namespace-from": {
"version": "7.8.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.3"
@@ -31175,7 +31163,6 @@
},
"node_modules/@babel/plugin-syntax-import-assertions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31189,7 +31176,6 @@
},
"node_modules/@babel/plugin-syntax-import-attributes": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31203,7 +31189,6 @@
},
"node_modules/@babel/plugin-syntax-import-meta": {
"version": "7.10.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -31214,7 +31199,6 @@
},
"node_modules/@babel/plugin-syntax-json-strings": {
"version": "7.8.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -31238,7 +31222,6 @@
},
"node_modules/@babel/plugin-syntax-logical-assignment-operators": {
"version": "7.10.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -31299,7 +31282,6 @@
},
"node_modules/@babel/plugin-syntax-private-property-in-object": {
"version": "7.14.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -31313,7 +31295,6 @@
},
"node_modules/@babel/plugin-syntax-top-level-await": {
"version": "7.14.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -31340,7 +31321,6 @@
},
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
"version": "7.18.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -31368,7 +31348,6 @@
},
"node_modules/@babel/plugin-transform-async-generator-functions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.5",
@@ -31426,7 +31405,6 @@
},
"node_modules/@babel/plugin-transform-class-properties": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
@@ -31441,7 +31419,6 @@
},
"node_modules/@babel/plugin-transform-class-static-block": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
@@ -31505,7 +31482,6 @@
},
"node_modules/@babel/plugin-transform-dotall-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -31520,7 +31496,6 @@
},
"node_modules/@babel/plugin-transform-duplicate-keys": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31534,7 +31509,6 @@
},
"node_modules/@babel/plugin-transform-dynamic-import": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31563,7 +31537,6 @@
},
"node_modules/@babel/plugin-transform-export-namespace-from": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31620,7 +31593,6 @@
},
"node_modules/@babel/plugin-transform-json-strings": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31648,7 +31620,6 @@
},
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31676,7 +31647,6 @@
},
"node_modules/@babel/plugin-transform-modules-amd": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
@@ -31706,7 +31676,6 @@
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-hoist-variables": "^7.22.5",
@@ -31723,7 +31692,6 @@
},
"node_modules/@babel/plugin-transform-modules-umd": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
@@ -31752,7 +31720,6 @@
},
"node_modules/@babel/plugin-transform-new-target": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31766,7 +31733,6 @@
},
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31781,7 +31747,6 @@
},
"node_modules/@babel/plugin-transform-numeric-separator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31809,7 +31774,6 @@
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.5",
@@ -31841,7 +31805,6 @@
},
"node_modules/@babel/plugin-transform-optional-catch-binding": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31856,7 +31819,6 @@
},
"node_modules/@babel/plugin-transform-optional-chaining": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31885,7 +31847,6 @@
},
"node_modules/@babel/plugin-transform-private-methods": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
@@ -31900,7 +31861,6 @@
},
"node_modules/@babel/plugin-transform-private-property-in-object": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
@@ -32000,7 +31960,6 @@
},
"node_modules/@babel/plugin-transform-reserved-words": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32092,7 +32051,6 @@
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32122,7 +32080,6 @@
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32136,7 +32093,6 @@
},
"node_modules/@babel/plugin-transform-unicode-property-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -32165,7 +32121,6 @@
},
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -32180,7 +32135,6 @@
},
"node_modules/@babel/preset-env": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.5",
@@ -32273,7 +32227,6 @@
},
"node_modules/@babel/preset-env/node_modules/semver": {
"version": "6.3.0",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -32296,7 +32249,6 @@
},
"node_modules/@babel/preset-modules": {
"version": "0.1.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
@@ -34946,7 +34898,6 @@
},
"node_modules/@types/eslint": {
"version": "8.40.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
@@ -34955,7 +34906,6 @@
},
"node_modules/@types/eslint-scope": {
"version": "3.7.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
@@ -34964,7 +34914,6 @@
},
"node_modules/@types/estree": {
"version": "1.0.1",
"dev": true,
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
@@ -35413,7 +35362,6 @@
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.6",
@@ -35422,22 +35370,18 @@
},
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
@@ -35447,12 +35391,10 @@
},
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35463,7 +35405,6 @@
},
"node_modules/@webassemblyjs/ieee754": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
@@ -35471,7 +35412,6 @@
},
"node_modules/@webassemblyjs/leb128": {
"version": "1.11.6",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@xtuc/long": "4.2.2"
@@ -35479,12 +35419,10 @@
},
"node_modules/@webassemblyjs/utf8": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35499,7 +35437,6 @@
},
"node_modules/@webassemblyjs/wasm-gen": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35511,7 +35448,6 @@
},
"node_modules/@webassemblyjs/wasm-opt": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35522,7 +35458,6 @@
},
"node_modules/@webassemblyjs/wasm-parser": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35535,7 +35470,6 @@
},
"node_modules/@webassemblyjs/wast-printer": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35593,12 +35527,10 @@
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@yarnpkg/lockfile": {
@@ -35654,7 +35586,6 @@
},
"node_modules/acorn-import-assertions": {
"version": "1.9.0",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8"
@@ -36759,7 +36690,6 @@
},
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
@@ -37860,7 +37790,6 @@
},
"node_modules/enhanced-resolve": {
"version": "5.15.0",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -37973,7 +37902,6 @@
},
"node_modules/es-module-lexer": {
"version": "1.3.0",
"dev": true,
"license": "MIT"
},
"node_modules/es-set-tostringtag": {
@@ -38304,7 +38232,6 @@
},
"node_modules/eslint-scope": {
"version": "5.1.1",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@@ -38316,7 +38243,6 @@
},
"node_modules/eslint-scope/node_modules/estraverse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -38502,7 +38428,6 @@
},
"node_modules/esrecurse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@@ -38513,7 +38438,6 @@
},
"node_modules/estraverse": {
"version": "5.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -38521,7 +38445,6 @@
},
"node_modules/esutils": {
"version": "2.0.3",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@@ -39578,7 +39501,6 @@
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/global": {
@@ -41822,7 +41744,6 @@
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
@@ -42090,7 +42011,6 @@
},
"node_modules/loader-runner": {
"version": "4.3.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
@@ -44742,7 +44662,6 @@
},
"node_modules/randombytes": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
@@ -45141,14 +45060,6 @@
"react-native": "*"
}
},
"node_modules/react-native-image-picker": {
"version": "4.1.2",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-image-zoom-viewer": {
"version": "3.0.1",
"license": "MIT",
@@ -45521,7 +45432,6 @@
},
"node_modules/react-test-renderer": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"react-is": "^18.2.0",
@@ -45534,12 +45444,10 @@
},
"node_modules/react-test-renderer/node_modules/react-is": {
"version": "18.2.0",
"dev": true,
"license": "MIT"
},
"node_modules/react-test-renderer/node_modules/scheduler": {
"version": "0.23.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -46375,7 +46283,6 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
@@ -47431,7 +47338,6 @@
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.9",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
@@ -47464,7 +47370,6 @@
},
"node_modules/terser-webpack-plugin/node_modules/jest-worker": {
"version": "27.5.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -47477,7 +47382,6 @@
},
"node_modules/terser-webpack-plugin/node_modules/supports-color": {
"version": "8.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -48222,7 +48126,6 @@
},
"node_modules/watchpack": {
"version": "2.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
@@ -48245,7 +48148,6 @@
},
"node_modules/webpack": {
"version": "5.88.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -48360,7 +48262,6 @@
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.0",
"version": "3.0.2",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -33,6 +33,7 @@ import {
SafeAreaView,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
useWindowDimensions
@@ -115,12 +116,14 @@ const ShareView = () => {
const appendNoteId = useShareStore((state) => state.appendNote);
const [note, setNote] = useState({ ...defaultNote });
const noteContent = useRef("");
const noteTitle = useRef("");
const [loading, setLoading] = useState(false);
const [loadingExtension, setLoadingExtension] = useState(true);
const [rawData, setRawData] = useState({
type: null,
value: null
});
const inputRef = useRef(null);
const [mode, setMode] = useState(1);
const keyboardHeight = useRef(0);
const { width, height } = useWindowDimensions();
@@ -192,6 +195,7 @@ const ShareView = () => {
}
return;
}
let note = { ...defaultNote };
for (let item of data) {
if (item.type === "text") {
@@ -207,7 +211,14 @@ const ShareView = () => {
if (!key) continue;
if (key.includes("TITLE") || key.includes("SUBJECT")) {
note.title = item[key];
console.log("Note title will be", note.title);
noteTitle.current = note.title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
if (key.includes("TEXT") && !note.content.data) {
note.content.data = item[key];
noteContent.current = item[key];
}
}
} else {
@@ -216,8 +227,9 @@ const ShareView = () => {
if (
(isImage(item.type) && item.size > IMAGE_SIZE_LIMIT) ||
(!isImage(item.type) && item.size > FILE_SIZE_LIMIT)
)
) {
continue;
}
setRawFiles((files) => {
const index = files.findIndex(
@@ -244,6 +256,7 @@ const ShareView = () => {
);
const onLoad = useCallback(() => {
console.log(noteContent.current, "current...");
eSendEvent(eOnLoadNote + "shareEditor", {
id: null,
content: {
@@ -275,7 +288,7 @@ const ShareView = () => {
const onPress = async () => {
setLoading(true);
if (!noteContent.current && rawFiles.length === 0) {
if (!noteContent.current && rawFiles.length === 0 && !noteTitle.current) {
setLoading(false);
return;
}
@@ -298,12 +311,14 @@ const ShareView = () => {
type: "tiptap"
},
id: note.id,
sessionId: Date.now()
sessionId: Date.now(),
title: noteTitle.current
};
} else {
noteData = { ...note };
noteData.content.data = noteContent.current;
noteData.sessionId = Date.now();
noteData.title = noteTitle.current;
}
try {
@@ -468,10 +483,29 @@ const ShareView = () => {
borderBottomWidth: 1,
paddingBottom: 12,
borderBottomColor: colors.secondary.background,
paddingHorizontal: 12
paddingHorizontal: 12,
gap: 10
}}
>
<Heading size={SIZE.lg}>Save to Notesnook</Heading>
<TextInput
placeholder="Enter note title"
ref={inputRef}
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "OpenSans-SemiBold",
fontSize: SIZE.lg,
paddingBottom: 0,
paddingTop: 0
}}
onChangeText={(value) => {
noteTitle.current = value;
}}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current.focus();
}}
/>
<Button
title="Done"
type="accent"
@@ -626,13 +660,15 @@ const ShareView = () => {
}}
>
{!loadingExtension && !loadingPage ? (
<Editor
editorRef={editorRef}
onLoad={onLoadEditor}
onChange={(html) => {
noteContent.current = html;
}}
/>
<>
<Editor
editorRef={editorRef}
onLoad={onLoadEditor}
onChange={(html) => {
noteContent.current = html;
}}
/>
</>
) : (
<>
{loadingPage ? (
@@ -646,7 +682,19 @@ const ShareView = () => {
</SafeAreaProvider>
</View>
{appendNoteId ? <AppendNote id={appendNoteId} /> : null}
{appendNoteId ? (
<AppendNote
id={appendNoteId}
onLoad={(title) => {
if (!noteTitle.current) {
noteTitle.current = title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
}}
/>
) : null}
<View
style={{
@@ -766,10 +814,16 @@ const ShareView = () => {
);
};
const AppendNote = ({ id }) => {
const AppendNote = ({ id, onLoad }) => {
const { colors } = useThemeColors();
const [item] = useDBItem(id, "note");
useEffect(() => {
if (item?.title) {
onLoad?.(item.title);
}
}, [item?.title, onLoad]);
return !item ? null : (
<Paragraph
size={SIZE.xs}

View File

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

View File

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

View File

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

View File

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

View File

@@ -121,7 +121,7 @@ function Header(props: HeaderProps) {
}}
defaultItems={() =>
db.tags.all.limit(10).items(undefined, {
sortBy: "dateCreated",
sortBy: "title",
sortDirection: "desc"
})
}

View File

@@ -87,6 +87,7 @@ function saveContent(noteId: string, ignoreEdit: boolean, content: string) {
const deferredSave = debounceWithId(saveContent, 100);
export default function TabsView() {
const sessions = useEditorStore((store) => store.sessions);
const documentPreview = useEditorStore((store) => store.documentPreview);
const activeSessionId = useEditorStore((store) => store.activeSessionId);
const arePropertiesVisible = useEditorStore(
@@ -95,7 +96,6 @@ export default function TabsView() {
const isTOCVisible = useEditorStore((store) => store.isTOCVisible);
const [dropRef, overlayRef] = useDragOverlay();
const sessions = useEditorStore.getState().sessions;
return (
<>
{IS_DESKTOP_APP ? (
@@ -488,12 +488,25 @@ export function Editor(props: EditorProps) {
editor?.attachFile(attachment);
}
}}
onGetAttachmentData={(attachment) => {
return downloadAttachment(
onGetAttachmentData={async (attachment) => {
logger.debug("Getting attachment data", {
hash: attachment.hash,
type: attachment.type
});
const result = await downloadAttachment(
attachment.hash,
attachment.type === "web-clip" ? "text" : "base64",
id?.toString()
);
if (!result)
logger.debug("Got no result after downloading attachment", {
hash: attachment.hash,
type: attachment.type
});
return result;
}}
onAttachFiles={async (files) => {
const editor = useEditorManager.getState().getEditor(id)?.editor;

View File

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

View File

@@ -24,7 +24,6 @@ import "@notesnook/editor/styles/fonts.css";
import {
Toolbar,
useTiptap,
PortalProvider,
Editor,
AttachmentType,
usePermissionHandler,
@@ -41,7 +40,6 @@ import {
import { Box, Flex } from "@theme-ui/components";
import {
PropsWithChildren,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
@@ -61,7 +59,6 @@ import { writeToClipboard } from "../../utils/clipboard";
import { useEditorStore } from "../../stores/editor-store";
import { parseInternalLink } from "@notesnook/core";
import Skeleton from "react-loading-skeleton";
import { showToast } from "../../utils/toast";
export type OnChangeHandler = (
content: () => string,
@@ -402,58 +399,56 @@ function TiptapWrapper(
}, [theme]);
return (
<PortalProvider>
<Flex
ref={containerRef}
sx={{
flex: 1,
flexDirection: "column",
".tiptap.ProseMirror": { pb: 150 }
<Flex
ref={containerRef}
sx={{
flex: 1,
flexDirection: "column",
".tiptap.ProseMirror": { pb: 150 }
}}
>
<TipTap
{...props}
onLoad={(editor) => {
props.onLoad?.(editor);
containerRef.current
?.querySelector(".editor-loading-container")
?.remove();
}}
>
<TipTap
{...props}
onLoad={(editor) => {
props.onLoad?.(editor);
containerRef.current
?.querySelector(".editor-loading-container")
?.remove();
}}
editorContainer={() => {
if (editorContainerRef.current) return editorContainerRef.current;
const editorContainer = document.createElement("div");
editorContainer.classList.add("selectable");
editorContainer.style.flex = "1";
editorContainer.style.cursor = "text";
editorContainer.style.color =
theme.scopes.editor?.primary?.paragraph ||
theme.scopes.base.primary.paragraph;
editorContainer.style.fontSize = `${editorConfig.fontSize}px`;
editorContainer.style.fontFamily =
getFontById(editorConfig.fontFamily)?.font || "sans-serif";
editorContainerRef.current = editorContainer;
return editorContainer;
}}
fontFamily={editorConfig.fontFamily}
fontSize={editorConfig.fontSize}
editorContainer={() => {
if (editorContainerRef.current) return editorContainerRef.current;
const editorContainer = document.createElement("div");
editorContainer.classList.add("selectable");
editorContainer.style.flex = "1";
editorContainer.style.cursor = "text";
editorContainer.style.color =
theme.scopes.editor?.primary?.paragraph ||
theme.scopes.base.primary.paragraph;
editorContainer.style.fontSize = `${editorConfig.fontSize}px`;
editorContainer.style.fontFamily =
getFontById(editorConfig.fontFamily)?.font || "sans-serif";
editorContainerRef.current = editorContainer;
return editorContainer;
}}
fontFamily={editorConfig.fontFamily}
fontSize={editorConfig.fontSize}
/>
{props.children}
<Box className="editor-loading-container">
<Skeleton
enableAnimation={false}
height={22}
style={{ marginTop: 16 }}
count={2}
/>
{props.children}
<Box className="editor-loading-container">
<Skeleton
enableAnimation={false}
height={22}
style={{ marginTop: 16 }}
count={2}
/>
<Skeleton
enableAnimation={false}
height={22}
width={25}
style={{ marginTop: 16 }}
/>
</Box>
</Flex>
</PortalProvider>
<Skeleton
enableAnimation={false}
height={22}
width={25}
style={{ marginTop: 16 }}
/>
</Box>
</Flex>
);
}
export default TiptapWrapper;

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 { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button, Flex, Text } from "@theme-ui/components";
import {
Plus,
@@ -46,7 +46,8 @@ import {
TreeEnvironmentRef
} from "react-complex-tree";
import { FlexScrollContainer } from "../components/scroll-container";
import { pluralize, usePromise } from "@notesnook/common";
import { pluralize } from "@notesnook/common";
import Field from "../components/field";
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
type NotebookReference = {
@@ -73,32 +74,17 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
const setIsMultiselect = useSelectionStore((store) => store.setIsMultiselect);
const isMultiselect = useSelectionStore((store) => store.isMultiselect);
const refreshNotebooks = useStore((store) => store.refresh);
// const notebooks = useStore((store) => store.notebooks);
const reloadItem = useRef<(changedItemIds: TreeItemIndex[]) => void>();
const treeRef = useRef<TreeEnvironmentRef>(null);
const rootNotebooks = usePromise(() =>
db.notebooks.roots.ids(db.settings.getGroupOptions("notebooks"))
);
const [notebooks, setNotebooks] = useState<string[]>([]);
useEffect(() => {
// for (const notebook of notebooks.ids) {
// if (isGroupHeader(notebook)) continue;
// // for (const topic of notebook.topics) {
// // const isSelected =
// // selected.findIndex(
// // (item) => item.id === notebook.id && item.topic === topic.id
// // ) > -1;
// // if (!isSelected && topicHasNotes(topic, noteIds)) {
// // selected.push({
// // id: notebook.id,
// // topic: topic.id,
// // op: "add",
// // new: false
// // });
// // }
// // }
// }
db.notebooks.roots
.ids(db.settings.getGroupOptions("notebooks"))
.then((ids) => setNotebooks(ids));
}, []);
useEffect(() => {
(async function () {
const selected: NotebookReference[] = useSelectionStore
.getState()
@@ -123,20 +109,6 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
setSelected(selected);
setIsMultiselect(selected.length > 1);
})();
// for (const notebook of noteIds
// .map((id) => db.relations.to({ id, type: "note" }, "notebook"))
// .flat()) {
// const isSelected =
// notebook && selected.findIndex((item) => item.id === notebook.id) > -1;
// if (!notebook || isSelected) continue;
// selected.push({
// id: notebook.id,
// op: "add",
// new: false
// });
// }
}, [noteIds, refreshNotebooks, setSelected, setIsMultiselect]);
const _onClose = useCallback(
@@ -155,7 +127,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
title={"Select notebooks"}
description={`Use ${
isMac() ? "cmd" : "ctrl"
}+click to select multiple topics`}
}+click to select multiple notebooks`}
onClose={() => _onClose(false)}
width={450}
positiveButton={{
@@ -194,6 +166,22 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
onClick: () => _onClose(false)
}}
>
<Field
autoFocus
sx={{ m: 0, mb: 2 }}
styles={{
input: { p: "7.5px" }
}}
placeholder={"Search notebooks"}
onChange={async (e) => {
const query = e.target.value.trim();
const ids = await (query
? db.lookup.notebooks(query).ids()
: db.notebooks.roots.ids(db.settings.getGroupOptions("notebooks")));
setNotebooks(ids);
reloadItem.current?.(["root"]);
}}
/>
{isMultiselect && (
<Button
variant="anchor"
@@ -210,8 +198,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
Reset selection
</Button>
)}
{rootNotebooks.status === "fulfilled" &&
rootNotebooks.value.length > 0 ? (
{notebooks.length > 0 ? (
<FlexScrollContainer>
<UncontrolledTreeEnvironment
ref={treeRef}
@@ -232,7 +219,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
isFolder: true,
canMove: false,
canRename: false,
children: rootNotebooks.value
children: notebooks
};
}
@@ -268,7 +255,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
isFolder: true,
canMove: false,
canRename: false,
children: rootNotebooks.value
children: notebooks
};
}
@@ -332,9 +319,9 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
sx={{ mt: 2 }}
onClick={() =>
showAddNotebookDialog().then(() =>
rootNotebooks.status === "fulfilled"
? rootNotebooks.refresh()
: null
db.notebooks.roots
.ids(db.settings.getGroupOptions("notebooks"))
.then((ids) => setNotebooks(ids))
)
}
>

View File

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

View File

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

View File

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

View File

@@ -43,16 +43,15 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
super({
start() {},
async pull(controller) {
if (signal?.aborted) {
controller.close();
return;
}
onProgress && onProgress(index);
const attachment = await resolve(ids[index++]);
if (!attachment) return;
try {
if (signal?.aborted) {
controller.close();
return;
}
onProgress && onProgress(index);
const attachment = await resolve(ids[index++]);
if (!attachment) return;
if (
!(await db
.fs()
@@ -87,7 +86,7 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
);
}
} catch (e) {
console.error(e);
console.error(e, attachment);
showToast("error", (e as Error).message);
} finally {
if (index === ids.length) {

View File

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

View File

@@ -0,0 +1,7 @@
- Now you can set title of note when sharing to Notesnook
- Fix back navigation from editor closes the app
- Fix errors during migration
- Fix sharing to Notesnook not working for some apps
- Numerous other bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,3 @@
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -151,7 +151,10 @@ async function resolveNotes(ids: string[]) {
notebooks: await db.notebooks.all.records(
Array.from(relationIds.notebooks)
),
tags: await db.tags.all.records(Array.from(relationIds.tags)),
tags: await db.tags.all.records(Array.from(relationIds.tags), {
sortBy: "title",
sortDirection: "desc"
}),
colors: await db.colors.all.records(Array.from(relationIds.colors)),
reminders: await db.reminders.all.records(
Array.from(relationIds.reminders)

View File

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

View File

@@ -363,7 +363,7 @@ test("permanently deleting a notebook should not delete independently deleted su
expect(trash.some((a) => a.id === child)).toBe(false);
}));
test.only("permanently deleted note should not have note fields", () =>
test("permanently deleted note should not have note fields", () =>
databaseTest().then(async (db) => {
const noteId = await db.notes.add(TEST_NOTE);
await db.notes.moveToTrash(noteId);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,10 +20,6 @@
/* color: var(--nn_primary_paragraph) ## TODO: use fixed color */
}
.main-editor > .ProseMirror:first-child {
margin-top: -5px !important;
}
::selection {
color: white;
background-color: var(--nn_primary_accent);

View File

@@ -21,7 +21,6 @@ import {
Editor,
getFontById,
getTableOfContents,
PortalProvider,
TiptapOptions,
Toolbar,
usePermissionHandler,
@@ -197,7 +196,14 @@ const Tiptap = ({
? useTabStore.getState().noteState[tabRef.current.noteId]
: undefined;
const top = noteState?.top;
logger("info", tabRef.current.noteId, noteState?.top);
logger(
"info",
"editor.onCreate",
tabRef.current?.noteId,
noteState?.top,
noteState?.to,
noteState?.from
);
if (noteState?.to || noteState?.from) {
editors[tabRef.current.id]?.chain().setTextSelection({
@@ -205,6 +211,7 @@ const Tiptap = ({
from: noteState.from
});
}
logger("info", "Scrolling to", top, useTabStore.getState().noteState);
containerRef.current?.scrollTo({
left: 0,
top: top || 0,
@@ -235,7 +242,6 @@ const Tiptap = ({
const _editor = useTiptap(tiptapOptions, [tiptapOptions]);
const update = useCallback(() => {
logger("info", "LOADING NOTE...");
editors[tabRef.current.id]?.commands.setTextSelection(0);
setTick((tick) => tick + 1);
globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder(
@@ -279,6 +285,13 @@ const Tiptap = ({
? state.noteState[tabRef.current.noteId]
: undefined;
if (noteState) {
if (
containerRef.current &&
containerRef.current?.scrollHeight < noteState.top
) {
console.log("Container too small to scroll.");
return;
}
containerRef.current?.scrollTo({
left: 0,
top: noteState.top,
@@ -318,6 +331,7 @@ const Tiptap = ({
};
updateScrollPosition(useTabStore.getState());
const unsub = useTabStore.subscribe((state, prevState) => {
if (state.currentTab !== tabRef.current.id) {
isFocusedRef.current = false;
@@ -801,11 +815,7 @@ const TiptapProvider = (): JSX.Element => {
}
}, [settings.fontSize, settings.fontFamily]);
return (
<PortalProvider>
<Tiptap settings={settings} getContentDiv={getContentDiv} />
</PortalProvider>
);
return <Tiptap settings={settings} getContentDiv={getContentDiv} />;
};
export default TiptapProvider;

View File

@@ -16,12 +16,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
PortalProvider,
TiptapOptions,
getFontById,
useTiptap
} from "@notesnook/editor";
import { TiptapOptions, getFontById, useTiptap } from "@notesnook/editor";
import { useThemeColors } from "@notesnook/theme";
import {
useCallback,
@@ -71,11 +66,7 @@ export const ReadonlyEditorProvider = (): JSX.Element => {
}
}, [settings.fontSize, settings.fontFamily]);
return (
<PortalProvider>
<Tiptap settings={settings} getContentDiv={getContentDiv} />
</PortalProvider>
);
return <Tiptap settings={settings} getContentDiv={getContentDiv} />;
};
const Tiptap = ({

View File

@@ -54,6 +54,7 @@ type Timers = {
selectionChange: NodeJS.Timeout | null;
change: NodeJS.Timeout | null;
wordCounter: NodeJS.Timeout | null;
scroll: NodeJS.Timeout | null;
};
function isInViewport(element: any) {
@@ -135,7 +136,8 @@ export function useEditorController({
const timers = useRef<Timers>({
selectionChange: null,
change: null,
wordCounter: null
wordCounter: null,
scroll: null
});
if (!tabRef.current.noteId && loading) {
@@ -210,14 +212,19 @@ export function useEditorController({
const scroll = useCallback(
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {
const noteId = useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab);
if (noteId) {
useTabStore.getState().setNoteState(noteId, {
top: _event.currentTarget.scrollTop
});
}
const value = _event.currentTarget.scrollTop;
if (timers.current.scroll !== null) clearTimeout(timers.current.scroll);
timers.current.scroll = setTimeout(() => {
if (
tabRef.current.noteId &&
tabRef.current.noteId === useTabStore.getState().getCurrentNoteId()
) {
logger("info", tabRef.current.noteId, value);
useTabStore.getState().setNoteState(tabRef.current.noteId, {
top: value
});
}
}, 16);
},
[]
);
@@ -259,10 +266,13 @@ export function useEditorController({
preserveWhitespace: true
});
editor.commands.setTextSelection({
from,
to
});
if (editor.isFocused && (to !== 1 || from !== 1)) {
logger("info", "Setting focus", to, from);
editor.commands.setTextSelection({
from,
to
});
}
countWords(0);
}

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -32,6 +32,7 @@ import { Editor, NodeViewRendererProps } from "@tiptap/core";
import { __serializeForClipboard, EditorView } from "prosemirror-view";
import { EmotionThemeProvider } from "@notesnook/theme";
import { isAndroid, isiOS } from "../../utils/platform";
import { useToolbarStore } from "../../toolbar/stores/toolbar-store";
// This is hacky workaround to manually handle serialization when
// drag/dropping on mobile devices.
@@ -41,7 +42,7 @@ declare module "prosemirror-view" {
slice: Slice
): { dom: HTMLElement; text: string };
}
const portalProviderAPI = new PortalProviderAPI();
export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
private domRef!: HTMLElement;
private contentDOMWrapper?: Node;
@@ -53,30 +54,10 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
pos = -1;
posEnd: number | undefined;
// in order to cleanly unmount a React Portal, we have to preserve the
// dom structure for the full node. However, Prosemirror/browser can,
// sometimes, detach the node before React is able to call "unmount".
// Unmounting a React child whose DOM counterpart is already removed
// results in a DOMException.
// To fix that, we observe and store all the detached subnodes and later add
// them back when we are ready to destroy our node view once & for all.
// This wouldn't be necessary if React allowed for a way to "sync" the DOM
// changes to its Virtual DOM.
detached: Set<Node> = new Set();
detachObserver = new MutationObserver((mutations) => {
const filtered = mutations.filter(
(m) => m.target === this.domRef && m.removedNodes.length > 0
);
for (const mutation of filtered) {
for (const node of mutation.removedNodes) this.detached.add(node);
}
});
constructor(
node: PMNode,
protected readonly editor: Editor,
protected readonly getPos: GetPosNode,
protected readonly portalProviderAPI: PortalProviderAPI,
protected readonly options: ReactNodeViewOptions<P>
) {
this.node = node;
@@ -105,11 +86,6 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
init() {
this.domRef = this.createDomRef();
this.domRef.ondragstart = (ev) => this.onDragStart(ev);
this.detachObserver.observe(this.domRef, {
childList: true,
subtree: true
});
// this.setDomAttrs(this.node, this.domRef);
const { dom: contentDOMWrapper, contentDOM } = this.getContentDOM() ?? {};
@@ -131,12 +107,12 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
private render() {
if (process.env.NODE_ENV === "test") return;
if (!this.domRef || !this.portalProviderAPI) {
if (!this.domRef) {
console.warn("Cannot render node view");
return;
}
this.portalProviderAPI.render(this.Component, this.domRef);
portalProviderAPI.render(this.Component, this.domRef);
}
createDomRef(): HTMLElement {
@@ -183,9 +159,16 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
Component: FunctionComponent = () => {
if (!this.options.component) return null;
return (
<EmotionThemeProvider scope="editor" injectCssVars={false}>
<EmotionThemeProvider
scope="editor"
injectCssVars={false}
theme={
useToolbarStore.getState().isMobile
? { space: [0, 10, 12, 20] }
: undefined
}
>
<this.options.component
{...(this.options.props as P)}
pos={this.pos}
@@ -489,14 +472,7 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
}
destroy() {
this.detachObserver.disconnect();
// add back the detached nodes because React expects an untouched
// DOM representation (and there's no way to reconcile the DOM later).
for (const node of this.detached) {
this.domRef.appendChild(node);
}
this.portalProviderAPI.remove(this.domRef);
this.detached.clear();
portalProviderAPI.remove(this.domRef);
this.domRef.remove();
}
}
@@ -507,18 +483,11 @@ export function createNodeView<TProps extends ReactNodeViewProps>(
) {
return ({ node, getPos, editor }: NodeViewRendererProps) => {
const _getPos = () => (typeof getPos === "boolean" ? -1 : getPos());
if (!editor.storage.portalProviderAPI) return {};
return new ReactNodeView<TProps>(
node,
editor as Editor,
_getPos,
editor.storage.portalProviderAPI,
{
...options,
component
}
).init();
return new ReactNodeView<TProps>(node, editor as Editor, _getPos, {
...options,
component
}).init();
};
}

View File

@@ -17,18 +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, {
FunctionComponent,
PropsWithChildren,
useContext,
useEffect,
useMemo,
useRef,
useState
} from "react";
import { createPortal, flushSync } from "react-dom";
import { FunctionComponent, PropsWithChildren } from "react";
import { flushSync } from "react-dom";
import { EventDispatcher } from "./event-dispatcher";
import { nanoid } from "nanoid";
import { Root, createRoot } from "react-dom/client";
export type BasePortalProviderProps = PropsWithChildren<unknown>;
@@ -45,93 +37,31 @@ export type PortalRendererState = {
export class PortalProviderAPI extends EventDispatcher<Portals> {
portals: Map<HTMLElement, MountedPortal> = new Map();
roots: Map<HTMLElement, Root> = new Map();
constructor() {
super();
}
/**
* Trigger an update in all subscribers.
*/
private update() {
this.emit("update", this.portals);
}
render(Component: FunctionComponent, container: HTMLElement) {
const portal = this.portals.get(container);
this.portals.set(container, { Component, key: portal?.key ?? nanoid() });
this.update();
}
/**
* Force an update in all the portals by setting new keys for every portal.
*
* Delete all orphaned containers (deleted from the DOM). This is useful for
* Decoration where there is no destroy method.
*/
forceUpdate(): void {
for (const [container, { Component }] of this.portals) {
this.portals.set(container, { Component, key: nanoid() });
}
const root = this.roots.get(container) || createRoot(container);
flushSync(() => root.render(<Component />));
this.roots.set(container, root);
}
remove(container: HTMLElement) {
// Remove the portal which was being wrapped in the provided container.
this.portals.delete(container);
// if container is already unmounted (maybe by prosemirror),
// no need to proceed
if (!container.parentNode) return;
// Trigger an update
this.update();
const root = this.roots.get(container);
if (!root) return;
this.roots.delete(container);
try {
root.unmount();
} catch {
// ignore
}
}
}
const PortalProviderContext = React.createContext<
PortalProviderAPI | undefined
>(undefined);
export function usePortalProvider() {
return useContext(PortalProviderContext);
}
export function PortalProvider(props: PropsWithChildren) {
const portalProviderAPI = useMemo(() => new PortalProviderAPI(), []);
return (
<PortalProviderContext.Provider value={portalProviderAPI}>
{props.children}
<PortalRenderer portalProviderAPI={portalProviderAPI} />
</PortalProviderContext.Provider>
);
}
function PortalRenderer(props: { portalProviderAPI: PortalProviderAPI }) {
const { portalProviderAPI } = props;
const mounted = useRef(true);
const [portals, setPortals] = useState(() =>
Array.from(portalProviderAPI.portals.entries())
);
useEffect(() => {
mounted.current = true;
function onUpdate(portalMap: Portals) {
// we have to make sure the component is mounted otherwise React
// throws an error.
if (!mounted.current) return;
// flushSync is necessary here, otherwise we get into a loop where
// ProseMirror destroys and recreates the node view over and over again.
flushSync(() => setPortals(Array.from(portalMap.entries())));
}
portalProviderAPI.on("update", onUpdate);
return () => {
mounted.current = false;
portalProviderAPI.off("update", onUpdate);
};
}, [portalProviderAPI]);
return (
<>
{portals.map(([container, { Component, key }]) =>
createPortal(<Component />, container, key)
)}
</>
);
}

View File

@@ -82,8 +82,6 @@ export function TableComponent(props: ReactNodeViewProps) {
}
export function TableNodeView(editor: TiptapEditor) {
if (!editor.storage.portalProviderAPI) return;
const api = editor.storage.portalProviderAPI;
class TableNode
extends ReactNodeView<ReactNodeViewProps<unknown>>
implements NodeView
@@ -93,7 +91,6 @@ export function TableNodeView(editor: TiptapEditor) {
node,
editor,
() => 0, // todo
api,
{
component: TableComponent,
shouldUpdate: (prev, next) => {

View File

@@ -61,7 +61,6 @@ import OrderedList from "./extensions/ordered-list";
import { OutlineList } from "./extensions/outline-list";
import { OutlineListItem } from "./extensions/outline-list-item";
import { Paragraph } from "./extensions/paragraph";
import { PortalProviderAPI, usePortalProvider } from "./extensions/react";
import { SearchReplace } from "./extensions/search-replace";
import { Table } from "./extensions/table";
import TableCell from "./extensions/table-cell";
@@ -87,7 +86,6 @@ import { useEditorSearchStore } from "./toolbar/stores/search-store";
import { DiffHighlighter } from "./extensions/diff-highlighter";
interface TiptapStorage {
portalProviderAPI?: PortalProviderAPI;
dateFormat?: DateTimeOptions["dateFormat"];
timeFormat?: DateTimeOptions["timeFormat"];
openLink?: (url: string) => void;
@@ -124,7 +122,7 @@ export type TiptapOptions = EditorOptions &
Omit<WebClipOptions, "HTMLAttributes"> &
Omit<ImageOptions, "HTMLAttributes"> &
DateTimeOptions &
Omit<TiptapStorage, "portalProviderAPI"> & {
TiptapStorage & {
downloadOptions?: DownloadOptions;
isMobile?: boolean;
doubleSpacedLines?: boolean;
@@ -153,7 +151,6 @@ const useTiptap = (
...restOptions
} = options;
const PortalProviderAPI = usePortalProvider();
const setIsMobile = useToolbarStore((store) => store.setIsMobile);
const closeAllPopups = useToolbarStore((store) => store.closeAllPopups);
const setDownloadOptions = useToolbarStore(
@@ -356,7 +353,6 @@ const useTiptap = (
})
],
onBeforeCreate: ({ editor }) => {
editor.storage.portalProviderAPI = PortalProviderAPI;
editor.storage.dateFormat = dateFormat;
editor.storage.timeFormat = timeFormat;
@@ -378,7 +374,6 @@ const useTiptap = (
downloadAttachment,
openAttachmentPicker,
getAttachmentData,
PortalProviderAPI,
onBeforeCreate,
openLink,
dateFormat,
@@ -403,7 +398,6 @@ const useTiptap = (
export { type Fragment } from "prosemirror-model";
export { type Attachment, type AttachmentType } from "./extensions/attachment";
export { type ImageAttributes } from "./extensions/image";
export * from "./extensions/react";
export * from "./toolbar";
export * from "./types";
export * from "./utils/word-counter";

View File

@@ -66,8 +66,8 @@ export function scrollIntoViewById(blockId: string) {
() =>
element.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest"
block: "start",
inline: "start"
}),
100
);

View File

@@ -33,6 +33,7 @@ import { useTheme } from "@emotion/react";
export type EmotionThemeProviderProps = {
scope?: keyof ThemeScopes;
injectCssVars?: boolean;
theme?: Partial<Theme>;
} & Omit<BoxProps, "variant" | "ref">;
function _EmotionThemeProvider(
@@ -44,6 +45,7 @@ function _EmotionThemeProvider(
scope = "base",
injectCssVars = true,
className,
theme: partialTheme,
...restProps
} = props;
const emotionTheme = useTheme() as Theme;
@@ -66,6 +68,7 @@ function _EmotionThemeProvider(
...(emotionTheme && "space" in emotionTheme
? emotionTheme
: themeProperties),
...partialTheme,
colors: themeProperties.colors
}}
>