mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 10:39:07 +02:00
Compare commits
59 Commits
fix-editor
...
feat/rende
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb8289805 | ||
|
|
58845e5508 | ||
|
|
543c6f17ed | ||
|
|
533f95fcff | ||
|
|
c02f1cde16 | ||
|
|
043f55ead0 | ||
|
|
fcece06a1c | ||
|
|
da7d79edc6 | ||
|
|
a2e7190a4a | ||
|
|
503f2b5876 | ||
|
|
68d57e2b27 | ||
|
|
400aa574cd | ||
|
|
5028bb1275 | ||
|
|
27ae5bb5a2 | ||
|
|
83316fe299 | ||
|
|
8869224de2 | ||
|
|
c4152f0fc0 | ||
|
|
d803005637 | ||
|
|
53d896be2b | ||
|
|
8c37c6409a | ||
|
|
9195244890 | ||
|
|
152794ccd0 | ||
|
|
2e8b234b63 | ||
|
|
055c647698 | ||
|
|
2b7bcfe7c6 | ||
|
|
3c8b8a7bc9 | ||
|
|
fde45fb6de | ||
|
|
4a616a3e67 | ||
|
|
bd0a338dd6 | ||
|
|
5ab721e3e2 | ||
|
|
cb70bb9429 | ||
|
|
82cadd473c | ||
|
|
0e8ea3ddeb | ||
|
|
511acc5fca | ||
|
|
001cf8ee92 | ||
|
|
a965cd694b | ||
|
|
d3c1c1959b | ||
|
|
9a866a552a | ||
|
|
837c296821 | ||
|
|
9aed6faed8 | ||
|
|
734f491e8d | ||
|
|
0ea13024e9 | ||
|
|
cb27039245 | ||
|
|
714ac2deac | ||
|
|
d605bc29bf | ||
|
|
080101bc45 | ||
|
|
4819e17da3 | ||
|
|
69a5d5a1b6 | ||
|
|
68a6d6c313 | ||
|
|
c51ef4d5f6 | ||
|
|
d9c5c1f893 | ||
|
|
5a75ca73a9 | ||
|
|
626d393376 | ||
|
|
68d06ff3f8 | ||
|
|
79f20d100c | ||
|
|
9d5bec3bfb | ||
|
|
ac96354b4c | ||
|
|
63f44d6fbc | ||
|
|
f9bfa88c83 |
10
.github/workflows/android.publish.internal.yml
vendored
10
.github/workflows/android.publish.internal.yml
vendored
@@ -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
|
||||
|
||||
11
.github/workflows/android.publish.yml
vendored
11
.github/workflows/android.publish.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/ios.publish.yml
vendored
9
.github/workflows/ios.publish.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/web.tests.yml
vendored
6
.github/workflows/web.tests.yml
vendored
@@ -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 }}
|
||||
|
||||
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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?.();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
|
||||
@@ -174,3 +174,4 @@ export const eUnlockWithPassword = "619";
|
||||
export const eUpdateNoteInEditor = "620";
|
||||
export const eOnEnterEditor = "621";
|
||||
export const eOnExitEditor = "622";
|
||||
export const eEditorReset = "623";
|
||||
|
||||
91
apps/mobile/app/utils/unlock-vault.ts
Normal file
91
apps/mobile/app/utils/unlock-vault.ts
Normal 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;
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
110
apps/mobile/package-lock.json
generated
110
apps/mobile/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.2",
|
||||
"version": "3.0.4",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
2
apps/theme-builder/package-lock.json
generated
2
apps/theme-builder/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
4
apps/web/package-lock.json
generated
4
apps/web/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ function ProgressDialog(props) {
|
||||
try {
|
||||
props.onDone(await props.action(setProgress));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
props.onDone(e);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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" });
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export default defineConfig({
|
||||
minify: "esbuild",
|
||||
cssMinify: true,
|
||||
emptyOutDir: true,
|
||||
sourcemap: !isDesktop,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
plugins: [emitEditorStyles()],
|
||||
|
||||
4
fastlane/metadata/android/en-US/changelogs/15059.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/15059.txt
Normal 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!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15064.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15064.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -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"
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -51,7 +51,7 @@ async function writeEncryptedBase64(
|
||||
hashType,
|
||||
iv: "some iv",
|
||||
salt: key.salt!,
|
||||
length: data.length
|
||||
size: data.length
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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", () =>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -255,6 +255,7 @@ export default class Vault {
|
||||
noteId,
|
||||
sessionId,
|
||||
data: encryptedContent,
|
||||
dateEdited: Date.now(),
|
||||
type: content.type
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
74
packages/core/src/database/fts.ts
Normal file
74
packages/core/src/database/fts.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user