Compare commits

..

59 Commits

Author SHA1 Message Date
Ammar Ahmed
fdb8289805 mobile: show diff in note history 2024-05-13 09:39:39 +05:00
Ammar Ahmed
58845e5508 mobile: release v3.0.4 2024-05-11 15:19:12 +05:00
Abdullah Atta
543c6f17ed web: enable sourcemaps for web app 2024-05-11 13:09:59 +05:00
Abdullah Atta
533f95fcff editor: fix editor crash due to TypeError: t.startsWith is not a function 2024-05-11 12:53:30 +05:00
Ammar Ahmed
c02f1cde16 mobile: disable template when pin note to notifications 2024-05-11 12:49:11 +05:00
Ammar Ahmed
043f55ead0 mobile: fix exporting note to text for share/copy 2024-05-11 12:49:11 +05:00
Ammar Ahmed
fcece06a1c mobile: fix update notification not showing on mobile app 2024-05-11 12:49:11 +05:00
Abdullah Atta
da7d79edc6 core: token without a scope is invalid 2024-05-11 12:36:23 +05:00
Ammar Ahmed
a2e7190a4a mobile: fix colors of error popup in editor 2024-05-11 11:26:15 +05:00
Ammar Ahmed
503f2b5876 mobile: fix cannot read property 'groupBy' of undefined 2024-05-11 11:22:59 +05:00
Abdullah Atta
68d57e2b27 web: bump version to 3.0.4 2024-05-11 10:48:48 +05:00
Abdullah Atta
400aa574cd core: fix 2fa codes not being sent 2024-05-11 10:29:02 +05:00
Abdullah Atta
5028bb1275 web: update snapshots 2024-05-10 15:30:35 +05:00
Abdullah Atta
27ae5bb5a2 desktop: use npx prebuildify instead of yarn 2024-05-10 15:24:39 +05:00
Abdullah Atta
83316fe299 web: update snapshots for linux 2024-05-10 15:20:57 +05:00
Ammar Ahmed
8869224de2 mobile: fix image preview not rendered on iOS 2024-05-10 15:04:28 +05:00
Ammar Ahmed
c4152f0fc0 mobile: set editor reset time to 10 minutes 2024-05-10 14:53:10 +05:00
Ammar Ahmed
d803005637 mobile: release v3.0.3 2024-05-10 14:47:27 +05:00
Ammar Ahmed
53d896be2b mobile: fix opening exported files after export 2024-05-10 14:44:12 +05:00
Abdullah Atta
8c37c6409a ci: use actions/upload-artifact@v4 2024-05-10 12:36:56 +05:00
Abdullah Atta
9195244890 web: fix tests 2024-05-10 12:31:55 +05:00
Ammar Ahmed
152794ccd0 mobile: fix image filename in picker 2024-05-10 12:24:40 +05:00
Ammar Ahmed
2e8b234b63 mobile: fix editor content update on realtime sync 2024-05-10 12:24:40 +05:00
Abdullah Atta
055c647698 web: add Audios tab in attachments dialog 2024-05-10 12:08:38 +05:00
Abdullah Atta
2b7bcfe7c6 web: bump version to 3.0.3 2024-05-10 12:00:06 +05:00
Abdullah Atta
3c8b8a7bc9 web: fix real time sync test 2024-05-10 11:58:56 +05:00
Abdullah Atta
fde45fb6de web: saving timeout notice shown every time 2024-05-10 11:58:44 +05:00
Abdullah Atta
4a616a3e67 web: fix tab not updating when note is locked/unlocked 2024-05-10 11:27:26 +05:00
Abdullah Atta
bd0a338dd6 core: fix dateEdited not updating on saving locked content 2024-05-10 10:46:11 +05:00
Abdullah Atta
5ab721e3e2 core: fix async iterator on sql collection only returning dateCreated & id 2024-05-10 10:22:03 +05:00
Ammar Ahmed
cb70bb9429 mobile: fix errors 2024-05-10 09:22:23 +05:00
Abdullah Atta
82cadd473c web: show error if saving takes more than 30 seconds 2024-05-10 09:22:23 +05:00
Ammar Ahmed
0e8ea3ddeb mobile: fix editor loading 2024-05-10 09:22:23 +05:00
Ammar Ahmed
511acc5fca mobile: handle editor not responding 2024-05-10 09:22:23 +05:00
Ammar Ahmed
001cf8ee92 mobile: add editor lock safety 2024-05-10 09:22:23 +05:00
Muhammad Ali
a965cd694b web: fix color not updating on note (#5573) 2024-05-10 09:16:59 +05:00
Ammar Ahmed
d3c1c1959b mobile: fix wording of push/pull sync 2024-05-09 17:39:35 +05:00
Abdullah Atta
9a866a552a web: replace force sync with force push/pull 2024-05-09 17:39:35 +05:00
Ammar Ahmed
837c296821 mobile: push/pull force sync 2024-05-09 17:39:35 +05:00
Abdullah Atta
9aed6faed8 common: fix locked notes not getting exported 2024-05-09 17:39:16 +05:00
Abdullah Atta
734f491e8d editor: fix crash due to undefined is not an object (evaluating 'a.nodeSize') (#5553) 2024-05-09 17:38:18 +05:00
Ammar Ahmed
0ea13024e9 mobile: fix editor crash due to position (#5519)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-05-09 15:55:33 +05:00
Abdullah Atta
cb27039245 web: close shared service on window close 2024-05-09 10:30:46 +05:00
Abdullah Atta
714ac2deac core: do not use auth access tokens as normal access tokens 2024-05-09 10:30:25 +05:00
Abdullah Atta
d605bc29bf desktop: fix crash on toggling custom dns (#5538) 2024-05-09 10:15:14 +05:00
Abdullah Atta
080101bc45 core: fix database disk image malformed error (#5525) 2024-05-09 10:08:44 +05:00
Abdullah Atta
4819e17da3 core: rerun sync if there are unsynced changes after sync complete 2024-05-08 16:48:59 +05:00
Ammar Ahmed
69a5d5a1b6 global: update .gitignore 2024-05-08 10:43:35 +05:00
Ammar Ahmed
68a6d6c313 mobile: fix crash when taking camera picture 2024-05-08 09:48:28 +05:00
Ammar Ahmed
c51ef4d5f6 mobile: fix camera permission 2024-05-08 09:48:28 +05:00
Ammar Ahmed
d9c5c1f893 mobile: remove margin in editor 2024-05-08 09:47:56 +05:00
Ammar Ahmed
5a75ca73a9 mobile: fix editor scrolling on update 2024-05-08 09:47:56 +05:00
Abdullah Atta
626d393376 desktop: use yarn instead of npx for build scripts 2024-05-08 09:14:55 +05:00
Abdullah Atta
68d06ff3f8 core: fix legacy backup restoration 2024-05-07 15:57:22 +05:00
Abdullah Atta
79f20d100c core: fix File already exists error on duplicate downloads 2024-05-07 12:57:51 +05:00
Ammar Ahmed
9d5bec3bfb ci: upload sourcemaps 2024-05-07 11:49:58 +05:00
Abdullah Atta
ac96354b4c core: handle case when updating item while a push is ongoing 2024-05-06 22:04:04 +05:00
Abdullah Atta
63f44d6fbc core: set synced to false on removing content by note id 2024-05-06 15:45:45 +05:00
Abdullah Atta
f9bfa88c83 core: set synced to false on unlinking relations 2024-05-06 15:45:45 +05:00
112 changed files with 2058 additions and 1015 deletions

View File

@@ -147,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -133,7 +133,6 @@ jobs:
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
@@ -148,8 +147,10 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload signed aab to Github
uses: actions/upload-artifact@v2
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: Notesnook.aab
path: ${{steps.sign_app.outputs.signedReleaseFile}}
name: sourcemaps
path: |
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -98,7 +98,10 @@ jobs:
api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Notesnook.ipa
path: Notesnook.ipa
name: Notesnook.zip
path: |
Notesnook.ipa
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -40,7 +40,7 @@ jobs:
run: npm run build:test:web
- name: Archive build artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
@@ -56,7 +56,7 @@ jobs:
uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
@@ -88,7 +88,7 @@ jobs:
working-directory: apps/web
- name: Upload test results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.shard }}

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.0.2",
"version": "3.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.0.2",
"version": "3.0.4",
"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.2",
"version": "3.0.4",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",

View File

@@ -38,7 +38,7 @@ await fs.rm("./build/", { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(
"npx nx build:desktop @notesnook/web",
"yarn nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..")
);
}
@@ -58,15 +58,15 @@ await fs.cp(path.join(webAppPath, "build"), "build", {
});
if (args.variant === "mas") {
await exec(`npm run bundle:mas`);
await exec(`yarn run bundle:mas`);
} else {
await exec(`npm run bundle`);
await exec(`yarn run bundle`);
}
await exec(`npx tsc`);
await exec(`yarn tsc`);
if (args.run) {
await exec(`npx electron-builder --dir --x64`);
await exec(`yarn electron-builder --dir --x64`);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {

View File

@@ -53,11 +53,11 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npx electron-builder install-app-deps");
await exec("yarn electron-builder install-app-deps");
}
await exec(`npm run bundle`);
execAsync(`npx`, [`tsc`]);
await exec(`yarn run bundle`);
execAsync(`yarn`, [`tsc`]);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -66,7 +66,7 @@ async function onChange(first) {
if (first) {
await spawnAndWaitUntil(
["npx", "nx", "start:desktop", "@notesnook/web"],
["yarn", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose")
);
@@ -78,7 +78,7 @@ async function onChange(first) {
}
execAsync(
"npx",
"yarn",
["electron", path.join("build", "electron.js")],
true,
cleanup

View File

@@ -58,11 +58,11 @@ export const osIntegrationRouter = t.router({
customDns: t.procedure.query(() => config.customDns),
setCustomDns: t.procedure
.input(z.boolean())
.input(z.boolean().optional())
.mutation(({ input: customDns }) => {
if (customDns) enableCustomDns();
else disableCustomDns();
config.customDns = customDns;
config.customDns = !!customDns;
}),
proxyRules: t.procedure.query(() => config.proxyRules),

View File

@@ -20,15 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Linking, View } from "react-native";
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Sync from "../../services/sync";
import { useThemeColors } from "@notesnook/theme";
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eCloseAnnouncementDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { PricingPlans } from "../premium/pricing-plans";
import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import { allowedOnPlatform, getStyle } from "./functions";
@@ -56,13 +54,6 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
/>
)
});
} else if (item.type === "force-sync") {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, true, () => {
eSendEvent(eCloseSheet);
});
}
};
return (

View File

@@ -67,7 +67,7 @@ export const Login = ({ changeMode }) => {
Progress.present();
setTimeout(() => {
if (!useUserStore.getState().syncing) {
Sync.run("global", false, true);
Sync.run("global", false, "full");
}
}, 5000);
});

View File

@@ -112,7 +112,7 @@ export const SessionExpired = () => {
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, true, async (complete) => {
Sync.run("global", false, "full", async (complete) => {
if (!complete) {
let user = await db.user.getUser();
if (!user) return;

View File

@@ -17,7 +17,13 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useState } from "react";
import { Image, ScrollView, TouchableOpacity, View } from "react-native";
import {
Image,
Platform,
ScrollView,
TouchableOpacity,
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "../../../../../../packages/theme/dist";
import { presentSheet } from "../../../services/event-manager";
@@ -61,11 +67,14 @@ export default function AttachImage({
Attaching {response?.length} image(s):
</Paragraph>
<ScrollView horizontal>
{response?.map((item) => (
{response?.map?.((item) => (
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
<Image
source={{
uri: item.sourceURL || item.path
uri:
Platform.OS === "ios"
? item.sourceURL || item.path
: item.path
}}
style={{
width: 100,

View File

@@ -586,7 +586,7 @@ export class VaultDialog extends Component {
}
async _copyNote(note) {
Clipboard.setString(await convertNoteToText(note));
Clipboard.setString((await convertNoteToText(note, true)) || "");
ToastManager.show({
heading: "Note copied",
type: "success",
@@ -602,7 +602,7 @@ export class VaultDialog extends Component {
await Share.open({
heading: "Share note",
failOnCancel: false,
message: await convertNoteToText(note)
message: (await convertNoteToText(note)) || ""
});
} catch (e) {
console.error(e);

View File

@@ -80,7 +80,7 @@ export default function List(props: ListProps) {
const groupOptions = useGroupOptions(groupType);
const _onRefresh = async () => {
Sync.run("global", false, true, () => {
Sync.run("global", false, "full", () => {
props.onRefresh?.();
});
};

View File

@@ -21,10 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import Editor from "../../screens/editor";
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { ToastManager, eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useTrashStore } from "../../stores/use-trash-store";
@@ -34,7 +34,7 @@ import DialogHeader from "../dialog/dialog-header";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
import { diff } from "diffblazer";
/**
*
@@ -121,12 +121,21 @@ export default function NotePreview({ session, content, note }) {
<ReadonlyEditor
editorId="historyPreview"
onLoad={async (loadContent) => {
if (content.data) {
const _note = note || (await db.notes.note(session?.noteId));
loadContent({
data: content.data,
id: _note.id
});
try {
if (content.data) {
const _note = note || (await db.notes.note(session?.noteId));
const currentContent = await db.content.get(_note.contentId);
loadContent({
data: diff(currentContent.data, content.data),
id: _note.id
});
}
} catch (e) {
ToastManager.error(
e,
"Failed to load history preview",
"local"
);
}
}}
/>

View File

@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { Fragment, useState } from "react";
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
import {
ActivityIndicator,
Linking,
Platform,
StyleSheet,
View
} from "react-native";
import FileViewer from "react-native-file-viewer";
import Share from "react-native-share";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
@@ -29,14 +35,12 @@ import { requestInAppReview } from "../../../services/app-review";
import {
PresentSheetOptions,
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import Exporter from "../../../services/exporter";
import PremiumService from "../../../services/premium";
import { useUserStore } from "../../../stores/use-user-store";
import { getElevationStyle } from "../../../utils/elevation";
import { eCloseSheet } from "../../../utils/events";
import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
@@ -65,6 +69,7 @@ const ExportNotesSheet = ({
filePath: string;
name: string;
type: string;
fileDir: string;
}
| undefined
>();
@@ -295,7 +300,9 @@ const ExportNotesSheet = ({
successfully as {result?.fileName}
</Paragraph>
<Button
title="Open"
title={
Platform.OS === "android" ? "Open file location" : "Open"
}
type="accent"
width={250}
fontSize={SIZE.md}
@@ -305,20 +312,23 @@ const ExportNotesSheet = ({
}}
onPress={async () => {
if (!result?.filePath) return;
eSendEvent(eCloseSheet);
await sleep(500);
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
console.log(e);
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
context: "local"
if (Platform.OS === "android") {
Linking.openURL(result.fileDir).catch((e) => {
ToastManager.error(e as Error);
});
});
} else {
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
context: "local"
});
});
}
}}
/>
<Button
@@ -331,10 +341,10 @@ const ExportNotesSheet = ({
borderRadius: 100
}}
onPress={async () => {
if (!result?.filePath) return;
if (!result) return;
if (Platform.OS === "ios") {
Share.open({
url: result.filePath
url: result?.fileDir + result.fileName
}).catch(console.log);
} else {
FileViewer.open(result.filePath, {
@@ -347,7 +357,7 @@ const ExportNotesSheet = ({
/>
<Button
title="Export in another format"
type="secondaryAccented"
type="inverted"
width={250}
fontSize={SIZE.md}
style={{

View File

@@ -35,7 +35,10 @@ import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
import { editorController } from "../../../screens/editor/tiptap/utils";
import {
EditorEvents,
editorController
} from "../../../screens/editor/tiptap/utils";
const ListNoteItem = ({
id,
@@ -190,13 +193,13 @@ export default function LinkNote(props: {
}
: undefined
);
editorController.current.commands.createInternalLink(
{
editorController.current?.postMessage(EditorEvents.resolve, {
data: {
href: link,
title: selectedNote.title
},
props.resolverId
);
resolverId: props.resolverId
});
};
const onSelectNote = async (note: Note) => {

View File

@@ -86,16 +86,16 @@ const Sort = ({ type, screen }) => {
<Button
title={
groupOptions?.sortDirection === "asc"
? groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
? groupOptions?.groupBy === "abc" ||
groupOptions?.sortBy === "title"
? "A - Z"
: groupOptions.sortBy === "dueDate"
: groupOptions?.sortBy === "dueDate"
? "Earliest first"
: "Old - New"
: groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
: groupOptions?.groupBy === "abc" ||
groupOptions?.sortBy === "title"
? "Z - A"
: groupOptions.sortBy === "dueDate"
: groupOptions?.sortBy === "dueDate"
? "Latest first"
: "New - Old"
}
@@ -134,7 +134,7 @@ const Sort = ({ type, screen }) => {
alignItems: "center"
}}
>
{groupOptions.groupBy === "abc" ? (
{groupOptions?.groupBy === "abc" ? (
<Button
type="secondary"
title="Title"
@@ -154,18 +154,18 @@ const Sort = ({ type, screen }) => {
item === "dateEdited") ? null : (
<Button
key={item}
type={groupOptions.sortBy === item ? "selected" : "plain"}
type={groupOptions?.sortBy === item ? "selected" : "plain"}
title={SORT[item]}
height={40}
iconPosition="left"
icon={groupOptions.sortBy === item ? "check" : null}
icon={groupOptions?.sortBy === item ? "check" : null}
style={{
marginRight: 10,
paddingHorizontal: 8
}}
buttonType={{
text:
groupOptions.sortBy === item
groupOptions?.sortBy === item
? colors.primary.accent
: colors.secondary.paragraph
}}
@@ -213,11 +213,11 @@ const Sort = ({ type, screen }) => {
key={item}
testID={"btn-" + item}
type={
groupOptions.groupBy === GROUP[item] ? "selected" : "plain"
groupOptions?.groupBy === GROUP[item] ? "selected" : "plain"
}
buttonType={{
text:
groupOptions.groupBy === GROUP[item]
groupOptions?.groupBy === GROUP[item]
? colors.primary.accent
: colors.secondary.paragraph
}}
@@ -235,7 +235,7 @@ const Sort = ({ type, screen }) => {
updateGroupOptions(_groupOptions);
}}
height={40}
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
icon={groupOptions?.groupBy === GROUP[item] ? "check" : null}
title={
item.slice(0, 1).toUpperCase() + item.slice(1, item.length)
}

View File

@@ -600,20 +600,11 @@ export const useActions = ({
});
return;
}
const text = await convertNoteToText(item as Note, false);
if (!text) {
ToastManager.error(
new Error(Errors.export("text")),
undefined,
"local"
);
return;
}
const html = text.replace(/\n/g, "<br />");
const text = await convertNoteToText(item as Note, true);
const html = (text || "").replace(/\n/g, "<br />");
await Notifications.displayNotification({
title: item.title,
message: (item as Note).headline || text,
message: (item as Note).headline || text || "",
subtitle: "",
bigText: html,
ongoing: true,
@@ -684,15 +675,11 @@ export const useActions = ({
} else {
processingId.current = "shareNote";
const convertedText = await convertNoteToText(item);
if (!convertedText) {
ToastManager.error(new Error(Errors.export("text")));
return;
}
processingId.current = undefined;
Share.open({
title: "Share note to",
failOnCancel: false,
message: convertedText
message: convertedText || ""
});
}
}
@@ -772,11 +759,7 @@ export const useActions = ({
} else {
processingId.current = "copyContent";
const text = await convertNoteToText(item as Note, true);
if (!text) {
ToastManager.error(new Error(Errors.export("text")));
return;
}
Clipboard.setString(text);
Clipboard.setString(text || "");
processingId.current = undefined;
ToastManager.show({
heading: "Note copied to clipboard",

View File

@@ -86,6 +86,7 @@ import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants";
import {
eCloseSheet,
eEditorReset,
eLoginSessionExpired,
eOnLoadNote,
eOpenAnnouncementDialog,
@@ -200,11 +201,13 @@ const onRequestPartialSync = async (
`onRequestPartialSync full:${full}, force:${force}, lastSyncTime:${lastSyncTime}`
);
if (full || force) {
await Sync.run("global", force, full, undefined, lastSyncTime);
} else {
await Sync.run("global", false, false, undefined, lastSyncTime);
}
await Sync.run(
"global",
force,
full ? "full" : "send",
undefined,
lastSyncTime
);
};
const onLogout = async (reason: string) => {
@@ -233,7 +236,7 @@ async function checkForShareExtensionLaunchedInBackground() {
if (notesAddedFromIntent || shareExtensionOpened) {
const id = useTabStore.getState().getCurrentNoteId();
const note = id && (await db.notes.note(id));
eSendEvent("webview_reset");
eSendEvent(eEditorReset);
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
}
@@ -530,6 +533,14 @@ export const useAppEvents = () => {
}
//@ts-ignore
globalThis["IS_SHARE_EXTENSION"] = false;
if (
SettingsService.getBackgroundEnterTime() + 60 * 1000 * 10 <
Date.now()
) {
// Reset the editor if the app has been in background for more than 10 minutes.
eSendEvent(eEditorReset);
}
} else {
await saveEditorState();
if (

View File

@@ -39,6 +39,7 @@ import {
eSubscribeEvent
} from "../../services/event-manager";
import {
eEditorReset,
eOnLoadNote,
eUnlockNote,
eUnlockWithBiometrics,
@@ -54,8 +55,10 @@ import { syncTabs, useTabStore } from "./tiptap/use-tab-store";
import {
editorController,
editorState,
openInternalLink
openInternalLink,
randId
} from "./tiptap/utils";
import { tabBarRef } from "../../utils/global-refs";
const style: ViewStyle = {
height: "100%",
@@ -102,24 +105,20 @@ const Editor = React.memo(
noToolbar,
noHeader
});
const renderKey = useRef(`editor-0` + editorId);
const renderKey = useRef(randId("editor-id") + editorId);
useImperativeHandle(ref, () => ({
get: () => editor
}));
useLockedNoteHandler();
const onError = useCallback(() => {
renderKey.current =
renderKey.current === `editor-0`
? `editor-1` + editorId
: `editor-0` + editorId;
renderKey.current = randId("editor-id") + editorId;
editor.state.current.ready = false;
editor.setLoading(true);
}, [editor, editorId]);
useEffect(() => {
const sub = [eSubscribeEvent("webview_reset", onError)];
const sub = [eSubscribeEvent(eEditorReset, onError)];
return () => {
sub.forEach((s) => s?.unsubscribe());
};
@@ -146,6 +145,7 @@ const Editor = React.memo(
nestedScrollEnabled
onError={onError}
injectedJavaScriptBeforeContentLoaded={`
globalThis.__DEV__ = ${__DEV__}
globalThis.readonly=${readonly};
globalThis.noToolbar=${noToolbar};
globalThis.noHeader=${noHeader};
@@ -339,7 +339,7 @@ const useLockedNoteHandler = () => {
}),
eSubscribeEvent(eUnlockWithPassword, onSubmit)
];
if (tabRef.current?.locked) {
if (tabRef.current?.locked && tabBarRef.current?.page() === 2) {
unlock();
}
return () => {

View File

@@ -75,14 +75,16 @@ export function ReadonlyEditor(props: {
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
console.log("Readonly editor loaded.");
props.onLoad?.((content: { data: string; id: string }) => {
noteId.current = content.id;
editorRef.current?.postMessage(
JSON.stringify({
type: "native:html",
value: content.data
})
);
setLoading(false);
setTimeout(() => {
noteId.current = content.id;
editorRef.current?.postMessage(
JSON.stringify({
type: "native:html",
value: content.data
})
);
setLoading(false);
}, 300);
});
} else if (editorMessage.type === EventTypes.getAttachmentData) {
const attachment = (editorMessage.value as any).attachment as Attachment;

View File

@@ -48,5 +48,6 @@ export const EventTypes = {
unlockWithBiometrics: "editor-events:unlock-biometrics",
disableReadonlyMode: "editor-events:disable-readonly-mode",
readonlyEditorLoaded: "readonlyEditorLoaded",
error: "editorError"
error: "editorError",
dbLogger: "editor-events:dbLogger"
};

View File

@@ -41,6 +41,7 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
import { eCloseSheet } from "../../../utils/events";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { basename } from "pathe";
const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({
@@ -108,7 +109,6 @@ const file = async (fileOptions: PickerOptions) => {
return;
}
console.log("file uri: ", uri);
uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
showEncryptionSheet(file);
const hash = await Sodium.hashFile({
@@ -164,13 +164,14 @@ const file = async (fileOptions: PickerOptions) => {
eSendEvent(eCloseSheet);
}, 1000);
} catch (e) {
eSendEvent(eCloseSheet);
ToastManager.show({
heading: (e as Error).message,
message: "You need internet access to attach a file",
type: "error",
context: "global"
});
console.log("attachment error: ", e);
DatabaseLogger.error(e);
}
};
@@ -186,7 +187,12 @@ const camera = async (options: PickerOptions) => {
maxFiles: 10,
writeTempFile: true
})
.then((response) => handleImageResponse(response, options))
.then((response) => {
handleImageResponse(
Array.isArray(response) ? response : [response],
options
);
})
.catch((e) => {
console.log("camera error: ", e);
});
@@ -211,7 +217,12 @@ const gallery = async (options: PickerOptions) => {
cropping: false,
multiple: true
})
.then((response) => handleImageResponse(response, options))
.then((response) =>
handleImageResponse(
Array.isArray(response) ? response : [response],
options
)
)
.catch((e) => {
console.log("gallery error: ", e);
});
@@ -255,6 +266,7 @@ const handleImageResponse = async (
response: Image[],
options: PickerOptions
) => {
console.log(response, "result-file-picker");
const result = await AttachImage.present(response, options.context);
if (!result) return;
const compress = result.compress;
@@ -290,7 +302,12 @@ const handleImageResponse = async (
type: "url"
});
const fileName = image.filename || "image";
const fileName = image.sourceURL
? basename(image.sourceURL)
: image.filename || "image";
console.log("attaching image...", fileName);
console.log("attaching file...");
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;

View File

@@ -37,6 +37,7 @@ export type EditorState = {
isAwaitingResult: boolean;
scrollPosition: number;
overlay?: boolean;
initialLoadCalled?: boolean;
};
export type Settings = {
@@ -74,6 +75,8 @@ export type EditorMessage<T> = {
type: string;
noteId: string;
tabId: number;
resolverId?: string;
hasTimeout?: boolean;
};
export type SavePayload = {
@@ -84,6 +87,7 @@ export type SavePayload = {
sessionHistoryId?: number;
ignoreEdit: boolean;
tabId: number;
pendingChanges?: boolean;
};
export type AppState = {

View File

@@ -77,7 +77,6 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState, openInternalLink } from "./utils";
const publishNote = async () => {
const user = useUserStore.getState().user;
if (!user) {
@@ -354,8 +353,15 @@ export const useEditorEvents = (
const data = event.nativeEvent.data;
const editorMessage = JSON.parse(data) as EditorMessage<any>;
if (editorMessage.hasTimeout && editorMessage.resolverId) {
editor.postMessage(EditorEvents.resolve, {
data: true,
resolverId: editorMessage.resolverId
});
}
if (editorMessage.type === EventTypes.load) {
console.log("Editor loaded");
DatabaseLogger.log("Editor is ready");
editor.onLoad();
return;
}
@@ -383,21 +389,36 @@ export const useEditorEvents = (
content: editorMessage.value.html as string,
noteId: noteId,
tabId: editorMessage.tabId,
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
pendingChanges: editorMessage.value?.pendingChanges
});
break;
case EventTypes.title:
DatabaseLogger.log("EventTypes.title");
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value as string,
title: editorMessage.value?.title as string,
noteId: noteId,
tabId: editorMessage.tabId,
ignoreEdit: false
ignoreEdit: false,
pendingChanges: editorMessage.value?.pendingChanges
});
break;
case EventTypes.logger:
logger.info("[WEBVIEW LOG]", editorMessage.value);
logger.info("[EDITOR LOG]", editorMessage.value);
break;
case EventTypes.dbLogger:
if (editorMessage.value.error) {
DatabaseLogger.error(
editorMessage.value.error,
editorMessage.value.error,
{
message: "[EDITOR_ERROR]" + editorMessage.value.message
}
);
} else {
DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message);
}
break;
case EventTypes.contentchange:
editor.onContentChanged(editorMessage.noteId);
@@ -485,17 +506,17 @@ export const useEditorEvents = (
console.log(
"Got attachment data:",
!!data,
(editorMessage.value as any).resolverId
editorMessage.resolverId
);
editor.postMessage(EditorEvents.attachmentData, {
resolverId: (editorMessage.value as any).resolverId,
editor.postMessage(EditorEvents.resolve, {
resolverId: editorMessage.resolverId,
data
});
})
.catch((e) => {
DatabaseLogger.error(e);
editor.postMessage(EditorEvents.attachmentData, {
resolverId: (editorMessage.value as any).resolverId,
editor.postMessage(EditorEvents.resolve, {
resolverId: editorMessage.resolverId,
data: undefined
});
});
@@ -622,7 +643,7 @@ export const useEditorEvents = (
case EventTypes.createInternalLink: {
LinkNote.present(
editorMessage.value.attributes,
editorMessage.value.resolverId
editorMessage.resolverId as string
);
break;
}

View File

@@ -39,6 +39,7 @@ import { DatabaseLogger, db } from "../../../common/database";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
@@ -46,16 +47,20 @@ import {
import Navigation from "../../../services/navigation";
import Notifications from "../../../services/notifications";
import SettingsService from "../../../services/settings";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useTagStore } from "../../../stores/use-tag-store";
import {
eEditorReset,
eEditorTabFocused,
eOnLoadNote,
eShowMergeDialog,
eUpdateNoteInEditor
} from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import { unlockVault } from "../../../utils/unlock-vault";
import { onNoteCreated } from "../../notes/common";
import Commands from "./commands";
import { EventTypes } from "./editor-events";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { syncTabs, useTabStore } from "./use-tab-store";
@@ -68,6 +73,7 @@ import {
isEditorLoaded,
post
} from "./utils";
import { sleep } from "../../../utils/time";
type NoteWithContent = Note & {
content?: NoteContent<false>;
@@ -145,8 +151,9 @@ export const useEditor = (
useEffect(() => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId) => {
console.log("Editot tab focus changed", lastTabFocused.current, tabId);
if (lastTabFocused.current !== tabId) lock.current = false;
lastTabFocused.current = tabId as number;
console.log(tabId);
});
return () => {
event?.unsubscribe();
@@ -165,10 +172,11 @@ export const useEditor = (
useEffect(() => {
if (loading) {
overlay(true);
state.current.ready = false;
setLoading(false);
}
}, [loading]);
}, [loading, overlay]);
const withTimer = useCallback(
(id: string, fn: () => void, duration: number) => {
@@ -216,7 +224,8 @@ export const useEditor = (
type,
ignoreEdit,
sessionHistoryId: currentSessionHistoryId,
tabId
tabId,
pendingChanges
}: SavePayload) => {
if (currentNotes.current[id as string]?.readonly || readonly) return;
try {
@@ -277,12 +286,26 @@ export const useEditor = (
);
}, 50);
const saveTimer = setTimeout(() => {
DatabaseLogger.log(`Note save timeout: ${id}...`);
ToastManager.error(
new Error(
"Copy your changes and restart the app to avoid data loss. If the issue persists, please report to us at support@streetwriters.co."
),
"Saving note is taking too long",
"global",
15000
);
}, 30 * 1000);
if (!locked) {
DatabaseLogger.log(`Saving note: ${id}...`);
id = await db.notes?.add({ ...noteData });
saved = true;
DatabaseLogger.log(`Note saved: ${id}...`);
clearTimeout(saveTimer);
if (!note && id) {
editorSessionHistory.newSession(id);
if (id) {
@@ -316,9 +339,22 @@ export const useEditor = (
Notifications.pinNote(id as string);
}
} else {
if (!db.vault.unlocked) {
if (pendingChanges) await sleep(3000);
const unlocked = await unlockVault({
title: "Unlock vault to save note",
paragraph: `This note is locked, unlock to save ${
pendingChanges ? "some pending" : ""
} changes`,
context: "global"
});
if (!unlocked)
throw new Error("Could not save note, vault is locked");
}
noteData.contentId = note?.contentId;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await db.vault?.save(noteData as any);
clearTimeout(saveTimer);
}
if (id && useTabStore.getState().getTabForNote(id) === tabId) {
@@ -341,6 +377,16 @@ export const useEditor = (
}
}
if (
id &&
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(EditorEvents.title, title || note?.title, tabId);
postMessage(EditorEvents.html, data, tabId);
currentNotes.current[id] = note;
}
saveCount.current++;
return id;
} catch (e) {
@@ -415,7 +461,11 @@ export const useEditor = (
commands.focus(tabId);
});
} else {
if (!event.item) return;
if (!event.item) {
overlay(false);
return;
}
console.log("LOADING NOTE", event.item.id);
const item = event.item;
const currentTab = useTabStore
@@ -443,7 +493,6 @@ export const useEditor = (
useTabStore.getState().focusTab(tabId);
setTimeout(() => {
if (blockIdRef.current) {
console.log("scrolling to block", blockIdRef.current);
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
@@ -502,6 +551,7 @@ export const useEditor = (
loadingState.current === currentContents.current[item.id]?.data
) {
// If note is already loading, return.
console.log("Note is already loading...");
return;
}
@@ -535,11 +585,8 @@ export const useEditor = (
10000
);
console.log("blockId", blockIdRef.current);
setTimeout(() => {
if (blockIdRef.current) {
console.log("scrolling to block", blockIdRef.current);
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
@@ -573,124 +620,131 @@ export const useEditor = (
data: Note | ContentItem | TrashItem | DeletedItem,
isLocal?: boolean
) => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
try {
await (async () => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
console.log("Removing tab");
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
}
return;
}
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
const tab = useTabStore.getState().getTab(tabId);
const note = data.type === "note" ? data : await db.notes?.note(noteId);
lock.current = true;
// Handle this case where note was locked on another device and synced.
const locked = await db.vaults.itemExists(
currentNotes.current[noteId] as ItemReference
);
if (note) {
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
} else {
useTabStore.getState().updateTab(tabId, {
locked: false,
noteLocked: false
});
commands.setLoading(true, tabId);
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
console.log("Removing tab");
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
}
}
} else if (!tab?.noteLocked && locked) {
// Note lock added.
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
}
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId);
}
commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved",
tabId as number
);
}
console.log("readonly state changed...", note.readonly);
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
}
if (data.type === "tiptap" && note && !isLocal) {
if (lastContentChangeTime.current[noteId] >= data.dateEdited) {
lock.current = false;
return;
}
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data, noteId);
if (!decryptedContent) {
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
} else {
await postMessage(
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContents.current?.data) {
lock.current = false;
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) {
currentContents.current[note.id] = data as UnencryptedContentItem;
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
const tab = useTabStore.getState().getTab(tabId);
const note =
data.type === "note" ? data : await db.notes?.note(noteId);
lock.current = true;
// Handle this case where note was locked on another device and synced.
const locked = await db.vaults.itemExists(
currentNotes.current[noteId] as ItemReference
);
if (note) {
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
} else {
useTabStore.getState().updateTab(tabId, {
locked: false,
noteLocked: false
});
commands.setLoading(true, tabId);
}
}
} else if (!tab?.noteLocked && locked) {
// Note lock added.
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
}
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId);
}
commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved",
tabId as number
);
}
console.log("readonly state changed...", note.readonly);
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
}
}
if (data.type === "tiptap" && note && !isLocal) {
if (lastContentChangeTime.current[noteId] >= data.dateEdited) {
return;
}
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data);
if (!decryptedContent) {
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
} else {
await postMessage(
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContents.current?.data) {
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) {
currentContents.current[note.id] =
data as UnencryptedContentItem;
}
}
}
})();
} catch (e) {
DatabaseLogger.error(e as Error, "Error when applying sync changes");
} finally {
lock.current = false;
}
lock.current = false;
},
[postMessage, commands]
);
@@ -713,7 +767,8 @@ export const useEditor = (
type,
ignoreEdit,
noteId,
tabId
tabId,
pendingChanges
}: {
noteId?: string;
title?: string;
@@ -721,18 +776,29 @@ export const useEditor = (
type: string;
ignoreEdit: boolean;
tabId: number;
pendingChanges?: boolean;
}) => {
DatabaseLogger.log(`Saving content...`);
DatabaseLogger.log(
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
);
if (
lock.current ||
(currentLoadingNoteId.current &&
currentLoadingNoteId.current === noteId)
) {
DatabaseLogger.log(`Skipped saving conent:
DatabaseLogger.log(`Skipped saving content:
lock.current: ${lock.current}
currentLoadingNoteId.current: ${currentLoadingNoteId.current}
`);
if (lock.current) {
setTimeout(() => {
if (lock.current) {
DatabaseLogger.warn("Editor force removed lock after 5 seconds");
lock.current = false;
}
}, 5000);
}
return;
}
@@ -740,7 +806,7 @@ export const useEditor = (
lastContentChangeTime.current[noteId] = Date.now();
}
if (type === EditorEvents.content && noteId) {
if (type === EventTypes.content && noteId) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
@@ -755,7 +821,8 @@ export const useEditor = (
id: noteId,
ignoreEdit,
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
tabId: tabId
tabId: tabId,
pendingChanges
};
withTimer(
noteId || "newnote",
@@ -767,7 +834,16 @@ export const useEditor = (
onChange(params.data);
return;
}
saveNote(params);
if (useSettingStore.getState().isAppLoading) {
const sub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
saveNote(params);
sub();
}
});
} else {
saveNote(params);
}
},
ignoreEdit ? 0 : 150
);
@@ -810,7 +886,7 @@ export const useEditor = (
useTabStore.getState().currentTab
))
) {
eSendEvent("webview_reset", "onReady");
eSendEvent(eEditorReset, "onReady");
return false;
} else {
syncTabs();
@@ -820,7 +896,6 @@ export const useEditor = (
}, [isDefaultEditor, restoreEditorState]);
const onLoad = useCallback(async () => {
if (currentNotes.current) overlay(true);
setTimeout(() => {
postMessage(EditorEvents.theme, theme);
});
@@ -835,12 +910,22 @@ export const useEditor = (
const noteId = useTabStore.getState().getCurrentNoteId();
if (!noteId) {
overlay(false);
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
state.current.currentlyEditing = false;
}
} else if (state.current?.initialLoadCalled) {
const note = currentNotes.current[noteId];
if (note) {
loadNote({
item: note
});
}
}
if (!state.current?.initialLoadCalled) {
state.current.initialLoadCalled = true;
}
overlay(false);
}, [
postMessage,
theme,

View File

@@ -46,7 +46,7 @@ export function editorState() {
return editorController.current?.state.current || defaultState;
}
export const EditorEvents: { [name: string]: string } = {
export const EditorEvents = {
html: "native:html",
updatehtml: "native:updatehtml",
title: "native:title",
@@ -55,7 +55,8 @@ export const EditorEvents: { [name: string]: string } = {
logger: "native:logger",
status: "native:status",
keyboardShown: "native:keyboardShown",
attachmentData: "native:attachment-data"
attachmentData: "native:attachment-data",
resolve: "native:resolve"
};
export function randId(prefix: string) {

View File

@@ -458,7 +458,7 @@ export const settingsGroups: SettingSection[] = [
id: "background-sync",
name: "Background sync (experimental)",
description:
"Periodically wake up the app in background to sync your notes from other devices.",
"Periodically wake up the app in background to run sync.",
type: "switch",
property: "backgroundSync",
onChange: (value) => {
@@ -470,22 +470,43 @@ export const settingsGroups: SettingSection[] = [
}
},
{
id: "sync-issues-fix",
name: "Having problems with sync",
description: "Try force sync to resolve issues with syncing",
icon: "sync-alert",
modifer: async () => {
id: "pull-sync",
name: "Force pull changes",
description: `Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.\n\nThese must only be used for troubleshooting. Using them regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.`,
modifer: () => {
presentDialog({
title: "Force sync",
title: "Force Pull changes",
paragraph:
"If your data on two devices is out of sync even after trying to sync normally. You can run force sync to solve such problems. Usually you should never need to run this otherwise. Force sync means that all your data on this device is reuploaded to the server.",
"This must only be used for troubleshooting. Using this regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
negativeText: "Cancel",
positiveText: "Start",
positivePress: async () => {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, true, () => {
Sync.run("global", true, "fetch", () => {
eSendEvent(eCloseSheet);
});
}
});
}
},
{
id: "push-sync",
name: "Force push changes",
description: `Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.`,
modifer: () => {
presentDialog({
title: "Force Push changes",
paragraph:
"This must only be used for troubleshooting. Using this regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
negativeText: "Cancel",
positiveText: "Start",
positivePress: async () => {
eSendEvent(eCloseSheet);
await sleep(300);
Progress.present();
Sync.run("global", true, "send", () => {
eSendEvent(eCloseSheet);
});
}

View File

@@ -162,7 +162,7 @@ export const ToastManager = {
});
},
hide: () => eSendEvent(eHideToast),
error: (e: Error, title?: string, context?: any) => {
error: (e: Error, title?: string, context?: any, duration = 5000) => {
ToastManager.show({
heading: title,
message: e?.message || "",
@@ -176,7 +176,7 @@ export const ToastManager = {
heading: "Logs copied!",
type: "success",
context: "global",
duration: 5000
duration: duration
});
}
});

View File

@@ -17,40 +17,26 @@ 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 { decode, EntityLevel } from "entities";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
import * as ScopedStorage from "react-native-scoped-storage";
import { zip } from "react-native-zip-archive";
import { DatabaseLogger, db } from "../common/database/index";
import { DatabaseLogger } from "../common/database/index";
import Storage from "../common/database/storage";
import {
exportNote as _exportNote,
ExportableAttachment,
ExportableNote,
exportNotes,
sanitizeFilename
exportNotes
} from "@notesnook/common";
import { Note } from "@notesnook/core";
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
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 BiometricService from "./biometrics";
import { ToastManager } from "./event-manager";
import { cacheDir } from "../common/filesystem/utils";
const MIMETypes = {
txt: "text/plain",
pdf: "application/pdf",
md: "text/markdown",
"md-frontmatter": "text/markdown",
html: "text/html"
};
import { unlockVault } from "../utils/unlock-vault";
const FolderNames: { [name: string]: string } = {
txt: "Text",
@@ -59,16 +45,6 @@ const FolderNames: { [name: string]: string } = {
html: "Html"
};
async function releasePermissions(path: string) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (const uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}
async function getPath(type: string) {
let path =
Platform.OS === "ios" &&
@@ -82,153 +58,11 @@ async function getPath(type: string) {
return path;
}
async function save(
path: string,
data: string,
fileName: string,
extension: "txt" | "pdf" | "md" | "html" | "md-frontmatter"
) {
let uri;
if (Platform.OS === "android") {
uri = await ScopedStorage.writeFile(
path,
data,
`${fileName}.${extension}`,
MIMETypes[extension],
extension === "pdf" ? "base64" : "utf8",
false
);
await releasePermissions(path);
} else {
path = path + fileName + `.${extension}`;
await RNFetchBlob.fs.writeFile(path, data, "utf8");
}
return uri || path;
}
async function makeHtml(note: Note, content?: NoteContent<false>) {
let html = await db.notes.export(note.id, {
format: "html",
contentItem: content
});
if (!html) return "";
html = decode(html, {
level: EntityLevel.HTML
});
return html;
}
async function exportAs(
type: string,
note: Note,
bulk?: boolean,
content?: NoteContent<false>
) {
let data;
switch (type) {
case "html":
{
data = await makeHtml(note, content);
}
break;
case "md":
data = await db.notes.export(note.id, {
format: "md",
contentItem: content
});
break;
case "md-frontmatter":
data = await db.notes.export(note.id, {
format: "md-frontmatter",
contentItem: content
});
break;
case "pdf":
{
const html = await makeHtml(note, content);
const fileName = sanitizeFilename(note.title + Date.now(), {
replacement: "_"
});
const options = {
html: html,
fileName:
Platform.OS === "ios" ? "/exported/PDF/" + fileName : fileName,
width: 595,
height: 852,
bgColor: "#FFFFFF",
padding: 30,
base64: bulk || Platform.OS === "android"
} as { [name: string]: any };
if (Platform.OS === "ios") {
options.directory = "Documents";
}
const res = await RNHTMLtoPDF.convert(options);
data = !bulk && Platform.OS === "ios" ? res.filePath : res.base64;
if (bulk && res.filePath) {
RNFetchBlob.fs.unlink(res.filePath);
}
}
break;
case "txt":
{
data = await db.notes.export(note.id, {
format: "txt",
contentItem: content
});
}
break;
}
return data;
}
async function unlockVault() {
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (biometry && fingerprint) {
const credentials = await BiometricService.getCredentials(
"Unlock vault",
"Unlock vault to export locked notes"
);
if (credentials) {
return db.vault.unlock(credentials.password);
}
}
useSettingStore.getState().setSheetKeyboardHandler(false);
return new Promise((resolve) => {
setImmediate(() => {
presentDialog({
context: "export-notes",
input: true,
secureTextEntry: true,
positiveText: "Unlock",
title: "Unlock vault",
paragraph: "Some exported notes are locked, Unlock to export them",
inputPlaceholder: "Enter password",
positivePress: async (value) => {
const unlocked = await db.vault.unlock(value);
if (!unlocked) {
ToastManager.show({
heading: "Invalid password",
message: "Please enter a valid password",
type: "error",
context: "local"
});
return false;
}
resolve(unlocked);
useSettingStore.getState().setSheetKeyboardHandler(true);
return true;
},
onClose: () => {
resolve(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}
});
});
async function unlockVaultForNoteExport() {
return await unlockVault({
title: "Unlock vault",
paragraph: "Some exported notes are locked, Unlock to export them",
context: "export-notes"
});
}
@@ -285,6 +119,7 @@ async function createZip(
callback: (progress?: string) => void
) {
const fileName = `nn-export-${totalNotes}-${type}-${Date.now()}.zip`;
const dir = path;
try {
callback("Creating zip");
const zipOutputPath =
@@ -292,6 +127,7 @@ async function createZip(
? join(path, fileName)
: join(RNFetchBlob.fs.dirs.CacheDir, fileName);
await zip(cacheFolder, zipOutputPath);
callback("Saving zip file");
if (Platform.OS === "android") {
const file = await ScopedStorage.createFile(
@@ -303,6 +139,8 @@ async function createZip(
await copyFileAsync("file://" + zipOutputPath, path);
await RNFetchBlob.fs.unlink(zipOutputPath);
callback();
} else {
path = zipOutputPath;
}
RNFetchBlob.fs.unlink(cacheFolder);
} catch (e) {
@@ -311,6 +149,7 @@ async function createZip(
return {
filePath: path,
fileDir: dir,
type: "application/zip",
name: "zip",
fileName: fileName,
@@ -388,7 +227,7 @@ async function bulkExport(
let currentAttachmentProgress = 0;
for await (const item of exportNotes(notes, {
format: type,
unlockVault: unlockVault as () => Promise<boolean>
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
})) {
if (item instanceof Error) {
DatabaseLogger.error(item);
@@ -432,7 +271,7 @@ async function exportNote(
let currentAttachmentProgress = 0;
for await (const item of _exportNote(note, {
format: type,
unlockVault: unlockVault as () => Promise<boolean>
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
})) {
if (item instanceof Error) {
DatabaseLogger.error(item);

View File

@@ -952,13 +952,13 @@ async function pinNote(id: string) {
const note = await db.notes.note(id as string);
if (!note) return;
let text = await convertNoteToText(note, false);
let text = await convertNoteToText(note, true);
if (!text) text = "";
const html = text.replace(/\n/g, "<br />");
Notifications.displayNotification({
title: note.title,
message: note.headline || text,
subtitle: note.headline || text,
subtitle: "",
bigText: html,
ongoing: true,
actions: ["UNPIN"],

View File

@@ -191,11 +191,11 @@ function canLockAppInBackground() {
}
let backgroundEnterTime = 0;
function appEnteredBackground() {
if (canLockAppInBackground()) {
backgroundEnterTime = Date.now();
}
backgroundEnterTime = Date.now();
}
const getBackgroundEnterTime = () => backgroundEnterTime;
function shouldLockAppOnEnterForeground() {
if (
useUserStore.getState().disableAppLockRequests ||
@@ -227,7 +227,8 @@ export const SettingsService = {
shouldLockAppOnEnterForeground,
canLockAppInBackground,
appEnteredBackground,
setPrivacyScreen
setPrivacyScreen,
getBackgroundEnterTime
};
init();

View File

@@ -32,19 +32,24 @@ export const ignoredMessages = [
"WebSocket failed to connect",
"Failed to start the HttpConnection before"
];
let pendingSync = undefined;
let syncTimer = 0;
let pendingSync: any = undefined;
let syncTimer: NodeJS.Timeout;
const run = async (
context = "global",
forced = false,
full = true,
onCompleted,
lastSyncTime
type: "full" | "send" | "fetch" = "full",
onCompleted?: (status?: number) => void,
lastSyncTime?: number
) => {
if (useUserStore.getState().syncing) {
DatabaseLogger.info("Sync in progress");
pendingSync = {
full: full
forced,
type: type,
context: context,
onCompleted,
lastSyncTime
};
return;
}
@@ -70,7 +75,7 @@ const run = async (
) {
initAfterSync();
pendingSync = undefined;
return onCompleted?.(false);
return onCompleted?.(SyncStatus.Failed);
}
userstore.setSyncing(true);
@@ -80,9 +85,8 @@ const run = async (
await BackgroundSync.doInBackground(async () => {
try {
await db.sync({
type: full ? "full" : "send",
force: forced,
lastSyncTime
type: type,
force: forced
});
} catch (e) {
error = e;
@@ -95,14 +99,16 @@ const run = async (
} catch (e) {
error = e;
if (
!ignoredMessages.find((message) => e.message?.includes(message)) &&
!ignoredMessages.find((message) =>
(e as Error).message?.includes(message)
) &&
userstore.user &&
status.isConnected &&
status.isInternetReachable
) {
userstore.setSyncing(false, SyncStatus.Failed);
if (status.isConnected && status.isInternetReachable) {
ToastManager.error(e, "Sync failed", context);
ToastManager.error(e as Error, "Sync failed", context);
}
}
@@ -115,7 +121,14 @@ const run = async (
);
onCompleted?.(error ? SyncStatus.Failed : SyncStatus.Passed);
setImmediate(() => {
if (pendingSync) Sync.run("global", false, pendingSync.full);
if (pendingSync)
Sync.run(
pendingSync.context,
pendingSync.forced,
pendingSync.type,
pendingSync.onCompleted,
pendingSync.lastSyncTime
);
});
}
}, 300);

View File

@@ -23,7 +23,7 @@ export const IOS_APPGROUPID = "group.org.streetwriters.notesnook";
export const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
export const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
export const BETA = true;
export const BETA = false;
export const STORE_LINK =
Platform.OS === "ios"

View File

@@ -174,3 +174,4 @@ export const eUnlockWithPassword = "619";
export const eUpdateNoteInEditor = "620";
export const eOnEnterEditor = "621";
export const eOnExitEditor = "622";
export const eEditorReset = "623";

View File

@@ -0,0 +1,91 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/database";
import { presentDialog } from "../components/dialog/functions";
import BiometricService from "../services/biometrics";
import { ToastManager } from "../services/event-manager";
import { useSettingStore } from "../stores/use-setting-store";
let unlockPromise: Promise<any> | undefined = undefined;
export async function unlockVault({
context,
title,
paragraph
}: {
context?: string;
title: string;
paragraph: string;
}) {
if (unlockPromise) {
console.log("Unlocking.... waiting for unlock promise");
return unlockPromise;
}
unlockPromise = new Promise(async (resolve) => {
const result = await (async () => {
if (db.vault.unlocked) return true;
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (biometry && fingerprint) {
const credentials = await BiometricService.getCredentials(
title,
paragraph
);
if (credentials) {
return db.vault.unlock(credentials.password);
}
}
useSettingStore.getState().setSheetKeyboardHandler(false);
return new Promise((resolve) => {
setImmediate(() => {
presentDialog({
context: context,
input: true,
secureTextEntry: true,
positiveText: "Unlock",
title: title,
paragraph: paragraph,
inputPlaceholder: "Enter password",
positivePress: async (value) => {
const unlocked = await db.vault.unlock(value);
if (!unlocked) {
ToastManager.show({
heading: "Invalid password",
message: "Please enter a valid password",
type: "error",
context: "local"
});
return false;
}
resolve(unlocked);
useSettingStore.getState().setSheetKeyboardHandler(true);
return true;
},
onClose: () => {
resolve(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}
});
});
});
})();
unlockPromise = undefined;
resolve(result);
});
return unlockPromise;
}

View File

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

View File

@@ -15,6 +15,8 @@
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<queries>

View File

@@ -1015,7 +1015,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2099;
CURRENT_PROJECT_VERSION = 2101;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1089,7 +1089,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.2;
MARKETING_VERSION = 3.0.4;
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 = 2099;
CURRENT_PROJECT_VERSION = 2101;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1194,7 +1194,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.2;
MARKETING_VERSION = 3.0.4;
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 = 2099;
CURRENT_PROJECT_VERSION = 2101;
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.2;
MARKETING_VERSION = 3.0.4;
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 = 2099;
CURRENT_PROJECT_VERSION = 2101;
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.2;
MARKETING_VERSION = 3.0.4;
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 = 2099;
CURRENT_PROJECT_VERSION = 2101;
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.2;
MARKETING_VERSION = 3.0.4;
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 = 2099;
CURRENT_PROJECT_VERSION = 2101;
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.2;
MARKETING_VERSION = 3.0.4;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -350,7 +350,8 @@ PODS:
- react-native-theme-switch-animation (0.6.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- react-native-webview (11.26.1):
- react-native-webview (13.10.0):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- React-NativeModulesApple (0.72.0):
- React-callinvoker
@@ -907,7 +908,7 @@ SPEC CHECKSUMS:
react-native-share-extension: faed334b1ddf165f1e576fcabd3dc1c9e748bfa9
react-native-sodium: 955bb0dc3ea05f8ea06d5e96cb89d1be7b5d7681
react-native-theme-switch-animation: 220f883f7be290e79f2ab022093ed1a7a5929e6d
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
react-native-webview: 90153193e679163ec257011fa457d02765120210
React-NativeModulesApple: 1d81d927ef1a67a3545a01e14c2e98500bf9b199
React-perflogger: 684a11499a0589cc42135d6d5cc04d0e4e0e261a
React-RCTActionSheet: 00b0a4c382a13b834124fa3f541a7d8d1d56efb9
@@ -958,4 +959,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
COCOAPODS: 1.14.2
COCOAPODS: 1.15.2

View File

@@ -60,7 +60,7 @@
"react-native-swiper-flatlist": "3.2.2",
"react-native-tooltips": "^1.0.3",
"react-native-vector-icons": "9.2.0",
"react-native-webview": "^11.14.1",
"react-native-webview": "^13.10.0",
"react-native-zip-archive": "6.0.9",
"react-native-quick-sqlite": "^8.0.6",
"react-native-theme-switch-animation": "^0.6.0",

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.0.0",
"version": "3.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.0.0",
"version": "3.0.3",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -3234,6 +3234,7 @@
"@szhsin/react-menu": "^4.1.0",
"buffer": "^6.0.3",
"framer-motion": "^10.16.8",
"localforage": "^1.10.0",
"mdi-react": "9.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -7162,7 +7163,7 @@
},
"../../packages/editor-mobile/node_modules/@types/prop-types": {
"version": "15.7.11",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/q": {
@@ -7182,7 +7183,7 @@
},
"../../packages/editor-mobile/node_modules/@types/react": {
"version": "18.2.39",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -7213,7 +7214,7 @@
},
"../../packages/editor-mobile/node_modules/@types/scheduler": {
"version": "0.16.8",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/semver": {
@@ -12071,7 +12072,7 @@
},
"../../packages/editor-mobile/node_modules/immer": {
"version": "9.0.21",
"devOptional": true,
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -22460,6 +22461,7 @@
},
"../../packages/editor/node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
"license": "MIT"
},
"../../packages/editor/node_modules/json-parse-even-better-errors": {
@@ -22515,6 +22517,7 @@
},
"../../packages/editor/node_modules/loose-envify": {
"version": "1.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -23036,6 +23039,7 @@
},
"../../packages/editor/node_modules/react": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -23054,6 +23058,7 @@
},
"../../packages/editor/node_modules/react-dom": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
@@ -23173,6 +23178,7 @@
},
"../../packages/editor/node_modules/scheduler": {
"version": "0.23.0",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -28495,7 +28501,7 @@
"react-native-tooltips": "^1.0.3",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "9.2.0",
"react-native-webview": "^11.14.1",
"react-native-webview": "^13.10.0",
"react-native-zip-archive": "6.0.9"
},
"devDependencies": {
@@ -30890,6 +30896,7 @@
},
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30903,6 +30910,7 @@
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31049,6 +31057,7 @@
},
"node_modules/@babel/plugin-proposal-private-property-in-object": {
"version": "7.21.0-placeholder-for-preset-env.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -31059,6 +31068,7 @@
},
"node_modules/@babel/plugin-proposal-unicode-property-regex": {
"version": "7.18.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -31104,6 +31114,7 @@
},
"node_modules/@babel/plugin-syntax-class-static-block": {
"version": "7.14.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -31140,6 +31151,7 @@
},
"node_modules/@babel/plugin-syntax-export-namespace-from": {
"version": "7.8.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.3"
@@ -31163,6 +31175,7 @@
},
"node_modules/@babel/plugin-syntax-import-assertions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31176,6 +31189,7 @@
},
"node_modules/@babel/plugin-syntax-import-attributes": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31189,6 +31203,7 @@
},
"node_modules/@babel/plugin-syntax-import-meta": {
"version": "7.10.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -31199,6 +31214,7 @@
},
"node_modules/@babel/plugin-syntax-json-strings": {
"version": "7.8.3",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
@@ -31222,6 +31238,7 @@
},
"node_modules/@babel/plugin-syntax-logical-assignment-operators": {
"version": "7.10.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
@@ -31282,6 +31299,7 @@
},
"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"
@@ -31295,6 +31313,7 @@
},
"node_modules/@babel/plugin-syntax-top-level-await": {
"version": "7.14.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
@@ -31321,6 +31340,7 @@
},
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
"version": "7.18.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
@@ -31348,6 +31368,7 @@
},
"node_modules/@babel/plugin-transform-async-generator-functions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.5",
@@ -31405,6 +31426,7 @@
},
"node_modules/@babel/plugin-transform-class-properties": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
@@ -31419,6 +31441,7 @@
},
"node_modules/@babel/plugin-transform-class-static-block": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
@@ -31482,6 +31505,7 @@
},
"node_modules/@babel/plugin-transform-dotall-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -31496,6 +31520,7 @@
},
"node_modules/@babel/plugin-transform-duplicate-keys": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31509,6 +31534,7 @@
},
"node_modules/@babel/plugin-transform-dynamic-import": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31537,6 +31563,7 @@
},
"node_modules/@babel/plugin-transform-export-namespace-from": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31593,6 +31620,7 @@
},
"node_modules/@babel/plugin-transform-json-strings": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31620,6 +31648,7 @@
},
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31647,6 +31676,7 @@
},
"node_modules/@babel/plugin-transform-modules-amd": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
@@ -31676,6 +31706,7 @@
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-hoist-variables": "^7.22.5",
@@ -31692,6 +31723,7 @@
},
"node_modules/@babel/plugin-transform-modules-umd": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
@@ -31720,6 +31752,7 @@
},
"node_modules/@babel/plugin-transform-new-target": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -31733,6 +31766,7 @@
},
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31747,6 +31781,7 @@
},
"node_modules/@babel/plugin-transform-numeric-separator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31774,6 +31809,7 @@
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.5",
@@ -31805,6 +31841,7 @@
},
"node_modules/@babel/plugin-transform-optional-catch-binding": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31819,6 +31856,7 @@
},
"node_modules/@babel/plugin-transform-optional-chaining": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -31847,6 +31885,7 @@
},
"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",
@@ -31861,6 +31900,7 @@
},
"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",
@@ -31960,6 +32000,7 @@
},
"node_modules/@babel/plugin-transform-reserved-words": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32051,6 +32092,7 @@
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32080,6 +32122,7 @@
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -32093,6 +32136,7 @@
},
"node_modules/@babel/plugin-transform-unicode-property-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -32121,6 +32165,7 @@
},
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
@@ -32135,6 +32180,7 @@
},
"node_modules/@babel/preset-env": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.22.5",
@@ -32227,6 +32273,7 @@
},
"node_modules/@babel/preset-env/node_modules/semver": {
"version": "6.3.0",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -32249,6 +32296,7 @@
},
"node_modules/@babel/preset-modules": {
"version": "0.1.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
@@ -34898,6 +34946,7 @@
},
"node_modules/@types/eslint": {
"version": "8.40.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
@@ -34906,6 +34955,7 @@
},
"node_modules/@types/eslint-scope": {
"version": "3.7.4",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
@@ -34914,6 +34964,7 @@
},
"node_modules/@types/estree": {
"version": "1.0.1",
"dev": true,
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
@@ -35362,6 +35413,7 @@
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.6",
@@ -35370,18 +35422,22 @@
},
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
@@ -35391,10 +35447,12 @@
},
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35405,6 +35463,7 @@
},
"node_modules/@webassemblyjs/ieee754": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
@@ -35412,6 +35471,7 @@
},
"node_modules/@webassemblyjs/leb128": {
"version": "1.11.6",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@xtuc/long": "4.2.2"
@@ -35419,10 +35479,12 @@
},
"node_modules/@webassemblyjs/utf8": {
"version": "1.11.6",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35437,6 +35499,7 @@
},
"node_modules/@webassemblyjs/wasm-gen": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35448,6 +35511,7 @@
},
"node_modules/@webassemblyjs/wasm-opt": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35458,6 +35522,7 @@
},
"node_modules/@webassemblyjs/wasm-parser": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35470,6 +35535,7 @@
},
"node_modules/@webassemblyjs/wast-printer": {
"version": "1.11.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
@@ -35527,10 +35593,12 @@
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@yarnpkg/lockfile": {
@@ -35586,6 +35654,7 @@
},
"node_modules/acorn-import-assertions": {
"version": "1.9.0",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8"
@@ -36690,6 +36759,7 @@
},
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
@@ -37790,6 +37860,7 @@
},
"node_modules/enhanced-resolve": {
"version": "5.15.0",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -37902,6 +37973,7 @@
},
"node_modules/es-module-lexer": {
"version": "1.3.0",
"dev": true,
"license": "MIT"
},
"node_modules/es-set-tostringtag": {
@@ -38232,6 +38304,7 @@
},
"node_modules/eslint-scope": {
"version": "5.1.1",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@@ -38243,6 +38316,7 @@
},
"node_modules/eslint-scope/node_modules/estraverse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -38428,6 +38502,7 @@
},
"node_modules/esrecurse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@@ -38438,6 +38513,7 @@
},
"node_modules/estraverse": {
"version": "5.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -38445,6 +38521,7 @@
},
"node_modules/esutils": {
"version": "2.0.3",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@@ -39501,6 +39578,7 @@
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/global": {
@@ -41744,6 +41822,7 @@
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
@@ -42011,6 +42090,7 @@
},
"node_modules/loader-runner": {
"version": "4.3.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
@@ -44662,6 +44742,7 @@
},
"node_modules/randombytes": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
@@ -45372,8 +45453,9 @@
}
},
"node_modules/react-native-webview": {
"version": "11.26.1",
"license": "MIT",
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.10.0.tgz",
"integrity": "sha512-bntBbc3JHBve17NL5fqBWPiOYYDz1VmYUPA0UbsUPHgZj6t7TXUQd4yn2yL//XFKXPDlvMO+MPB9E1uAYpEp/g==",
"dependencies": {
"escape-string-regexp": "2.0.0",
"invariant": "2.2.4"
@@ -45432,6 +45514,7 @@
},
"node_modules/react-test-renderer": {
"version": "18.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"react-is": "^18.2.0",
@@ -45444,10 +45527,12 @@
},
"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"
@@ -46283,6 +46368,7 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
@@ -47338,6 +47424,7 @@
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.9",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
@@ -47370,6 +47457,7 @@
},
"node_modules/terser-webpack-plugin/node_modules/jest-worker": {
"version": "27.5.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -47382,6 +47470,7 @@
},
"node_modules/terser-webpack-plugin/node_modules/supports-color": {
"version": "8.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -48126,6 +48215,7 @@
},
"node_modules/watchpack": {
"version": "2.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
@@ -48148,6 +48238,7 @@
},
"node_modules/webpack": {
"version": "5.88.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -48262,6 +48353,7 @@
},
"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.2",
"version": "3.0.4",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -0,0 +1,12 @@
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
index 5de0845..1b158d8 100644
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
@@ -692,6 +692,7 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
image.putString("mime", options.outMimeType);
image.putInt("size", (int) new File(compressedImagePath).length());
image.putString("modificationDate", String.valueOf(modificationDate));
+ image.putString("sourceURL", path);
if (includeBase64) {
image.putString("data", getBase64StringFromFile(compressedImagePath));

View File

@@ -1,169 +0,0 @@
diff --git a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
index 8e59033..c87febe 100644
--- a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
+++ b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
@@ -1,6 +1,7 @@
package com.imagepicker;
import android.Manifest;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
import android.content.ContentResolver;
@@ -21,6 +22,7 @@ import android.provider.OpenableColumns;
import android.util.Base64;
import android.webkit.MimeTypeMap;
+import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
import androidx.exifinterface.media.ExifInterface;
@@ -36,6 +38,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -266,7 +269,9 @@ public class Utils {
MediaMetadataRetriever m = new MediaMetadataRetriever();
m.setDataSource(context, uri);
int duration = Math.round(Float.parseFloat(m.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION))) / 1000;
- m.release();
+ try {
+ m.release();
+ } catch(Exception e) {}
return duration;
}
@@ -383,7 +388,7 @@ public class Utils {
return fileUris;
}
- static ReadableMap getImageResponseMap(Uri uri, Options options, Context context) {
+ static WritableMap getImageResponseMap(Uri uri, Options options, Context context) {
String fileName = uri.getLastPathSegment();
int[] dimensions = getImageDimensions(uri, context);
@@ -418,13 +423,16 @@ public class Utils {
for(int i = 0; i < fileUris.size(); ++i) {
Uri uri = fileUris.get(i);
+ String fileName = getNameFromURI(context,uri);
if (isImageType(uri, context)) {
if (uri.getScheme().contains("content")) {
uri = getAppSpecificStorageUri(uri, context);
}
uri = resizeImage(uri, context, options);
- assets.pushMap(getImageResponseMap(uri, options, context));
+ WritableMap resMap = getImageResponseMap(uri, options, context);
+ resMap.putString("originalFileName",fileName);
+ assets.pushMap(resMap);
} else if (isVideoType(uri, context)) {
assets.pushMap(getVideoResponseMap(uri, context));
} else {
@@ -438,6 +446,32 @@ public class Utils {
return response;
}
+ /**
+ * Return file name from Uri given.
+ * @param context the context, cannot be null.
+ * @param uri uri request for file name, cannot be null
+ * @return the corresponding display name for file defined in uri or null if error occurs.
+ */
+ @SuppressLint("Range")
+ static String getNameFromURI(@NonNull Context context, @NonNull Uri uri) {
+ String result = null;
+ Cursor c = null;
+ try {
+ c = context.getContentResolver().query(uri, null, null, null, null);
+ c.moveToFirst();
+ result = c.getString(c.getColumnIndex(OpenableColumns.DISPLAY_NAME));
+ }
+ catch (Exception e){
+ // error occurs
+ }
+ finally {
+ if(c != null){
+ c.close();
+ }
+ }
+ return result;
+ }
+
static ReadableMap getErrorMap(String errCode, String errMsg) {
WritableMap map = Arguments.createMap();
map.putString("errorCode", errCode);
diff --git a/node_modules/react-native-image-picker/ios/ImagePickerManager.m b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
index b634ffb..18b8950 100644
--- a/node_modules/react-native-image-picker/ios/ImagePickerManager.m
+++ b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
@@ -96,6 +96,39 @@ - (void) showPickerViewController:(UIViewController *)picker
});
}
+-(NSString *)cachesDirectoryName
+{
+ static NSString *cachePath = nil;
+ if(!cachePath) {
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
+ cachePath = [paths objectAtIndex:0];
+ }
+
+ return cachePath;
+}
+
+///
+/// Builds paths for identifiers in the cache directory
+///
+-(NSString *)pathForName:(NSString *)name
+{
+ NSString *cachePath = [self cachesDirectoryName];
+ NSString *path = [cachePath stringByAppendingPathComponent:name];
+ return path;
+}
+
+#pragma mark - NSData Cache methods
+
+///
+/// Saves the given data to the cache directory
+///
+-(NSString *)saveToCacheDirectory:(NSData *)data withName:(NSString *)name
+{
+ NSString *path = [self pathForName:name];
+ [data writeToFile:path atomically:YES];
+ return path;
+}
+
#pragma mark - Helpers
-(NSMutableDictionary *)mapImageToAsset:(UIImage *)image data:(NSData *)data {
@@ -121,22 +154,14 @@ -(NSMutableDictionary *)mapImageToAsset:(UIImage *)image data:(NSData *)data {
asset[@"type"] = [@"image/" stringByAppendingString:fileType];
NSString *fileName = [self getImageFileName:fileType];
- NSString *path = [[NSTemporaryDirectory() stringByStandardizingPath] stringByAppendingPathComponent:fileName];
- [data writeToFile:path atomically:YES];
+ NSString *path = [self saveToCacheDirectory:data withName:fileName];
if ([self.options[@"includeBase64"] boolValue]) {
asset[@"base64"] = [data base64EncodedStringWithOptions:0];
}
- NSURL *fileURL = [NSURL fileURLWithPath:path];
- asset[@"uri"] = [fileURL absoluteString];
-
- NSNumber *fileSizeValue = nil;
- NSError *fileSizeError = nil;
- [fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError];
- if (fileSizeValue){
- asset[@"fileSize"] = fileSizeValue;
- }
+ asset[@"uri"] = path;
+ asset[@"fileSize"] = [NSNumber numberWithInteger:[data length]];
asset[@"fileName"] = fileName;
asset[@"width"] = @(image.size.width);

View File

@@ -978,7 +978,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "3.0.0",
"version": "3.0.4",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -1 +1 @@
Test note 1 (9) Test note 1 (8) Test note 1 (7) Test note 1 (6) Test note 1 (5) Test note 1 (4) Test note 1 (3) Test note 1 (2) Test note 1 (1) Test note 1 (0) 53ad8e4e40ebebd0f400498d
53ad8e4e40ebebd0f400498dTest note 1 (0) Test note 1 (1) Test note 1 (2) Test note 1 (3) Test note 1 (4) Test note 1 (5) Test note 1 (6) Test note 1 (7) Test note 1 (8) Test note 1 (9)

View File

@@ -1 +1 @@
Test note 2 (9)Test note 2 (8)Test note 2 (7)Test note 2 (6)Test note 2 (5)Test note 2 (4)Test note 2 (3)Test note 2 (2)Test note 2 (1)Test note 2 (0)f054d19e9a2f46eff7b9bb25
f054d19e9a2f46eff7b9bb25Test note 2 (0)Test note 2 (1)Test note 2 (2)Test note 2 (3)Test note 2 (4)Test note 2 (5)Test note 2 (6)Test note 2 (7)Test note 2 (8)Test note 2 (9)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -1 +1 @@
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made

View File

@@ -262,7 +262,7 @@ test("using Shift+Click when no notes are selected should not crash the app", as
info.setTimeout(60 * 1000);
const { notes } = await populateList(page, 5);
await page.reload();
await notes.focus();
const note = await notes.findNote({ title: "Test note 3" });

View File

@@ -123,7 +123,7 @@ export class AppModel {
);
}
async waitForSync(state: "completed" | "synced" = "completed") {
async waitForSync(state: "completed" | "synced" | "syncing" = "completed") {
await this.page
.locator(getTestId(`sync-status-${state}`))
.waitFor({ state: "visible" });

View File

@@ -284,11 +284,9 @@ test("change title of a locked note", async ({ page }) => {
await page.waitForTimeout(150);
await page.reload();
await page.waitForTimeout(500);
const editedNote = await notes.findNote({ title, content: NOTE.content });
await notes.waitForList();
const editedNote = await notes.findNote({ title });
await editedNote?.openLockedNote(PASSWORD);
await notes.editor.waitForLoading();
expect(await note?.getTitle()).toContain(title);
expect(await notes.editor.getTitle()).toContain(title);
});

View File

@@ -17,12 +17,25 @@
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
<style>
img {
max-width: 100% !important;
height: auto !important;
border-radius: 5px;
.image-container {
display: block;
}
.image-container.align-right {
display: flex;
justify-content: end;
}
.image-container.align-center {
display: flex;
justify-content: center;
}
.image-container.float {
float: left;
}
.image-container.float.align-right {
float: right;
}
body {
background-color: transparent !important;
color: #202124;
@@ -166,6 +179,6 @@
</head>
<body>
<h1>Test 1</h1>
<p data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
<p data-block-id="xxx" data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
</body>
</html>

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { test, Browser, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { USER } from "./utils";
import exp from "constants";
async function createDevice(browser: Browser) {
// Create two isolated browser contexts
@@ -40,7 +39,9 @@ async function actAndSync<T>(
) {
const results = await Promise.all([
...actions.filter((a) => !!a),
...devices.map((d) => d.waitForSync("synced"))
...devices.map((d) =>
d.waitForSync("syncing").then(() => d.waitForSync("synced"))
)
]);
await Promise.all(devices.map((d) => d.page.waitForTimeout(2000)));
@@ -61,6 +62,7 @@ test(`edits in a note opened on 2 devices should sync in real-time`, async ({
createDevice(browser),
createDevice(browser)
]);
const [notesA, notesB] = await Promise.all(
[deviceA, deviceB].map((d) => d.goToNotes())
);
@@ -72,14 +74,17 @@ test(`edits in a note opened on 2 devices should sync in real-time`, async ({
if ((await notesB.editor.getContent("text")) !== "")
await actAndSync([deviceA, deviceB], notesB.editor.clear());
await expect(notesA.editor.content).toHaveText("");
await expect(notesB.editor.content).toHaveText("");
await expect(notesA.editor.content).toBeEmpty();
await expect(notesB.editor.content).toBeEmpty();
await actAndSync([deviceA, deviceB], notesB.editor.setContent(newContent));
expect(noteA).toBeDefined();
expect(noteB).toBeDefined();
await expect(notesA.editor.content).toHaveText(newContent);
await expect(notesB.editor.content).toHaveText(newContent);
await (await deviceA.goToSettings())?.logout();
await (await deviceB.goToSettings())?.logout();
});
function makeid(length: number) {

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.0.2",
"version": "3.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.0.2",
"version": "3.0.4",
"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.2",
"version": "3.0.4",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -31,6 +31,8 @@ import {
} from "@notesnook/common";
import Vault from "./vault";
import { ExportStream } from "../utils/streams/export-stream";
import { showToast } from "../utils/toast";
import { confirm } from "./dialog-controller";
export async function exportToPDF(
title: string,
@@ -79,20 +81,36 @@ export async function exportNotes(
format: "pdf" | "md" | "txt" | "html" | "md-frontmatter",
notes: FilteredSelector<Note>
): Promise<boolean> {
return await TaskManager.startTask({
const result = await TaskManager.startTask({
type: "modal",
title: "Exporting notes",
subtitle: "Please wait while your notes are exported.",
action: async (report) => {
const errors: Error[] = [];
const exportStream = new ExportStream(report, (e) => errors.push(e));
await fromAsyncIterator(
_exportNotes(notes, { format, unlockVault: Vault.unlockVault })
)
.pipeThrough(new ExportStream(report))
.pipeThrough(exportStream)
.pipeThrough(createZipStream())
.pipeTo(await createWriteStream("notes.zip"));
return true;
return {
errors,
count: exportStream.progress
};
}
});
confirm({
title: `Exported ${result.count} notes`,
message:
result.errors.length > 0
? `Export completed with ${result.errors.length} errors:
${result.errors.map((e, i) => `${i + 1}. ${e.message}`).join("\n")}`
: "Export completed with 0 errors.",
positiveButtonText: "Okay"
});
return true;
}
const FORMAT_TO_EXT = {
@@ -130,7 +148,9 @@ export async function exportNote(
unlockVault: Vault.unlockVault
})
)
.pipeThrough(new ExportStream(report))
.pipeThrough(
new ExportStream(report, (e) => showToast("error", e.message))
)
.pipeThrough(createZipStream())
.pipeTo(
await createWriteStream(

View File

@@ -75,6 +75,10 @@ export class SharedService<T extends object> extends EventTarget {
{ signal: this.#onClose.signal }
);
window.addEventListener("beforeunload", () => {
this.close();
});
this.proxy = this.#createProxy();
}

View File

@@ -303,7 +303,7 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
break;
}
case "force-sync": {
await appStore.sync(true, true);
await appStore.sync({ type: "full", force: true });
break;
}
case "backup": {

View File

@@ -73,15 +73,44 @@ import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
function saveContent(noteId: string, ignoreEdit: boolean, content: string) {
async function saveContent(
noteId: string,
ignoreEdit: boolean,
content: string
) {
logger.debug("saving content", {
noteId,
ignoreEdit,
length: content.length
});
useEditorStore.getState().saveSessionContent(noteId, ignoreEdit, {
type: "tiptap",
data: content
await Promise.race([
useEditorStore.getState().saveSessionContent(noteId, ignoreEdit, {
type: "tiptap",
data: content
}),
new Promise((_, reject) =>
setTimeout(
() =>
reject(
new Error(
"Saving this note is taking too long. Copy your changes and restart the app to prevent data loss. If the problem persists, please report it to us at support@streetwriters.co."
)
),
30 * 1000
)
)
]).catch((e) => {
const { hide } = showToast(
"error",
(e as Error).message,
[
{
text: "Dismiss",
onClick: () => hide()
}
],
0
);
});
}
const deferredSave = debounceWithId(saveContent, 100);
@@ -246,7 +275,7 @@ function EditorView({
if (!item.locked) return editor.updateContent(item.data);
const result = await db.vault
.decryptContent(item, item.noteId)
.decryptContent(item)
.catch(() => EV.publish(EVENTS.vaultLocked));
if (!result) return;
editor.updateContent(result.data);

View File

@@ -212,7 +212,8 @@ import {
mdiCalendarBlank,
mdiFormatListBulleted,
mdiLink,
mdiWindowClose
mdiWindowClose,
mdiFileMusicOutline
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -516,6 +517,7 @@ export const FilePDF = createIcon(
);
export const FileDocument = createIcon(mdiFileDocumentOutline);
export const FileVideo = createIcon(mdiFileVideoOutline);
export const FileAudio = createIcon(mdiFileMusicOutline);
export const FileGeneral = createIcon(mdiFileOutline);
export const FileWebClip = createIcon(mdiWeb);
export const Unlink = createIcon(mdiLinkOff);

View File

@@ -284,7 +284,8 @@ export default React.memo(Note, function (prevProps, nextProps) {
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified &&
prevProps.attachments?.failed === nextProps.attachments?.failed &&
prevProps.attachments?.total === nextProps.attachments?.total &&
prevProps.locked === nextProps.locked
prevProps.locked === nextProps.locked &&
prevProps.color?.id === nextProps.color?.id
);
});

View File

@@ -37,6 +37,7 @@ import {
Close,
DoubleCheckmark,
Download,
FileAudio,
FileDocument,
FileGeneral,
FileImage,
@@ -111,7 +112,8 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
images: 0,
orphaned: 0,
uploads: 0,
videos: 0
videos: 0,
audio: 0
});
const [selected, setSelected] = useState<string[]>([]);
const [sortBy, setSortBy] = useState<SortOptions>({
@@ -375,7 +377,14 @@ function AttachmentRow(
);
}
type Route = "all" | "images" | "documents" | "videos" | "uploads" | "orphaned";
type Route =
| "all"
| "images"
| "documents"
| "videos"
| "audio"
| "uploads"
| "orphaned";
const routes: { id: Route; icon: Icon; title: string }[] = [
{
@@ -398,6 +407,11 @@ const routes: { id: Route; icon: Icon; title: string }[] = [
icon: FileVideo,
title: "Videos"
},
{
id: "audio",
icon: FileAudio,
title: "Audios"
},
{
id: "uploads",
icon: Uploading,
@@ -525,6 +539,7 @@ async function getCounts(): Promise<Record<Route, number>> {
documents: await db.attachments.documents.count(),
images: await db.attachments.images.count(),
videos: await db.attachments.videos.count(),
audio: await db.attachments.audios.count(),
uploads: await db.attachments.pending.count(),
orphaned: await db.attachments.orphaned.count()
};
@@ -541,5 +556,7 @@ function filterAttachments(route: Route) {
? db.attachments.documents
: route === "orphaned"
? db.attachments.orphaned
: route === "audio"
? db.attachments.audios
: db.attachments.pending;
}

View File

@@ -33,6 +33,7 @@ function ProgressDialog(props) {
try {
props.onDone(await props.action(setProgress));
} catch (e) {
console.error(e);
props.onDone(e);
}
})();

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { SettingsGroup } from "./types";
import { useStore as useAppStore } from "../../stores/app-store";
import { confirm } from "../../common/dialog-controller";
export const SyncSettings: SettingsGroup[] = [
{
@@ -78,14 +79,56 @@ export const SyncSettings: SettingsGroup[] = [
{
key: "force-sync",
title: "Having problems with sync?",
description: "Try force sync to resolve issues with syncing.",
description: `Force push:
Use this if some changes from this device are not appearing on other devices.This will push everything on this device and overwrite whatever is one the server.
Force pull:
Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.
**These must only be used for troubleshooting. Using them regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.**`,
keywords: ["force sync", "sync troubleshoot"],
components: [
{
type: "button",
title: "Force sync",
title: "Force push",
variant: "error",
action: () => useAppStore.getState().sync(true, true)
action: () =>
confirm({
title: "Are you sure?",
message:
"This must only be used for troubleshooting. Using them regularly for sync is **not recommended** and will lead to **unexpected data loss** and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
checks: {
accept: { text: "I understand.", default: false }
},
positiveButtonText: "Proceed",
negativeButtonText: "Cancel"
}).then((result) => {
if (!result || !result.accept) return;
return useAppStore
.getState()
.sync({ force: true, type: "send" });
})
},
{
type: "button",
title: "Force pull",
variant: "error",
action: () =>
confirm({
title: "Are you sure?",
message:
"This must only be used for troubleshooting. Using them regularly for sync is **not recommended** and will lead to **unexpected data loss** and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
checks: {
accept: { text: "I understand.", default: false }
},
positiveButtonText: "Proceed",
negativeButtonText: "Cancel"
}).then((result) => {
if (!result || !result.accept) return;
return useAppStore
.getState()
.sync({ force: true, type: "fetch" });
})
}
]
}

View File

@@ -41,6 +41,7 @@ import {
} from "../utils/page-visibility";
import { NetworkCheck } from "../utils/network-check";
import { Color, Notebook, Tag } from "@notesnook/core";
import { SyncOptions } from "@notesnook/core/dist/api/sync";
type SyncState =
| "synced"
@@ -57,7 +58,7 @@ type SyncStatus = {
};
const networkCheck = new NetworkCheck();
let syncStatusTimeout = 0;
let pendingSync: { full: boolean } | undefined = undefined;
let pendingSync: SyncOptions | undefined = undefined;
class AppStore extends BaseStore<AppStore> {
// default state
@@ -69,11 +70,7 @@ class AppStore extends BaseStore<AppStore> {
isSyncEnabled = Config.get("syncEnabled", true);
isRealtimeSyncEnabled = Config.get("isRealtimeSyncEnabled", true);
syncStatus: SyncStatus = {
key: navigator.onLine
? Config.get("syncEnabled", true)
? "synced"
: "disabled"
: "offline",
key: navigator.onLine ? "disabled" : "offline",
progress: null,
type: undefined
};
@@ -115,9 +112,9 @@ class AppStore extends BaseStore<AppStore> {
db.eventManager.subscribe(
EVENTS.databaseSyncRequested,
async (full, force, lastSynced) => {
async (full, force) => {
if (!this.get().isAutoSyncEnabled) return;
await this.get().sync(full, force, lastSynced);
await this.get().sync({ type: full ? "full" : "send", force });
}
);
@@ -268,7 +265,7 @@ class AppStore extends BaseStore<AppStore> {
this.set((state) => (state.lastSynced = lastSynced));
};
sync = async (full = true, force = false, lastSynced?: number) => {
sync = async (options: SyncOptions = { type: "full", force: false }) => {
if (
this.isSyncing() ||
!this.get().isSyncEnabled ||
@@ -276,14 +273,12 @@ class AppStore extends BaseStore<AppStore> {
!(await networkCheck.waitForInternet())
) {
logger.info("Ignoring duplicate sync", {
full,
force,
lastSynced,
options,
syncing: this.isSyncing(),
syncDisabled: !this.get().isSyncEnabled,
offline: !navigator.onLine
});
if (this.isSyncing()) pendingSync = { full };
if (this.isSyncing()) pendingSync = options;
return;
}
@@ -292,10 +287,7 @@ class AppStore extends BaseStore<AppStore> {
this.updateSyncStatus("syncing");
try {
const result = await db.sync({
type: full ? "full" : "send",
force
});
const result = await db.sync(options);
if (!result) return this.updateSyncStatus("failed");
this.updateSyncStatus("completed", true);
@@ -304,9 +296,9 @@ class AppStore extends BaseStore<AppStore> {
if (pendingSync) {
logger.info("Running pending sync", pendingSync);
const isFullSync = pendingSync.full;
const syncOptions = { ...pendingSync };
pendingSync = undefined;
await this.get().sync(isFullSync, false);
await this.get().sync(syncOptions);
}
} catch (err) {
if (!(err instanceof Error)) {
@@ -316,9 +308,6 @@ class AppStore extends BaseStore<AppStore> {
logger.error(err);
if (err.cause === "MERGE_CONFLICT") {
// TODO: reopen conflicted note
// const sessionId = editorstore.get().session.id;
// if (sessionId) await editorstore.openSession(sessionId, true);
await this.refresh();
this.updateSyncStatus("conflicts");
} else {

View File

@@ -47,6 +47,7 @@ import { getFormattedHistorySessionDate } from "@notesnook/common";
import { isCipher } from "@notesnook/core/dist/database/crypto";
import { hashNavigate } from "../navigation";
import { AppEventManager, AppEvents } from "../common/app-events";
import Vault from "../common/vault";
export enum SaveState {
NotSaved = -1,
@@ -222,8 +223,15 @@ class EditorStore extends BaseStore<EditorStore> {
const clearIds: string[] = [];
for (const session of sessions) {
if (session.type === "new") continue;
if (session.id !== item.id && session.note.id !== item.id) continue;
const noteId = isDeleted(item)
? null
: item.type === "note"
? item.id
: item.type === "tiptap"
? item.noteId
: null;
if (noteId && session.id !== noteId && session.note.id !== noteId)
continue;
if (isDeleted(item) || isTrashItem(item)) clearIds.push(session.id);
// if a note is locked, reopen the session
else if (
@@ -232,21 +240,26 @@ class EditorStore extends BaseStore<EditorStore> {
item.type === "tiptap" &&
item.locked
) {
openSession(item.id, { force: true, silent: true });
waitForSync().then(() =>
openSession(session.note.id, { force: true, silent: true })
);
}
// if locked note is unlocked, reopen the session
else if (
session.type === "locked" ||
(session.type === "default" &&
session.locked &&
item.type === "tiptap" &&
!item.locked)
(session.type === "locked" ||
(session.type === "default" && session.locked)) &&
item.type === "tiptap" &&
!item.locked
) {
openSession(item.id, { force: true, silent: true });
waitForSync().then(() =>
openSession(session.note.id, { force: true, silent: true })
);
}
// if a deleted note is restored, reopen the session
else if (session.type === "deleted" && item.type === "note") {
openSession(item.id, { force: true, silent: true });
waitForSync().then(() =>
openSession(session.note.id, { force: true, silent: true })
);
}
// if a readonly note is made editable, reopen the session
else if (
@@ -254,7 +267,9 @@ class EditorStore extends BaseStore<EditorStore> {
item.type === "note" &&
!item.readonly
)
openSession(item.id, { force: true, silent: true });
waitForSync().then(() =>
openSession(session.note.id, { force: true, silent: true })
);
// update the note in all sessions
else if (item.type === "note") {
updateSession(
@@ -328,12 +343,15 @@ class EditorStore extends BaseStore<EditorStore> {
const contentId =
session.type !== "new" && session.note.contentId;
if (!contentId || !event.ids.includes(contentId)) continue;
if (
// when a note is locked or unlocked
session.type !== "locked" &&
!!event.item.locked &&
(session.type !== "default" || !session.locked)
// if note is locked
(session.type === "default" &&
!session.locked &&
event.item.locked) ||
// if note is unlocked
((session.type === "locked" ||
(session.type === "default" && session.locked)) &&
!event.item.locked)
) {
openSession(session.id, { force: true, silent: true });
}
@@ -600,7 +618,7 @@ class EditorStore extends BaseStore<EditorStore> {
: undefined;
if (content?.locked) {
await db.vault.add(noteId);
await Vault.lockNote(noteId);
return this.openSession(note, { ...options, force: true });
}
@@ -963,3 +981,10 @@ function getSessionId(session: DefaultEditorSession | NewEditorSession) {
if (sessionId + SESSION_DURATION < Date.now()) return `${Date.now()}`;
return `${sessionId}`;
}
async function waitForSync() {
if (!appStore.get().isSyncing()) return true;
return new Promise((resolve) => {
db.eventManager.subscribe(EVENTS.syncCompleted, resolve, true);
});
}

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ExportableItem } from "@notesnook/common";
import { db } from "../../common/db";
import { showToast } from "../toast";
import { ZipFile } from "./zip-stream";
import { lazify } from "../lazify";
@@ -27,12 +26,15 @@ export class ExportStream extends TransformStream<
ExportableItem | Error,
ZipFile
> {
constructor(report: (progress: { text: string; current?: number }) => void) {
let progress = 0;
progress = 0;
constructor(
report: (progress: { text: string; current?: number }) => void,
handleError: (error: Error) => void
) {
super({
async transform(item, controller) {
transform: async (item, controller) => {
if (item instanceof Error) {
showToast("error", item.message);
handleError(item);
return;
}
if (item.type === "attachment") {
@@ -56,13 +58,13 @@ export class ExportStream extends TransformStream<
if (!stream) return;
controller.enqueue({ ...item, data: stream });
report({
current: progress++,
current: this.progress++,
text: `Saving attachment: ${item.path}`
});
} else {
controller.enqueue(item);
report({
current: progress++,
current: this.progress++,
text: `Exporting note: ${item.path}`
});
}

View File

@@ -56,6 +56,7 @@ export default defineConfig({
minify: "esbuild",
cssMinify: true,
emptyOutDir: true,
sourcemap: !isDesktop,
rollupOptions: {
output: {
plugins: [emitEditorStyles()],

View File

@@ -0,0 +1,4 @@
- Added push/pull changes to troubleshoot sync issues in settings
- 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

@@ -104,31 +104,37 @@ export async function* exportNotes(
continue;
}
const content = await exportContent(note, {
unlockVault: options.unlockVault,
format,
attachmentsRoot,
pendingAttachments,
resolveInternalLink: (link) => {
const internalLink = parseInternalLink(link);
if (!internalLink) return link;
const paths = notePathMap.get(internalLink.id);
if (!paths) return link;
// if the internal link is linking within the same note
if (paths === notePaths) return `{{NOTE_PATH:}}`;
return `{{NOTE_PATH:${paths[0]}}}`;
}
});
if (!content) continue;
try {
const content = await exportContent(note, {
unlockVault: options.unlockVault,
format,
attachmentsRoot,
pendingAttachments,
resolveInternalLink: (link) => {
const internalLink = parseInternalLink(link);
if (!internalLink) return link;
const paths = notePathMap.get(internalLink.id);
if (!paths) return link;
// if the internal link is linking within the same note
if (paths === notePaths) return `{{NOTE_PATH:}}`;
return `{{NOTE_PATH:${paths[0]}}}`;
}
});
if (!content) continue;
for (const path of notePaths) {
yield <ExportableNote>{
type: "note",
path,
data: resolvePaths(content, path),
mtime: new Date(note.dateEdited),
ctime: new Date(note.dateCreated)
};
for (const path of notePaths) {
yield <ExportableNote>{
type: "note",
path,
data: resolvePaths(content, path),
mtime: new Date(note.dateEdited),
ctime: new Date(note.dateCreated)
};
}
} catch (e) {
yield new Error(
`Failed to export note "${note.title}": ${(e as Error).message}`
);
}
}
@@ -225,10 +231,15 @@ export async function exportContent(
}
const contentItem = rawContent?.locked
? await database.vault
.decryptContent(rawContent, note.id)
.catch(() => undefined)
: rawContent;
? await database.vault.decryptContent(rawContent)
: // .catch((e) => {
// console.error(e, note);
// return <NoteContent<false>>{
// type: "tiptap",
// data: `This note could not be decrypted: ${e}`
// };
// })
rawContent;
const { data, type } =
format === "pdf"

View File

@@ -95,6 +95,67 @@ test(
TEST_TIMEOUT
);
test(
"edge case 1: items updated while push is running",
async (t) => {
const [deviceA, deviceB] = await Promise.all([
initializeDevice("deviceA"),
initializeDevice("deviceB")
]);
t.onTestFinished(async () => {
console.log(`${t.task.name} log out`);
await cleanup(deviceA, deviceB);
});
const id = await deviceA.notes.add({ title: "hello" });
for (let i = 0; i < 5; ++i) {
if (i > 0) await deviceA.notes.add({ id, title: `edit ${i - 1}` });
await Promise.all([
deviceA.sync({ type: "send" }),
new Promise((resolve) => setTimeout(resolve, 40)).then(() =>
deviceA.notes.add({ id, title: `edit ${i}` })
)
]);
expect((await deviceA.notes.note(id))?.title).toBe(`edit ${i}`);
expect((await deviceA.notes.note(id))?.synced).toBe(true);
await deviceB.sync({ type: "fetch" });
expect((await deviceB.notes.note(id))?.title).toBe(`edit ${i}`);
}
},
TEST_TIMEOUT * 10
);
test(
"edge case 2: new items added while push is running",
async (t) => {
const [deviceA, deviceB] = await Promise.all([
initializeDevice("deviceA"),
initializeDevice("deviceB")
]);
t.onTestFinished(async () => {
console.log(`${t.task.name} log out`);
await cleanup(deviceA, deviceB);
});
const id = await deviceA.notes.add({ title: "hello" });
for (let i = 0; i < 5; ++i) {
if (i > 0) await deviceA.notes.add({ id, title: `edit ${i - 1}` });
await Promise.all([
deviceA.sync({ type: "send" }),
new Promise((resolve) => setTimeout(resolve, 40)).then(() =>
deviceA.notes.add({ title: `note ${i}` })
)
]);
await deviceB.sync({ type: "fetch" });
expect(await deviceB.notes.all.count()).toBe(i + 2);
}
},
TEST_TIMEOUT * 10
);
// test(
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
// async () => {

View File

@@ -51,7 +51,7 @@ async function writeEncryptedBase64(
hashType,
iv: "some iv",
salt: key.salt!,
length: data.length
size: data.length
};
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect, test } from "vitest";
import { TEST_NOTE, databaseTest, noteTest } from "./utils";
import { databaseTest, noteTest } from "./utils";
test("updating deleted content should not throw", () =>
databaseTest().then(async (db) => {

View File

@@ -144,6 +144,7 @@ test("save an edited locked note", () =>
expect(content.data.cipher).toBeTypeOf("string");
expect(() => JSON.parse(content.data.cipher)).toThrow();
expect(note.dateEdited).toBeLessThan((await db.notes.note(id)).dateEdited);
expect(note.dateEdited).toBeLessThan(content.dateEdited);
}));
test("change vault password", () =>

View File

@@ -95,6 +95,7 @@ class Database {
isInitialized = false;
eventManager = new EventManager();
sseMutex = new Mutex();
_fs?: FileStorage;
storage: StorageAccessor = () => {
if (!this.options?.storage)
@@ -109,7 +110,10 @@ class Database {
throw new Error(
"Database not initialized. Did you forget to call db.setup()?"
);
return new FileStorage(this.options.fs, this.tokenManager);
return (
this._fs ||
(this._fs = new FileStorage(this.options.fs, this.tokenManager))
);
};
crypto: CryptoAccessor = () => {

View File

@@ -25,6 +25,7 @@ import { AnyColumnWithTable, Kysely, sql } from "kysely";
import { FilteredSelector } from "../database/sql-collection";
import { VirtualizedGrouping } from "../utils/virtualized-grouping";
import { logger } from "../logger";
import { rebuildSearchIndex } from "../database/fts";
type SearchResults<T> = {
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
@@ -245,4 +246,9 @@ export default class Lookup {
if (!ids.length) return [];
return selector.items(ids);
}
async rebuild() {
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
await rebuildSearchIndex(db);
}
}

View File

@@ -92,7 +92,10 @@ class MFAManager {
}
async sendCode(method: "sms" | "email") {
const token = await this.tokenManager.getAccessToken();
const token = await this.tokenManager.getAccessToken([
"IdentityServerApi",
"auth:grant_types:mfa"
]);
if (!token) throw new Error("Unauthorized.");
return await http.post(

View File

@@ -90,6 +90,25 @@ test("localOnly note should get included as a deleted item in collector", () =>
expect(items[1].type).toBe("note");
}));
test("unlinked relation should get included in collector", () =>
databaseTest().then(async (db) => {
await loginFakeUser(db);
const collector = new Collector(db);
await db.relations.add(
{ id: "h", type: "note" },
{ id: "h", type: "attachment" }
);
await iteratorToArray(collector.collect(100, false));
await db.relations.from({ id: "h", type: "note" }, "attachment").unlink();
const items = await iteratorToArray(collector.collect(100, false));
expect(items).toHaveLength(1);
expect(items[0].items[0].id).toBe("cd93df7a4c64fbd5f100361d629ac5b5");
}));
async function iteratorToArray(iterator) {
let items = [];
for await (const item of iterator) {

View File

@@ -33,6 +33,15 @@ class Collector {
logger = logger.scope("SyncCollector");
constructor(private readonly db: Database) {}
async hasUnsyncedChanges() {
for (const itemType of SYNC_ITEM_TYPES) {
const collectionKey = SYNC_COLLECTIONS_MAP[itemType];
const collection = this.db[collectionKey].collection;
if ((await collection.unsyncedCount()) > 0) return true;
}
return false;
}
async *collect(
chunkSize: number,
isForceSync = false
@@ -46,6 +55,7 @@ class Collector {
for (const itemType of SYNC_ITEM_TYPES) {
const collectionKey = SYNC_COLLECTIONS_MAP[itemType];
const collection = this.db[collectionKey].collection;
let pushTimestamp = Date.now();
for await (const chunk of collection.unsynced(chunkSize, isForceSync)) {
const items = await this.prepareChunk(chunk, key);
if (!items) continue;
@@ -54,8 +64,20 @@ class Collector {
await collection.update(
chunk.map((i) => i.id),
{ synced: true },
{ sendEvent: false }
{
sendEvent: false,
// EDGE CASE:
// Sometimes an item can get updated while it's being pushed.
// The result is that its `synced` property becomes true even
// though it's modification wasn't yet synced.
// In order to prevent that, we only set the `synced` property
// to true for items that haven't been modified since we last ran
// the push. Everything else will be collected again in the next
// push.
condition: (eb) => eb("dateModified", "<=", pushTimestamp)
}
);
pushTimestamp = Date.now();
}
}
}

View File

@@ -160,7 +160,7 @@ class Sync {
)
this.logger.info("New data sent");
await this.stop();
await this.stop(options);
if (!(await checkSyncStatus(SYNC_CHECK_IDS.autoSync))) {
await this.connection.stop();
@@ -265,7 +265,15 @@ class Sync {
return true;
}
async stop() {
async stop(options: SyncOptions) {
if (
(options.type === "send" || options.type === "full") &&
(await this.collector.hasUnsyncedChanges())
) {
this.logger.info("Changes made during last sync. Syncing again...");
await this.start({ type: "send" });
return;
}
// refresh monographs
await this.db.monographs.refresh().catch(this.logger.error);
// update trash cache

View File

@@ -32,6 +32,15 @@ export type Token = {
refresh_token: string;
};
type Scope = (typeof SCOPES)[number];
const SCOPES = [
"notesnook.sync",
"offline_access",
"IdentityServerApi",
"auth:grant_types:mfa",
"auth:grant_types:mfa_password"
] as const;
const ENDPOINTS = {
token: "/connect/token",
revoke: "/connect/revocation",
@@ -79,10 +88,14 @@ class TokenManager {
return scopes.includes("offline_access") && Boolean(refresh_token);
}
async getAccessToken(forceRenew = false) {
async getAccessToken(
scopes: Scope[] = ["notesnook.sync", "IdentityServerApi"],
forceRenew = false
) {
return await getSafeToken(async () => {
const token = await this.getToken(true, forceRenew);
if (!token) return;
if (!token || !token.scope) return;
if (!scopes.some((s) => token.scope.includes(s))) return;
return token.access_token;
}, "Error getting access token:");
}

View File

@@ -99,8 +99,9 @@ class UserManager {
async authenticateMultiFactorCode(code: string, method: string) {
if (!code || !method) throw new Error("code & method are required.");
const token = await this.tokenManager.getAccessToken();
if (!token) throw new Error("Unauthorized.");
const token = await this.tokenManager.getToken();
if (!token || token.scope !== "auth:grant_types:mfa")
throw new Error("No token found.");
await this.tokenManager.saveToken(
await http.post(
@@ -111,7 +112,7 @@ class UserManager {
"mfa:code": code,
"mfa:method": method
},
token
token.access_token
)
);
return true;
@@ -126,7 +127,8 @@ class UserManager {
if (!email || !password) throw new Error("email & password are required.");
const token = await this.tokenManager.getToken();
if (!token) throw new Error("No token found.");
if (!token || token.scope !== "auth:grant_types:mfa_password")
throw new Error("No token found.");
email = email.toLowerCase();
if (!hashedPassword) {
@@ -189,7 +191,7 @@ class UserManager {
username: email,
password: hashedPassword,
grant_type: code ? "mfa" : "password",
scope: "notesnook.sync offline_access openid IdentityServerApi",
scope: "notesnook.sync offline_access IdentityServerApi",
client_id: "notesnook",
"mfa:code": code,
"mfa:method": method

View File

@@ -255,6 +255,7 @@ export default class Vault {
noteId,
sessionId,
data: encryptedContent,
dateEdited: Date.now(),
type: content.type
});
}

View File

@@ -197,7 +197,7 @@ export class Content implements ICollection {
await this.db
.sql()
.replaceInto("content")
.columns(["id", "dateModified", "deleted"])
.columns(["id", "dateModified", "deleted", "synced"])
.expression((eb) =>
eb
.selectFrom("content")
@@ -205,7 +205,8 @@ export class Content implements ICollection {
.select((eb) => [
"content.id",
eb.lit(Date.now()).as("dateModified"),
eb.lit(1).as("deleted")
eb.lit(1).as("deleted"),
eb.lit(0).as("synced")
])
)
.execute();

View File

@@ -154,7 +154,7 @@ export class Relations implements ICollection {
await this.db
.sql()
.replaceInto("relations")
.columns(["id", "dateModified", "deleted"])
.columns(["id", "dateModified", "deleted", "synced"])
.expression((eb) =>
eb
.selectFrom("relations")
@@ -169,7 +169,8 @@ export class Relations implements ICollection {
.select((eb) => [
"relations.id",
eb.lit(Date.now()).as("dateModified"),
eb.lit(1).as("deleted")
eb.lit(1).as("deleted"),
eb.lit(0).as("synced")
])
)
.execute();
@@ -265,7 +266,7 @@ class RelationsArray<TType extends keyof RelatableTable> {
await this.db
.sql()
.replaceInto("relations")
.columns(["id", "dateModified", "deleted"])
.columns(["id", "dateModified", "deleted", "synced"])
.expression((eb) =>
eb
.selectFrom("relations")
@@ -274,7 +275,8 @@ class RelationsArray<TType extends keyof RelatableTable> {
.select((eb) => [
"relations.id",
eb.lit(Date.now()).as("dateModified"),
eb.lit(1).as("deleted")
eb.lit(true).as("deleted"),
eb.lit(false).as("synced")
])
)
.execute();

View File

@@ -22,10 +22,12 @@ import { CURRENT_DATABASE_VERSION } from "../common.js";
import Migrator from "./migrator.js";
import Database from "../api/index.js";
import {
Attachment,
Item,
MaybeDeletedItem,
Note,
Notebook,
Relation,
ValueOf,
isDeleted
} from "../types.js";
@@ -36,6 +38,7 @@ import { DatabaseCollection } from "./index.js";
import { DefaultColors } from "../collections/colors.js";
import { toChunks } from "../utils/array.js";
import { logger } from "../logger.js";
import { clone } from "../utils/clone.js";
type BackupDataItem = MaybeDeletedItem<Item> | string[];
type BackupPlatform = "web" | "mobile" | "node";
@@ -92,10 +95,44 @@ function isEncryptedBackup(
return "encrypted" in backup ? backup.encrypted : isCipher(backup.data);
}
function isLegacyBackupFile(
backup: LegacyBackupFile | BackupFile
): backup is LegacyBackupFile {
return backup.version <= 5.8;
/**
* Due to a bug in v3.0, legacy backups were created with version set to 6.1
* while their actual data was at version 5.9. This caused various issues when
* restoring such a backup.
* This function tries to work around that bug by detecting the version based on
* the actual data.
*/
function isLegacyBackup(data: BackupDataItem[]) {
const note = data.find(
(c): c is Note => !isDeleted(c) && !Array.isArray(c) && c.type === "note"
);
if (note)
return (
"color" in note ||
"notebooks" in note ||
"tags" in note ||
"locked" in note
);
const notebook = data.find(
(c): c is Notebook =>
!isDeleted(c) && !Array.isArray(c) && c.type === "notebook"
);
if (notebook) return "topics" in notebook;
const attachment = data.find(
(c): c is Attachment =>
!isDeleted(c) && !Array.isArray(c) && c.type === "attachment"
);
if (attachment) return "noteIds" in attachment;
const relation = data.find(
(c): c is Relation =>
!isDeleted(c) && !Array.isArray(c) && c.type === "relation"
);
if (relation) return "from" in relation || "to" in relation;
return false;
}
const MAX_CHUNK_SIZE = 10 * 1024 * 1024;
@@ -221,7 +258,7 @@ export default class Backup {
yield {
path: `${chunkIndex++}-${encrypt ? "encrypted" : "plain"}-${hash}`,
data: `{
"version": ${CURRENT_DATABASE_VERSION},
"version": 5.9,
"type": "${type}",
"date": ${Date.now()},
"data": ${itemsJSON},
@@ -397,13 +434,16 @@ export default class Backup {
if (!data) throw new Error("No data found.");
const normalizedData: BackupDataItem[] = Array.isArray(data)
? (data as BackupDataItem[])
: typeof data === "object"
? Object.values(data)
: [];
await this.migrateData(
Array.isArray(data)
? (data as BackupDataItem[])
: typeof data === "object"
? Object.values(data)
: [],
backup.version
normalizedData,
backup.version === 6.1 && isLegacyBackup(normalizedData)
? 5.9
: backup.version
);
}

View File

@@ -30,16 +30,22 @@ import { logger } from "../logger";
export type FileStorageAccessor = () => FileStorage;
export type DownloadableFile = {
filename: string;
// metadata: AttachmentMetadata;
chunkSize: number;
};
export type QueueItem = DownloadableFile & {
cancel?: (reason?: string) => Promise<void>;
operation?: Promise<boolean>;
};
export class FileStorage {
downloads = new Map<string, QueueItem[]>();
uploads = new Map<string, QueueItem[]>();
id = Date.now();
downloads = new Map<string, QueueItem>();
uploads = new Map<string, QueueItem>();
groups = {
downloads: new Map<string, Set<string>>(),
uploads: new Map<string, Set<string>>()
};
constructor(
private readonly fs: IFileStorage,
private readonly tokenManager: TokenManager
@@ -50,12 +56,26 @@ export class FileStorage {
groupId: string,
eventData?: Record<string, unknown>
) {
let current = 0;
const token = await this.tokenManager.getAccessToken();
const total = files.length;
let current = 0;
this.downloads.set(groupId, files);
const group = this.groups.downloads.get(groupId) || new Set();
files.forEach((f) => group.add(f.filename));
this.groups.downloads.set(groupId, group);
for (const file of files as QueueItem[]) {
if (!group.has(file.filename)) continue;
const download = this.downloads.get(file.filename);
if (download && download.operation) {
logger.debug("[queueDownloads] duplicate download", {
filename: file.filename,
groupId
});
await download.operation;
continue;
}
const { filename, chunkSize } = file;
if (await this.exists(filename)) {
current++;
@@ -68,14 +88,6 @@ export class FileStorage {
continue;
}
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const { execute, cancel } = this.fs.downloadFile(filename, {
url,
chunkSize,
headers: { Authorization: `Bearer ${token}` }
});
file.cancel = cancel;
EV.publish(EVENTS.fileDownload, {
total,
current,
@@ -83,7 +95,22 @@ export class FileStorage {
filename
});
const result = await execute().catch(() => false);
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const { execute, cancel } = this.fs.downloadFile(filename, {
url,
chunkSize,
headers: { Authorization: `Bearer ${token}` }
});
file.cancel = cancel;
file.operation = execute()
.catch(() => false)
.finally(() => {
this.downloads.delete(filename);
group.delete(filename);
});
this.downloads.set(filename, file);
const result = await file.operation;
if (eventData)
EV.publish(EVENTS.fileDownloaded, {
success: result,
@@ -94,17 +121,31 @@ export class FileStorage {
eventData
});
}
this.downloads.delete(groupId);
}
async queueUploads(files: DownloadableFile[], groupId: string) {
let current = 0;
const token = await this.tokenManager.getAccessToken();
const total = files.length;
let current = 0;
this.uploads.set(groupId, files);
const group = this.groups.uploads.get(groupId) || new Set();
files.forEach((f) => group.add(f.filename));
this.groups.uploads.set(groupId, group);
for (const file of files as QueueItem[]) {
if (!group.has(file.filename)) continue;
const upload = this.uploads.get(file.filename);
if (upload && upload.operation) {
logger.debug("[queueUploads] duplicate upload", {
filename: file.filename,
groupId
});
await file.operation;
continue;
}
const { filename, chunkSize } = file;
let error = null;
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const { execute, cancel } = this.fs.uploadFile(filename, {
chunkSize,
@@ -112,6 +153,16 @@ export class FileStorage {
headers: { Authorization: `Bearer ${token}` }
});
file.cancel = cancel;
file.operation = execute()
.catch((e) => {
logger.error(e, "failed to upload attachment", { hash: filename });
error = e;
return false;
})
.finally(() => {
this.uploads.delete(filename);
group.delete(filename);
});
EV.publish(EVENTS.fileUpload, {
total,
@@ -120,13 +171,8 @@ export class FileStorage {
filename
});
let error = null;
const result = await execute().catch((e) => {
logger.error(e, "failed to upload attachment", { hash: filename });
error = e;
return false;
});
this.uploads.set(filename, file);
const result = await file.operation;
EV.publish(EVENTS.fileUploaded, {
error,
success: result,
@@ -136,44 +182,67 @@ export class FileStorage {
filename
});
}
this.uploads.delete(groupId);
}
async downloadFile(groupId: string, filename: string, chunkSize: number) {
if (await this.exists(filename)) return true;
const download = this.downloads.get(filename);
if (download && download.operation) {
logger.debug("[downloadFile] duplicate download", { filename, groupId });
return await download.operation;
}
logger.debug("[downloadFile] downloading", { filename, groupId });
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const file: QueueItem = { filename, chunkSize };
const token = await this.tokenManager.getAccessToken();
const group = this.groups.downloads.get(groupId) || new Set();
const { execute, cancel } = this.fs.downloadFile(filename, {
url,
chunkSize,
headers: { Authorization: `Bearer ${token}` }
});
this.downloads.set(groupId, [{ cancel, filename, chunkSize }]);
const result = await execute();
this.downloads.delete(groupId);
return result;
file.cancel = cancel;
file.operation = execute().finally(() => {
this.downloads.delete(filename);
group.delete(filename);
});
this.downloads.set(filename, file);
this.groups.downloads.set(groupId, group.add(filename));
return await file.operation;
}
async cancel(groupId: string) {
const queues = [
{ type: "download", files: this.downloads.get(groupId) },
{ type: "upload", files: this.uploads.get(groupId) }
].filter((a) => !!a.files);
{
type: "download",
ids: this.groups.downloads.get(groupId),
files: this.downloads
},
{
type: "upload",
ids: this.groups.uploads.get(groupId),
files: this.uploads
}
].filter((a) => !!a.ids);
for (const queue of queues) {
if (!queue.files) continue;
for (let i = 0; i < queue.files.length; ++i) {
const file = queue.files[i];
if (file.cancel) await file.cancel("Operation canceled.");
queue.files.splice(i, 1);
if (!queue.ids) continue;
for (const filename of queue.ids) {
const file = queue.files.get(filename);
if (file?.cancel) await file.cancel("Operation canceled.");
queue.ids.delete(filename);
}
if (queue.type === "download") {
this.downloads.delete(groupId);
this.groups.downloads.delete(groupId);
EV.publish(EVENTS.downloadCanceled, { groupId, canceled: true });
} else if (queue.type === "upload") {
this.uploads.delete(groupId);
this.groups.uploads.delete(groupId);
EV.publish(EVENTS.uploadCanceled, { groupId, canceled: true });
}
}

View File

@@ -0,0 +1,74 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Kysely, sql } from "kysely";
import { RawDatabaseSchema } from ".";
export async function rebuildSearchIndex(db: Kysely<RawDatabaseSchema>) {
await db.transaction().execute(async (tx) => {
for (const query of [
sql`INSERT INTO content_fts(content_fts) VALUES('delete-all')`,
sql`INSERT INTO notes_fts(notes_fts) VALUES('delete-all')`
]) {
await query.execute(tx);
}
await tx
.insertInto("content_fts")
.columns(["rowid", "id", "data", "noteId"])
.expression((eb) =>
eb
.selectFrom("content")
.where((eb) =>
eb.and([
eb("noteId", "is not", null),
eb("data", "is not", null),
eb("deleted", "is not", true)
])
)
.select([
"rowid",
"id",
sql`IIF(locked == 1, '', data)`.as("data"),
"noteId"
])
)
.execute();
await tx
.insertInto("notes_fts")
.columns(["rowid", "id", "title"])
.expression((eb) =>
eb
.selectFrom("notes")
.where((eb) =>
eb.and([eb("title", "is not", null), eb("deleted", "is not", true)])
)
.select(["rowid", "id", "title"])
)
.execute();
for (const query of [
sql`INSERT INTO content_fts(content_fts) VALUES('optimize')`,
sql`INSERT INTO notes_fts(notes_fts) VALUES('optimize')`
]) {
await query.execute(tx);
}
});
}

View File

@@ -167,6 +167,7 @@ export interface DatabaseCollection<T, IsAsync extends boolean> {
delete(ids: string[]): Promise<void>;
exists(id: string): AsyncOrSyncResult<IsAsync, boolean>;
count(): AsyncOrSyncResult<IsAsync, number>;
unsyncedCount(): Promise<number>;
get(id: string): AsyncOrSyncResult<IsAsync, T | undefined>;
put(items: (T | undefined)[]): Promise<SQLiteItem<T>[]>;
update(ids: string[], partial: Partial<T>): Promise<void>;

View File

@@ -24,6 +24,7 @@ import {
MigrationProvider,
sql
} from "kysely";
import { rebuildSearchIndex } from "./fts";
const COLLATE_NOCASE: ColumnBuilderCallback = (col) =>
col.modifyEnd(sql`collate nocase`);
@@ -283,6 +284,11 @@ export class NNMigrationProvider implements MigrationProvider {
.execute();
},
async down(db) {}
},
"2": {
async up(db) {
await rebuildSearchIndex(db);
}
}
};
}

View File

@@ -41,6 +41,7 @@ import {
} from "../types";
import { IndexedCollection } from "./indexed-collection";
import { SQLCollection } from "./sql-collection";
import { logger } from "../logger";
export type RawItem = MaybeDeletedItem<Item>;
type MigratableCollection = {
@@ -136,10 +137,16 @@ class Migrator {
for (let i = 0; i < items.length; ++i) {
const item = items[i];
// can be true due to corrupted data.
if (Array.isArray(item)) continue;
if (Array.isArray(item)) {
logger.debug("Skipping item during migration to SQLite", {
table,
version,
item
});
continue;
}
if (!item) continue;
const itemId = item.id;
let migrated = await migrateItem(
item,
version,
@@ -161,14 +168,7 @@ class Migrator {
);
}
if (migrated === true) {
toAdd.push(item);
// if id changed after migration, we need to delete the old one.
if (item.id !== itemId) {
// await collection.deleteItem(itemId);
}
}
if (migrated !== "skip") toAdd.push(item);
}
if (toAdd.length > 0) {
@@ -217,7 +217,7 @@ class Migrator {
);
}
if (!migrated) continue;
if (!migrated || migrated === "skip") continue;
toAdd.push(item);

View File

@@ -21,7 +21,7 @@ import { MaybeDeletedItem, isDeleted } from "../types";
import EventManager from "../utils/event-manager";
import { DatabaseAccessor, DatabaseCollection, DatabaseSchema } from ".";
import { SQLCollection } from "./sql-collection";
import { Kysely, Transaction } from "kysely";
import { Kysely } from "kysely";
import { Sanitizer } from "./sanitizer";
export class SQLCachedCollection<
@@ -171,6 +171,10 @@ export class SQLCachedCollection<
}
}
async unsyncedCount() {
return this.collection.unsyncedCount();
}
// has(id: string) {
// return this.cache.has(id);
// }

View File

@@ -226,7 +226,14 @@ export class SQLCollection<
async update(
ids: string[],
partial: Partial<SQLiteItem<T>>,
options: { sendEvent: boolean } = { sendEvent: true }
options: {
sendEvent: boolean;
condition?: ExpressionOrFactory<
DatabaseSchema,
keyof DatabaseSchema,
SqlBool
>;
} = { sendEvent: true }
) {
if (!this.sanitizer.sanitize(this.type, partial)) return;
@@ -237,6 +244,7 @@ export class SQLCollection<
await tx
.updateTable<keyof DatabaseSchema>(this.type)
.where("id", "in", chunk)
.$if(!!options.condition, (eb) => eb.where(options.condition!))
.set({
...partial,
dateModified: Date.now(),
@@ -270,6 +278,21 @@ export class SQLCollection<
return items;
}
async unsyncedCount() {
const { count } =
(await this.db()
.selectFrom<keyof DatabaseSchema>(this.type)
.select((a) => a.fn.count<number>("id").as("count"))
.where(isFalse("synced"))
.$if(this.type === "attachments", (eb) =>
eb.where((eb) =>
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])
)
)
.executeTakeFirst()) || {};
return count || 0;
}
async *unsynced(
chunkSize: number,
forceSync?: boolean
@@ -529,9 +552,11 @@ export class FilteredSelector<T extends Item> {
async *[Symbol.asyncIterator]() {
let lastRow: any | null = null;
const fields = this._fields.slice();
if (!fields.find((f) => f.includes(".dateCreated")))
fields.push("dateCreated");
if (!fields.find((f) => f.includes(".id"))) fields.push("id");
if (fields.length > 0) {
if (!fields.find((f) => f.includes(".dateCreated")))
fields.push("dateCreated");
if (!fields.find((f) => f.includes(".id"))) fields.push("id");
}
while (true) {
const rows = await this.filter

View File

@@ -31,16 +31,16 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.addEvent("insert")
.when((eb) =>
eb.and([
eb("new.deleted", "is not", true),
eb("new.locked", "is not", true),
eb("new.data", "is not", null)
eb("new.noteId", "is not", null),
eb("new.data", "is not", null),
eb("new.deleted", "is not", true)
])
)
.addQuery((c) =>
c.insertInto("content_fts").values({
rowid: sql`new.rowid`,
id: sql`new.id`,
data: sql`new.data`,
data: sql`IIF(new.locked == 1, '', new.data)`,
noteId: sql`new.noteId`
})
)
@@ -53,6 +53,13 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.onTable("content", "main")
.after()
.addEvent("delete")
.when((eb) =>
eb.and([
eb("old.noteId", "is not", null),
eb("old.data", "is not", null),
eb("old.deleted", "is not", true)
])
)
.addQuery((c) =>
c.insertInto("content_fts").values({
content_fts: sql.lit("delete"),
@@ -73,9 +80,9 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.addEvent("update")
.when((eb) =>
eb.and([
eb("old.deleted", "is not", true),
eb("old.noteId", "is not", null),
eb("old.data", "is not", null)
eb("old.data", "is not", null),
eb("old.deleted", "is not", true)
])
)
.addQuery((c) =>
@@ -107,7 +114,8 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.addEvent("insert")
.when((eb) =>
eb.and([
eb.or([eb("new.deleted", "is", null), eb("new.deleted", "==", false)])
eb("new.title", "is not", null),
eb("new.deleted", "is not", true)
])
)
.addQuery((c) =>
@@ -126,6 +134,12 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.onTable("notes", "main")
.after()
.addEvent("delete")
.when((eb) =>
eb.and([
eb("old.title", "is not", null),
eb("old.deleted", "is not", true)
])
)
.addQuery((c) =>
c.insertInto("notes_fts").values({
notes_fts: sql.lit("delete"),

View File

@@ -1,7 +1,7 @@
module.exports = {
all: {
DISABLE_ESLINT_PLUGIN: true,
GENERATE_SOURCEMAP: process.env.NODE_ENV === "development",
GENERATE_SOURCEMAP: true,
BROWSER: "none"
}
};

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