mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 11:08:47 +02:00
Compare commits
2 Commits
v3.0.3
...
fix-camera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d3f693b0 | ||
|
|
978e63ea78 |
@@ -148,7 +148,7 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
|
||||
2
.github/workflows/android.publish.yml
vendored
2
.github/workflows/android.publish.yml
vendored
@@ -148,7 +148,7 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
|
||||
2
.github/workflows/ios.publish.yml
vendored
2
.github/workflows/ios.publish.yml
vendored
@@ -98,7 +98,7 @@ jobs:
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload Notesnook.ipa to Github
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Notesnook.zip
|
||||
path: |
|
||||
|
||||
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@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
working-directory: apps/web
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
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.3",
|
||||
"version": "3.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"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(
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
"npx 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(`yarn run bundle:mas`);
|
||||
await exec(`npm run bundle:mas`);
|
||||
} else {
|
||||
await exec(`yarn run bundle`);
|
||||
await exec(`npm run bundle`);
|
||||
}
|
||||
|
||||
await exec(`yarn tsc`);
|
||||
await exec(`npx tsc`);
|
||||
|
||||
if (args.run) {
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
await exec(`npx 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("yarn electron-builder install-app-deps");
|
||||
await exec("npx electron-builder install-app-deps");
|
||||
}
|
||||
|
||||
await exec(`yarn run bundle`);
|
||||
execAsync(`yarn`, [`tsc`]);
|
||||
await exec(`npm run bundle`);
|
||||
execAsync(`npx`, [`tsc`]);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
@@ -66,7 +66,7 @@ async function onChange(first) {
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
["npx", "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(
|
||||
"yarn",
|
||||
"npx",
|
||||
["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().optional())
|
||||
.input(z.boolean())
|
||||
.mutation(({ input: customDns }) => {
|
||||
if (customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
config.customDns = !!customDns;
|
||||
config.customDns = customDns;
|
||||
}),
|
||||
|
||||
proxyRules: t.procedure.query(() => config.proxyRules),
|
||||
|
||||
@@ -20,13 +20,15 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eCloseAnnouncementDialog } from "../../utils/events";
|
||||
import Sync from "../../services/sync";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseAnnouncementDialog, eCloseSheet } 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";
|
||||
|
||||
@@ -54,6 +56,13 @@ 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, "full");
|
||||
Sync.run("global", false, true);
|
||||
}
|
||||
}, 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, "full", async (complete) => {
|
||||
Sync.run("global", false, true, async (complete) => {
|
||||
if (!complete) {
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
|
||||
@@ -17,13 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Image,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { Image, 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";
|
||||
@@ -71,10 +65,7 @@ export default function AttachImage({
|
||||
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
|
||||
<Image
|
||||
source={{
|
||||
uri:
|
||||
Platform.OS === "ios"
|
||||
? item.sourceURL || item.path
|
||||
: item.path
|
||||
uri: item.sourceURL || item.path
|
||||
}}
|
||||
style={{
|
||||
width: 100,
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function List(props: ListProps) {
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
Sync.run("global", false, "full", () => {
|
||||
Sync.run("global", false, true, () => {
|
||||
props.onRefresh?.();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,13 +19,7 @@ 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,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from "react-native";
|
||||
import { ActivityIndicator, 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";
|
||||
@@ -35,12 +29,14 @@ 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";
|
||||
@@ -69,7 +65,6 @@ const ExportNotesSheet = ({
|
||||
filePath: string;
|
||||
name: string;
|
||||
type: string;
|
||||
fileDir: string;
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
@@ -300,9 +295,7 @@ const ExportNotesSheet = ({
|
||||
successfully as {result?.fileName}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={
|
||||
Platform.OS === "android" ? "Open file location" : "Open"
|
||||
}
|
||||
title="Open"
|
||||
type="accent"
|
||||
width={250}
|
||||
fontSize={SIZE.md}
|
||||
@@ -312,23 +305,20 @@ const ExportNotesSheet = ({
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result?.filePath) return;
|
||||
if (Platform.OS === "android") {
|
||||
Linking.openURL(result.fileDir).catch((e) => {
|
||||
ToastManager.error(e as Error);
|
||||
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"
|
||||
});
|
||||
} 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
|
||||
@@ -341,10 +331,10 @@ const ExportNotesSheet = ({
|
||||
borderRadius: 100
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result) return;
|
||||
if (!result?.filePath) return;
|
||||
if (Platform.OS === "ios") {
|
||||
Share.open({
|
||||
url: result?.fileDir + result.fileName
|
||||
url: result.filePath
|
||||
}).catch(console.log);
|
||||
} else {
|
||||
FileViewer.open(result.filePath, {
|
||||
@@ -357,7 +347,7 @@ const ExportNotesSheet = ({
|
||||
/>
|
||||
<Button
|
||||
title="Export in another format"
|
||||
type="inverted"
|
||||
type="secondaryAccented"
|
||||
width={250}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
|
||||
@@ -35,10 +35,7 @@ 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 {
|
||||
EditorEvents,
|
||||
editorController
|
||||
} from "../../../screens/editor/tiptap/utils";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
|
||||
const ListNoteItem = ({
|
||||
id,
|
||||
@@ -193,13 +190,13 @@ export default function LinkNote(props: {
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
editorController.current?.postMessage(EditorEvents.resolve, {
|
||||
data: {
|
||||
editorController.current.commands.createInternalLink(
|
||||
{
|
||||
href: link,
|
||||
title: selectedNote.title
|
||||
},
|
||||
resolverId: props.resolverId
|
||||
});
|
||||
props.resolverId
|
||||
);
|
||||
};
|
||||
|
||||
const onSelectNote = async (note: Note) => {
|
||||
|
||||
@@ -86,7 +86,6 @@ import { updateStatusBarColor } from "../utils/colors";
|
||||
import { BETA } from "../utils/constants";
|
||||
import {
|
||||
eCloseSheet,
|
||||
eEditorReset,
|
||||
eLoginSessionExpired,
|
||||
eOnLoadNote,
|
||||
eOpenAnnouncementDialog,
|
||||
@@ -201,13 +200,11 @@ const onRequestPartialSync = async (
|
||||
`onRequestPartialSync full:${full}, force:${force}, lastSyncTime:${lastSyncTime}`
|
||||
);
|
||||
|
||||
await Sync.run(
|
||||
"global",
|
||||
force,
|
||||
full ? "full" : "send",
|
||||
undefined,
|
||||
lastSyncTime
|
||||
);
|
||||
if (full || force) {
|
||||
await Sync.run("global", force, full, undefined, lastSyncTime);
|
||||
} else {
|
||||
await Sync.run("global", false, false, undefined, lastSyncTime);
|
||||
}
|
||||
};
|
||||
|
||||
const onLogout = async (reason: string) => {
|
||||
@@ -236,7 +233,7 @@ async function checkForShareExtensionLaunchedInBackground() {
|
||||
if (notesAddedFromIntent || shareExtensionOpened) {
|
||||
const id = useTabStore.getState().getCurrentNoteId();
|
||||
const note = id && (await db.notes.note(id));
|
||||
eSendEvent(eEditorReset);
|
||||
eSendEvent("webview_reset");
|
||||
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
|
||||
MMKV.removeItem("shareExtensionOpened");
|
||||
}
|
||||
@@ -533,14 +530,6 @@ 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,7 +39,6 @@ import {
|
||||
eSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import {
|
||||
eEditorReset,
|
||||
eOnLoadNote,
|
||||
eUnlockNote,
|
||||
eUnlockWithBiometrics,
|
||||
@@ -55,10 +54,8 @@ import { syncTabs, useTabStore } from "./tiptap/use-tab-store";
|
||||
import {
|
||||
editorController,
|
||||
editorState,
|
||||
openInternalLink,
|
||||
randId
|
||||
openInternalLink
|
||||
} from "./tiptap/utils";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
@@ -105,20 +102,24 @@ const Editor = React.memo(
|
||||
noToolbar,
|
||||
noHeader
|
||||
});
|
||||
const renderKey = useRef(randId("editor-id") + editorId);
|
||||
const renderKey = useRef(`editor-0` + editorId);
|
||||
useImperativeHandle(ref, () => ({
|
||||
get: () => editor
|
||||
}));
|
||||
useLockedNoteHandler();
|
||||
|
||||
const onError = useCallback(() => {
|
||||
renderKey.current = randId("editor-id") + editorId;
|
||||
renderKey.current =
|
||||
renderKey.current === `editor-0`
|
||||
? `editor-1` + editorId
|
||||
: `editor-0` + editorId;
|
||||
|
||||
editor.state.current.ready = false;
|
||||
editor.setLoading(true);
|
||||
}, [editor, editorId]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = [eSubscribeEvent(eEditorReset, onError)];
|
||||
const sub = [eSubscribeEvent("webview_reset", onError)];
|
||||
return () => {
|
||||
sub.forEach((s) => s?.unsubscribe());
|
||||
};
|
||||
@@ -145,7 +146,6 @@ 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 && tabBarRef.current?.page() === 2) {
|
||||
if (tabRef.current?.locked) {
|
||||
unlock();
|
||||
}
|
||||
return () => {
|
||||
|
||||
@@ -48,6 +48,5 @@ export const EventTypes = {
|
||||
unlockWithBiometrics: "editor-events:unlock-biometrics",
|
||||
disableReadonlyMode: "editor-events:disable-readonly-mode",
|
||||
readonlyEditorLoaded: "readonlyEditorLoaded",
|
||||
error: "editorError",
|
||||
dbLogger: "editor-events:dbLogger"
|
||||
error: "editorError"
|
||||
};
|
||||
|
||||
@@ -41,7 +41,6 @@ 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({
|
||||
@@ -109,6 +108,7 @@ 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,14 +164,13 @@ 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"
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
console.log("attachment error: ", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -266,7 +265,6 @@ 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;
|
||||
@@ -302,12 +300,7 @@ const handleImageResponse = async (
|
||||
type: "url"
|
||||
});
|
||||
|
||||
const fileName = image.sourceURL
|
||||
? basename(image.sourceURL)
|
||||
: image.filename || "image";
|
||||
|
||||
console.log("attaching image...", fileName);
|
||||
|
||||
const fileName = image.filename || "image";
|
||||
console.log("attaching file...");
|
||||
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export type EditorState = {
|
||||
isAwaitingResult: boolean;
|
||||
scrollPosition: number;
|
||||
overlay?: boolean;
|
||||
initialLoadCalled?: boolean;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
@@ -75,8 +74,6 @@ export type EditorMessage<T> = {
|
||||
type: string;
|
||||
noteId: string;
|
||||
tabId: number;
|
||||
resolverId?: string;
|
||||
hasTimeout?: boolean;
|
||||
};
|
||||
|
||||
export type SavePayload = {
|
||||
@@ -87,7 +84,6 @@ export type SavePayload = {
|
||||
sessionHistoryId?: number;
|
||||
ignoreEdit: boolean;
|
||||
tabId: number;
|
||||
pendingChanges?: boolean;
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
|
||||
@@ -77,6 +77,7 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { EditorEvents, editorState, openInternalLink } from "./utils";
|
||||
|
||||
|
||||
const publishNote = async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user) {
|
||||
@@ -353,15 +354,8 @@ 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) {
|
||||
DatabaseLogger.log("Editor is ready");
|
||||
console.log("Editor loaded");
|
||||
editor.onLoad();
|
||||
return;
|
||||
}
|
||||
@@ -389,36 +383,21 @@ export const useEditorEvents = (
|
||||
content: editorMessage.value.html as string,
|
||||
noteId: noteId,
|
||||
tabId: editorMessage.tabId,
|
||||
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
|
||||
pendingChanges: editorMessage.value?.pendingChanges
|
||||
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit
|
||||
});
|
||||
break;
|
||||
case EventTypes.title:
|
||||
DatabaseLogger.log("EventTypes.title");
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
title: editorMessage.value?.title as string,
|
||||
title: editorMessage.value as string,
|
||||
noteId: noteId,
|
||||
tabId: editorMessage.tabId,
|
||||
ignoreEdit: false,
|
||||
pendingChanges: editorMessage.value?.pendingChanges
|
||||
ignoreEdit: false
|
||||
});
|
||||
break;
|
||||
case EventTypes.logger:
|
||||
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);
|
||||
}
|
||||
logger.info("[WEBVIEW LOG]", editorMessage.value);
|
||||
break;
|
||||
case EventTypes.contentchange:
|
||||
editor.onContentChanged(editorMessage.noteId);
|
||||
@@ -506,17 +485,17 @@ export const useEditorEvents = (
|
||||
console.log(
|
||||
"Got attachment data:",
|
||||
!!data,
|
||||
editorMessage.resolverId
|
||||
(editorMessage.value as any).resolverId
|
||||
);
|
||||
editor.postMessage(EditorEvents.resolve, {
|
||||
resolverId: editorMessage.resolverId,
|
||||
editor.postMessage(EditorEvents.attachmentData, {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
DatabaseLogger.error(e);
|
||||
editor.postMessage(EditorEvents.resolve, {
|
||||
resolverId: editorMessage.resolverId,
|
||||
editor.postMessage(EditorEvents.attachmentData, {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data: undefined
|
||||
});
|
||||
});
|
||||
@@ -643,7 +622,7 @@ export const useEditorEvents = (
|
||||
case EventTypes.createInternalLink: {
|
||||
LinkNote.present(
|
||||
editorMessage.value.attributes,
|
||||
editorMessage.resolverId as string
|
||||
editorMessage.value.resolverId
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ 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
|
||||
@@ -47,20 +46,16 @@ 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";
|
||||
@@ -73,7 +68,6 @@ import {
|
||||
isEditorLoaded,
|
||||
post
|
||||
} from "./utils";
|
||||
import { sleep } from "../../../utils/time";
|
||||
|
||||
type NoteWithContent = Note & {
|
||||
content?: NoteContent<false>;
|
||||
@@ -151,9 +145,8 @@ 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();
|
||||
@@ -172,11 +165,10 @@ export const useEditor = (
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
overlay(true);
|
||||
state.current.ready = false;
|
||||
setLoading(false);
|
||||
}
|
||||
}, [loading, overlay]);
|
||||
}, [loading]);
|
||||
|
||||
const withTimer = useCallback(
|
||||
(id: string, fn: () => void, duration: number) => {
|
||||
@@ -224,8 +216,7 @@ export const useEditor = (
|
||||
type,
|
||||
ignoreEdit,
|
||||
sessionHistoryId: currentSessionHistoryId,
|
||||
tabId,
|
||||
pendingChanges
|
||||
tabId
|
||||
}: SavePayload) => {
|
||||
if (currentNotes.current[id as string]?.readonly || readonly) return;
|
||||
try {
|
||||
@@ -286,26 +277,12 @@ 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) {
|
||||
@@ -339,22 +316,9 @@ 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) {
|
||||
@@ -377,16 +341,6 @@ 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) {
|
||||
@@ -461,11 +415,7 @@ export const useEditor = (
|
||||
commands.focus(tabId);
|
||||
});
|
||||
} else {
|
||||
if (!event.item) {
|
||||
overlay(false);
|
||||
return;
|
||||
}
|
||||
console.log("LOADING NOTE", event.item.id);
|
||||
if (!event.item) return;
|
||||
const item = event.item;
|
||||
|
||||
const currentTab = useTabStore
|
||||
@@ -493,6 +443,7 @@ 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;
|
||||
}
|
||||
@@ -551,7 +502,6 @@ export const useEditor = (
|
||||
loadingState.current === currentContents.current[item.id]?.data
|
||||
) {
|
||||
// If note is already loading, return.
|
||||
console.log("Note is already loading...");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -585,8 +535,11 @@ 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;
|
||||
}
|
||||
@@ -620,131 +573,124 @@ export const useEditor = (
|
||||
data: Note | ContentItem | TrashItem | DeletedItem,
|
||||
isLocal?: boolean
|
||||
) => {
|
||||
try {
|
||||
await (async () => {
|
||||
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
|
||||
if (!data) return;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
lastContentChangeTime.current[note.id] = note.dateEdited;
|
||||
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
|
||||
if (!isEncryptedContent(data)) {
|
||||
currentContents.current[note.id] = data as UnencryptedContentItem;
|
||||
}
|
||||
|
||||
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]
|
||||
);
|
||||
@@ -767,8 +713,7 @@ export const useEditor = (
|
||||
type,
|
||||
ignoreEdit,
|
||||
noteId,
|
||||
tabId,
|
||||
pendingChanges
|
||||
tabId
|
||||
}: {
|
||||
noteId?: string;
|
||||
title?: string;
|
||||
@@ -776,29 +721,18 @@ export const useEditor = (
|
||||
type: string;
|
||||
ignoreEdit: boolean;
|
||||
tabId: number;
|
||||
pendingChanges?: boolean;
|
||||
}) => {
|
||||
DatabaseLogger.log(
|
||||
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
|
||||
);
|
||||
DatabaseLogger.log(`Saving content...`);
|
||||
if (
|
||||
lock.current ||
|
||||
(currentLoadingNoteId.current &&
|
||||
currentLoadingNoteId.current === noteId)
|
||||
) {
|
||||
DatabaseLogger.log(`Skipped saving content:
|
||||
DatabaseLogger.log(`Skipped saving conent:
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -806,7 +740,7 @@ export const useEditor = (
|
||||
lastContentChangeTime.current[noteId] = Date.now();
|
||||
}
|
||||
|
||||
if (type === EventTypes.content && noteId) {
|
||||
if (type === EditorEvents.content && noteId) {
|
||||
currentContents.current[noteId as string] = {
|
||||
data: content,
|
||||
type: "tiptap",
|
||||
@@ -821,8 +755,7 @@ export const useEditor = (
|
||||
id: noteId,
|
||||
ignoreEdit,
|
||||
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
|
||||
tabId: tabId,
|
||||
pendingChanges
|
||||
tabId: tabId
|
||||
};
|
||||
withTimer(
|
||||
noteId || "newnote",
|
||||
@@ -834,16 +767,7 @@ export const useEditor = (
|
||||
onChange(params.data);
|
||||
return;
|
||||
}
|
||||
if (useSettingStore.getState().isAppLoading) {
|
||||
const sub = useSettingStore.subscribe((state) => {
|
||||
if (!state.isAppLoading) {
|
||||
saveNote(params);
|
||||
sub();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
saveNote(params);
|
||||
}
|
||||
saveNote(params);
|
||||
},
|
||||
ignoreEdit ? 0 : 150
|
||||
);
|
||||
@@ -886,7 +810,7 @@ export const useEditor = (
|
||||
useTabStore.getState().currentTab
|
||||
))
|
||||
) {
|
||||
eSendEvent(eEditorReset, "onReady");
|
||||
eSendEvent("webview_reset", "onReady");
|
||||
return false;
|
||||
} else {
|
||||
syncTabs();
|
||||
@@ -896,6 +820,7 @@ export const useEditor = (
|
||||
}, [isDefaultEditor, restoreEditorState]);
|
||||
|
||||
const onLoad = useCallback(async () => {
|
||||
if (currentNotes.current) overlay(true);
|
||||
setTimeout(() => {
|
||||
postMessage(EditorEvents.theme, theme);
|
||||
});
|
||||
@@ -910,22 +835,12 @@ 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 = {
|
||||
export const EditorEvents: { [name: string]: string } = {
|
||||
html: "native:html",
|
||||
updatehtml: "native:updatehtml",
|
||||
title: "native:title",
|
||||
@@ -55,8 +55,7 @@ export const EditorEvents = {
|
||||
logger: "native:logger",
|
||||
status: "native:status",
|
||||
keyboardShown: "native:keyboardShown",
|
||||
attachmentData: "native:attachment-data",
|
||||
resolve: "native:resolve"
|
||||
attachmentData: "native:attachment-data"
|
||||
};
|
||||
|
||||
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 run sync.",
|
||||
"Periodically wake up the app in background to sync your notes from other devices.",
|
||||
type: "switch",
|
||||
property: "backgroundSync",
|
||||
onChange: (value) => {
|
||||
@@ -470,43 +470,22 @@ export const settingsGroups: SettingSection[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
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: () => {
|
||||
id: "sync-issues-fix",
|
||||
name: "Having problems with sync",
|
||||
description: "Try force sync to resolve issues with syncing",
|
||||
icon: "sync-alert",
|
||||
modifer: async () => {
|
||||
presentDialog({
|
||||
title: "Force Pull changes",
|
||||
title: "Force sync",
|
||||
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.",
|
||||
"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.",
|
||||
negativeText: "Cancel",
|
||||
positiveText: "Start",
|
||||
positivePress: async () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Progress.present();
|
||||
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", () => {
|
||||
Sync.run("global", true, true, () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ export const ToastManager = {
|
||||
});
|
||||
},
|
||||
hide: () => eSendEvent(eHideToast),
|
||||
error: (e: Error, title?: string, context?: any, duration = 5000) => {
|
||||
error: (e: Error, title?: string, context?: any) => {
|
||||
ToastManager.show({
|
||||
heading: title,
|
||||
message: e?.message || "",
|
||||
@@ -176,7 +176,7 @@ export const ToastManager = {
|
||||
heading: "Logs copied!",
|
||||
type: "success",
|
||||
context: "global",
|
||||
duration: duration
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,26 +17,40 @@ 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 } from "../common/database/index";
|
||||
import { DatabaseLogger, db } from "../common/database/index";
|
||||
import Storage from "../common/database/storage";
|
||||
|
||||
import {
|
||||
exportNote as _exportNote,
|
||||
ExportableAttachment,
|
||||
ExportableNote,
|
||||
exportNotes
|
||||
exportNotes,
|
||||
sanitizeFilename
|
||||
} 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";
|
||||
import { unlockVault } from "../utils/unlock-vault";
|
||||
|
||||
const MIMETypes = {
|
||||
txt: "text/plain",
|
||||
pdf: "application/pdf",
|
||||
md: "text/markdown",
|
||||
"md-frontmatter": "text/markdown",
|
||||
html: "text/html"
|
||||
};
|
||||
|
||||
const FolderNames: { [name: string]: string } = {
|
||||
txt: "Text",
|
||||
@@ -45,6 +59,16 @@ 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" &&
|
||||
@@ -58,11 +82,153 @@ async function getPath(type: string) {
|
||||
return path;
|
||||
}
|
||||
|
||||
async function unlockVaultForNoteExport() {
|
||||
return await unlockVault({
|
||||
title: "Unlock vault",
|
||||
paragraph: "Some exported notes are locked, Unlock to export them",
|
||||
context: "export-notes"
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,7 +285,6 @@ 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 =
|
||||
@@ -127,7 +292,6 @@ 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(
|
||||
@@ -139,8 +303,6 @@ async function createZip(
|
||||
await copyFileAsync("file://" + zipOutputPath, path);
|
||||
await RNFetchBlob.fs.unlink(zipOutputPath);
|
||||
callback();
|
||||
} else {
|
||||
path = zipOutputPath;
|
||||
}
|
||||
RNFetchBlob.fs.unlink(cacheFolder);
|
||||
} catch (e) {
|
||||
@@ -149,7 +311,6 @@ async function createZip(
|
||||
|
||||
return {
|
||||
filePath: path,
|
||||
fileDir: dir,
|
||||
type: "application/zip",
|
||||
name: "zip",
|
||||
fileName: fileName,
|
||||
@@ -227,7 +388,7 @@ async function bulkExport(
|
||||
let currentAttachmentProgress = 0;
|
||||
for await (const item of exportNotes(notes, {
|
||||
format: type,
|
||||
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
|
||||
unlockVault: unlockVault as () => Promise<boolean>
|
||||
})) {
|
||||
if (item instanceof Error) {
|
||||
DatabaseLogger.error(item);
|
||||
@@ -271,7 +432,7 @@ async function exportNote(
|
||||
let currentAttachmentProgress = 0;
|
||||
for await (const item of _exportNote(note, {
|
||||
format: type,
|
||||
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
|
||||
unlockVault: unlockVault as () => Promise<boolean>
|
||||
})) {
|
||||
if (item instanceof Error) {
|
||||
DatabaseLogger.error(item);
|
||||
|
||||
@@ -191,11 +191,11 @@ function canLockAppInBackground() {
|
||||
}
|
||||
let backgroundEnterTime = 0;
|
||||
function appEnteredBackground() {
|
||||
backgroundEnterTime = Date.now();
|
||||
if (canLockAppInBackground()) {
|
||||
backgroundEnterTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
const getBackgroundEnterTime = () => backgroundEnterTime;
|
||||
|
||||
function shouldLockAppOnEnterForeground() {
|
||||
if (
|
||||
useUserStore.getState().disableAppLockRequests ||
|
||||
@@ -227,8 +227,7 @@ export const SettingsService = {
|
||||
shouldLockAppOnEnterForeground,
|
||||
canLockAppInBackground,
|
||||
appEnteredBackground,
|
||||
setPrivacyScreen,
|
||||
getBackgroundEnterTime
|
||||
setPrivacyScreen
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
@@ -32,24 +32,19 @@ export const ignoredMessages = [
|
||||
"WebSocket failed to connect",
|
||||
"Failed to start the HttpConnection before"
|
||||
];
|
||||
let pendingSync: any = undefined;
|
||||
let syncTimer: NodeJS.Timeout;
|
||||
|
||||
let pendingSync = undefined;
|
||||
let syncTimer = 0;
|
||||
const run = async (
|
||||
context = "global",
|
||||
forced = false,
|
||||
type: "full" | "send" | "fetch" = "full",
|
||||
onCompleted?: (status?: number) => void,
|
||||
lastSyncTime?: number
|
||||
full = true,
|
||||
onCompleted,
|
||||
lastSyncTime
|
||||
) => {
|
||||
if (useUserStore.getState().syncing) {
|
||||
DatabaseLogger.info("Sync in progress");
|
||||
pendingSync = {
|
||||
forced,
|
||||
type: type,
|
||||
context: context,
|
||||
onCompleted,
|
||||
lastSyncTime
|
||||
full: full
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -75,7 +70,7 @@ const run = async (
|
||||
) {
|
||||
initAfterSync();
|
||||
pendingSync = undefined;
|
||||
return onCompleted?.(SyncStatus.Failed);
|
||||
return onCompleted?.(false);
|
||||
}
|
||||
userstore.setSyncing(true);
|
||||
|
||||
@@ -85,8 +80,9 @@ const run = async (
|
||||
await BackgroundSync.doInBackground(async () => {
|
||||
try {
|
||||
await db.sync({
|
||||
type: type,
|
||||
force: forced
|
||||
type: full ? "full" : "send",
|
||||
force: forced,
|
||||
lastSyncTime
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
@@ -99,16 +95,14 @@ const run = async (
|
||||
} catch (e) {
|
||||
error = e;
|
||||
if (
|
||||
!ignoredMessages.find((message) =>
|
||||
(e as Error).message?.includes(message)
|
||||
) &&
|
||||
!ignoredMessages.find((message) => e.message?.includes(message)) &&
|
||||
userstore.user &&
|
||||
status.isConnected &&
|
||||
status.isInternetReachable
|
||||
) {
|
||||
userstore.setSyncing(false, SyncStatus.Failed);
|
||||
if (status.isConnected && status.isInternetReachable) {
|
||||
ToastManager.error(e as Error, "Sync failed", context);
|
||||
ToastManager.error(e, "Sync failed", context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,14 +115,7 @@ const run = async (
|
||||
);
|
||||
onCompleted?.(error ? SyncStatus.Failed : SyncStatus.Passed);
|
||||
setImmediate(() => {
|
||||
if (pendingSync)
|
||||
Sync.run(
|
||||
pendingSync.context,
|
||||
pendingSync.forced,
|
||||
pendingSync.type,
|
||||
pendingSync.onCompleted,
|
||||
pendingSync.lastSyncTime
|
||||
);
|
||||
if (pendingSync) Sync.run("global", false, pendingSync.full);
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
@@ -174,4 +174,3 @@ export const eUnlockWithPassword = "619";
|
||||
export const eUpdateNoteInEditor = "620";
|
||||
export const eOnEnterEditor = "621";
|
||||
export const eOnExitEditor = "622";
|
||||
export const eEditorReset = "623";
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { 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 3011
|
||||
versionCode 3010
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
- Added push/pull changes to troubleshoot sync issues in settings
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -1015,7 +1015,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1120,7 +1120,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1194,7 +1194,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1353,7 +1353,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1365,7 +1365,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1396,7 +1396,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1409,7 +1409,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1439,7 +1439,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1544,7 +1544,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1619,7 +1619,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -350,8 +350,7 @@ PODS:
|
||||
- react-native-theme-switch-animation (0.6.0):
|
||||
- RCT-Folly (= 2021.07.22.00)
|
||||
- React-Core
|
||||
- react-native-webview (13.10.0):
|
||||
- RCT-Folly (= 2021.07.22.00)
|
||||
- react-native-webview (11.26.1):
|
||||
- React-Core
|
||||
- React-NativeModulesApple (0.72.0):
|
||||
- React-callinvoker
|
||||
@@ -908,7 +907,7 @@ SPEC CHECKSUMS:
|
||||
react-native-share-extension: faed334b1ddf165f1e576fcabd3dc1c9e748bfa9
|
||||
react-native-sodium: 955bb0dc3ea05f8ea06d5e96cb89d1be7b5d7681
|
||||
react-native-theme-switch-animation: 220f883f7be290e79f2ab022093ed1a7a5929e6d
|
||||
react-native-webview: 90153193e679163ec257011fa457d02765120210
|
||||
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
|
||||
React-NativeModulesApple: 1d81d927ef1a67a3545a01e14c2e98500bf9b199
|
||||
React-perflogger: 684a11499a0589cc42135d6d5cc04d0e4e0e261a
|
||||
React-RCTActionSheet: 00b0a4c382a13b834124fa3f541a7d8d1d56efb9
|
||||
@@ -959,4 +958,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.14.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": "^13.10.0",
|
||||
"react-native-webview": "^11.14.1",
|
||||
"react-native-zip-archive": "6.0.9",
|
||||
"react-native-quick-sqlite": "^8.0.6",
|
||||
"react-native-theme-switch-animation": "^0.6.0",
|
||||
|
||||
12
apps/mobile/package-lock.json
generated
12
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.2",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.2",
|
||||
"version": "3.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -3234,7 +3234,6 @@
|
||||
"@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",
|
||||
@@ -28496,7 +28495,7 @@
|
||||
"react-native-tooltips": "^1.0.3",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-vector-icons": "9.2.0",
|
||||
"react-native-webview": "^13.10.0",
|
||||
"react-native-webview": "^11.14.1",
|
||||
"react-native-zip-archive": "6.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45373,9 +45372,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webview": {
|
||||
"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==",
|
||||
"version": "11.26.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "2.0.0",
|
||||
"invariant": "2.2.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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));
|
||||
169
apps/mobile/patches/react-native-image-picker+4.1.2.patch
Normal file
169
apps/mobile/patches/react-native-image-picker+4.1.2.patch
Normal file
@@ -0,0 +1,169 @@
|
||||
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);
|
||||
@@ -1 +1 @@
|
||||
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)
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
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)
|
||||
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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 94 KiB |
@@ -1 +1 @@
|
||||
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made
|
||||
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
|
||||
@@ -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 notes.focus();
|
||||
await page.reload();
|
||||
|
||||
const note = await notes.findNote({ title: "Test note 3" });
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export class AppModel {
|
||||
);
|
||||
}
|
||||
|
||||
async waitForSync(state: "completed" | "synced" | "syncing" = "completed") {
|
||||
async waitForSync(state: "completed" | "synced" = "completed") {
|
||||
await this.page
|
||||
.locator(getTestId(`sync-status-${state}`))
|
||||
.waitFor({ state: "visible" });
|
||||
|
||||
@@ -284,9 +284,11 @@ test("change title of a locked note", async ({ page }) => {
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await page.reload();
|
||||
await notes.waitForList();
|
||||
const editedNote = await notes.findNote({ title });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const editedNote = await notes.findNote({ title, content: NOTE.content });
|
||||
await editedNote?.openLockedNote(PASSWORD);
|
||||
await notes.editor.waitForLoading();
|
||||
expect(await note?.getTitle()).toContain(title);
|
||||
expect(await notes.editor.getTitle()).toContain(title);
|
||||
});
|
||||
|
||||
@@ -17,25 +17,12 @@
|
||||
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
|
||||
|
||||
<style>
|
||||
|
||||
.image-container {
|
||||
display: block;
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.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;
|
||||
@@ -179,6 +166,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test 1</h1>
|
||||
<p data-block-id="xxx" data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
|
||||
<p data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,6 +19,7 @@ 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
|
||||
@@ -39,9 +40,7 @@ async function actAndSync<T>(
|
||||
) {
|
||||
const results = await Promise.all([
|
||||
...actions.filter((a) => !!a),
|
||||
...devices.map((d) =>
|
||||
d.waitForSync("syncing").then(() => d.waitForSync("synced"))
|
||||
)
|
||||
...devices.map((d) => d.waitForSync("synced"))
|
||||
]);
|
||||
|
||||
await Promise.all(devices.map((d) => d.page.waitForTimeout(2000)));
|
||||
@@ -62,7 +61,6 @@ 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())
|
||||
);
|
||||
@@ -74,17 +72,14 @@ 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).toBeEmpty();
|
||||
await expect(notesB.editor.content).toBeEmpty();
|
||||
await expect(notesA.editor.content).toHaveText("");
|
||||
await expect(notesB.editor.content).toHaveText("");
|
||||
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.3",
|
||||
"version": "3.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -31,8 +31,6 @@ 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,
|
||||
@@ -81,36 +79,20 @@ export async function exportNotes(
|
||||
format: "pdf" | "md" | "txt" | "html" | "md-frontmatter",
|
||||
notes: FilteredSelector<Note>
|
||||
): Promise<boolean> {
|
||||
const result = await TaskManager.startTask({
|
||||
return 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(exportStream)
|
||||
.pipeThrough(new ExportStream(report))
|
||||
.pipeThrough(createZipStream())
|
||||
.pipeTo(await createWriteStream("notes.zip"));
|
||||
return {
|
||||
errors,
|
||||
count: exportStream.progress
|
||||
};
|
||||
return true;
|
||||
}
|
||||
});
|
||||
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 = {
|
||||
@@ -148,9 +130,7 @@ export async function exportNote(
|
||||
unlockVault: Vault.unlockVault
|
||||
})
|
||||
)
|
||||
.pipeThrough(
|
||||
new ExportStream(report, (e) => showToast("error", e.message))
|
||||
)
|
||||
.pipeThrough(new ExportStream(report))
|
||||
.pipeThrough(createZipStream())
|
||||
.pipeTo(
|
||||
await createWriteStream(
|
||||
|
||||
@@ -75,10 +75,6 @@ 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({ type: "full", force: true });
|
||||
await appStore.sync(true, true);
|
||||
break;
|
||||
}
|
||||
case "backup": {
|
||||
|
||||
@@ -73,44 +73,15 @@ import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
async function saveContent(
|
||||
noteId: string,
|
||||
ignoreEdit: boolean,
|
||||
content: string
|
||||
) {
|
||||
function saveContent(noteId: string, ignoreEdit: boolean, content: string) {
|
||||
logger.debug("saving content", {
|
||||
noteId,
|
||||
ignoreEdit,
|
||||
length: content.length
|
||||
});
|
||||
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
|
||||
);
|
||||
useEditorStore.getState().saveSessionContent(noteId, ignoreEdit, {
|
||||
type: "tiptap",
|
||||
data: content
|
||||
});
|
||||
}
|
||||
const deferredSave = debounceWithId(saveContent, 100);
|
||||
@@ -275,7 +246,7 @@ function EditorView({
|
||||
if (!item.locked) return editor.updateContent(item.data);
|
||||
|
||||
const result = await db.vault
|
||||
.decryptContent(item)
|
||||
.decryptContent(item, item.noteId)
|
||||
.catch(() => EV.publish(EVENTS.vaultLocked));
|
||||
if (!result) return;
|
||||
editor.updateContent(result.data);
|
||||
|
||||
@@ -212,8 +212,7 @@ import {
|
||||
mdiCalendarBlank,
|
||||
mdiFormatListBulleted,
|
||||
mdiLink,
|
||||
mdiWindowClose,
|
||||
mdiFileMusicOutline
|
||||
mdiWindowClose
|
||||
} from "@mdi/js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
@@ -517,7 +516,6 @@ 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,8 +284,7 @@ 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.color?.id === nextProps.color?.id
|
||||
prevProps.locked === nextProps.locked
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
Close,
|
||||
DoubleCheckmark,
|
||||
Download,
|
||||
FileAudio,
|
||||
FileDocument,
|
||||
FileGeneral,
|
||||
FileImage,
|
||||
@@ -112,8 +111,7 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
||||
images: 0,
|
||||
orphaned: 0,
|
||||
uploads: 0,
|
||||
videos: 0,
|
||||
audio: 0
|
||||
videos: 0
|
||||
});
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [sortBy, setSortBy] = useState<SortOptions>({
|
||||
@@ -377,14 +375,7 @@ function AttachmentRow(
|
||||
);
|
||||
}
|
||||
|
||||
type Route =
|
||||
| "all"
|
||||
| "images"
|
||||
| "documents"
|
||||
| "videos"
|
||||
| "audio"
|
||||
| "uploads"
|
||||
| "orphaned";
|
||||
type Route = "all" | "images" | "documents" | "videos" | "uploads" | "orphaned";
|
||||
|
||||
const routes: { id: Route; icon: Icon; title: string }[] = [
|
||||
{
|
||||
@@ -407,11 +398,6 @@ const routes: { id: Route; icon: Icon; title: string }[] = [
|
||||
icon: FileVideo,
|
||||
title: "Videos"
|
||||
},
|
||||
{
|
||||
id: "audio",
|
||||
icon: FileAudio,
|
||||
title: "Audios"
|
||||
},
|
||||
{
|
||||
id: "uploads",
|
||||
icon: Uploading,
|
||||
@@ -539,7 +525,6 @@ 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()
|
||||
};
|
||||
@@ -556,7 +541,5 @@ function filterAttachments(route: Route) {
|
||||
? db.attachments.documents
|
||||
: route === "orphaned"
|
||||
? db.attachments.orphaned
|
||||
: route === "audio"
|
||||
? db.attachments.audios
|
||||
: db.attachments.pending;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ function ProgressDialog(props) {
|
||||
try {
|
||||
props.onDone(await props.action(setProgress));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
props.onDone(e);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -19,7 +19,6 @@ 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[] = [
|
||||
{
|
||||
@@ -79,56 +78,14 @@ export const SyncSettings: SettingsGroup[] = [
|
||||
{
|
||||
key: "force-sync",
|
||||
title: "Having problems with sync?",
|
||||
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.**`,
|
||||
description: "Try force sync to resolve issues with syncing.",
|
||||
keywords: ["force sync", "sync troubleshoot"],
|
||||
components: [
|
||||
{
|
||||
type: "button",
|
||||
title: "Force push",
|
||||
title: "Force sync",
|
||||
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: "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" });
|
||||
})
|
||||
action: () => useAppStore.getState().sync(true, true)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ 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"
|
||||
@@ -58,7 +57,7 @@ type SyncStatus = {
|
||||
};
|
||||
const networkCheck = new NetworkCheck();
|
||||
let syncStatusTimeout = 0;
|
||||
let pendingSync: SyncOptions | undefined = undefined;
|
||||
let pendingSync: { full: boolean } | undefined = undefined;
|
||||
|
||||
class AppStore extends BaseStore<AppStore> {
|
||||
// default state
|
||||
@@ -70,7 +69,11 @@ class AppStore extends BaseStore<AppStore> {
|
||||
isSyncEnabled = Config.get("syncEnabled", true);
|
||||
isRealtimeSyncEnabled = Config.get("isRealtimeSyncEnabled", true);
|
||||
syncStatus: SyncStatus = {
|
||||
key: navigator.onLine ? "disabled" : "offline",
|
||||
key: navigator.onLine
|
||||
? Config.get("syncEnabled", true)
|
||||
? "synced"
|
||||
: "disabled"
|
||||
: "offline",
|
||||
progress: null,
|
||||
type: undefined
|
||||
};
|
||||
@@ -112,9 +115,9 @@ class AppStore extends BaseStore<AppStore> {
|
||||
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.databaseSyncRequested,
|
||||
async (full, force) => {
|
||||
async (full, force, lastSynced) => {
|
||||
if (!this.get().isAutoSyncEnabled) return;
|
||||
await this.get().sync({ type: full ? "full" : "send", force });
|
||||
await this.get().sync(full, force, lastSynced);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -265,7 +268,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
this.set((state) => (state.lastSynced = lastSynced));
|
||||
};
|
||||
|
||||
sync = async (options: SyncOptions = { type: "full", force: false }) => {
|
||||
sync = async (full = true, force = false, lastSynced?: number) => {
|
||||
if (
|
||||
this.isSyncing() ||
|
||||
!this.get().isSyncEnabled ||
|
||||
@@ -273,12 +276,14 @@ class AppStore extends BaseStore<AppStore> {
|
||||
!(await networkCheck.waitForInternet())
|
||||
) {
|
||||
logger.info("Ignoring duplicate sync", {
|
||||
options,
|
||||
full,
|
||||
force,
|
||||
lastSynced,
|
||||
syncing: this.isSyncing(),
|
||||
syncDisabled: !this.get().isSyncEnabled,
|
||||
offline: !navigator.onLine
|
||||
});
|
||||
if (this.isSyncing()) pendingSync = options;
|
||||
if (this.isSyncing()) pendingSync = { full };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,7 +292,10 @@ class AppStore extends BaseStore<AppStore> {
|
||||
|
||||
this.updateSyncStatus("syncing");
|
||||
try {
|
||||
const result = await db.sync(options);
|
||||
const result = await db.sync({
|
||||
type: full ? "full" : "send",
|
||||
force
|
||||
});
|
||||
|
||||
if (!result) return this.updateSyncStatus("failed");
|
||||
this.updateSyncStatus("completed", true);
|
||||
@@ -296,9 +304,9 @@ class AppStore extends BaseStore<AppStore> {
|
||||
|
||||
if (pendingSync) {
|
||||
logger.info("Running pending sync", pendingSync);
|
||||
const syncOptions = { ...pendingSync };
|
||||
const isFullSync = pendingSync.full;
|
||||
pendingSync = undefined;
|
||||
await this.get().sync(syncOptions);
|
||||
await this.get().sync(isFullSync, false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
@@ -308,6 +316,9 @@ 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,7 +47,6 @@ 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,
|
||||
@@ -223,15 +222,8 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const clearIds: string[] = [];
|
||||
for (const session of sessions) {
|
||||
if (session.type === "new") 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 (session.id !== item.id && session.note.id !== item.id) continue;
|
||||
|
||||
if (isDeleted(item) || isTrashItem(item)) clearIds.push(session.id);
|
||||
// if a note is locked, reopen the session
|
||||
else if (
|
||||
@@ -240,26 +232,21 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
item.type === "tiptap" &&
|
||||
item.locked
|
||||
) {
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
openSession(item.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)
|
||||
) {
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
openSession(item.id, { force: true, silent: true });
|
||||
}
|
||||
// if a deleted note is restored, reopen the session
|
||||
else if (session.type === "deleted" && item.type === "note") {
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
openSession(item.id, { force: true, silent: true });
|
||||
}
|
||||
// if a readonly note is made editable, reopen the session
|
||||
else if (
|
||||
@@ -267,9 +254,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
item.type === "note" &&
|
||||
!item.readonly
|
||||
)
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
openSession(item.id, { force: true, silent: true });
|
||||
// update the note in all sessions
|
||||
else if (item.type === "note") {
|
||||
updateSession(
|
||||
@@ -343,15 +328,12 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const contentId =
|
||||
session.type !== "new" && session.note.contentId;
|
||||
if (!contentId || !event.ids.includes(contentId)) continue;
|
||||
|
||||
if (
|
||||
// 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)
|
||||
// when a note is locked or unlocked
|
||||
session.type !== "locked" &&
|
||||
!!event.item.locked &&
|
||||
(session.type !== "default" || !session.locked)
|
||||
) {
|
||||
openSession(session.id, { force: true, silent: true });
|
||||
}
|
||||
@@ -618,7 +600,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
: undefined;
|
||||
|
||||
if (content?.locked) {
|
||||
await Vault.lockNote(noteId);
|
||||
await db.vault.add(noteId);
|
||||
return this.openSession(note, { ...options, force: true });
|
||||
}
|
||||
|
||||
@@ -981,10 +963,3 @@ 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,6 +19,7 @@ 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";
|
||||
|
||||
@@ -26,15 +27,12 @@ export class ExportStream extends TransformStream<
|
||||
ExportableItem | Error,
|
||||
ZipFile
|
||||
> {
|
||||
progress = 0;
|
||||
constructor(
|
||||
report: (progress: { text: string; current?: number }) => void,
|
||||
handleError: (error: Error) => void
|
||||
) {
|
||||
constructor(report: (progress: { text: string; current?: number }) => void) {
|
||||
let progress = 0;
|
||||
super({
|
||||
transform: async (item, controller) => {
|
||||
async transform(item, controller) {
|
||||
if (item instanceof Error) {
|
||||
handleError(item);
|
||||
showToast("error", item.message);
|
||||
return;
|
||||
}
|
||||
if (item.type === "attachment") {
|
||||
@@ -58,13 +56,13 @@ export class ExportStream extends TransformStream<
|
||||
if (!stream) return;
|
||||
controller.enqueue({ ...item, data: stream });
|
||||
report({
|
||||
current: this.progress++,
|
||||
current: progress++,
|
||||
text: `Saving attachment: ${item.path}`
|
||||
});
|
||||
} else {
|
||||
controller.enqueue(item);
|
||||
report({
|
||||
current: this.progress++,
|
||||
current: progress++,
|
||||
text: `Exporting note: ${item.path}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
- Added push/pull changes to troubleshoot sync issues in settings
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -104,37 +104,31 @@ export async function* exportNotes(
|
||||
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)
|
||||
};
|
||||
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]}}}`;
|
||||
}
|
||||
} catch (e) {
|
||||
yield new Error(
|
||||
`Failed to export note "${note.title}": ${(e as Error).message}`
|
||||
);
|
||||
});
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,15 +225,10 @@ export async function exportContent(
|
||||
}
|
||||
|
||||
const contentItem = rawContent?.locked
|
||||
? 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;
|
||||
? await database.vault
|
||||
.decryptContent(rawContent, note.id)
|
||||
.catch(() => undefined)
|
||||
: rawContent;
|
||||
|
||||
const { data, type } =
|
||||
format === "pdf"
|
||||
|
||||
@@ -109,17 +109,16 @@ test(
|
||||
});
|
||||
|
||||
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}` });
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await Promise.all([
|
||||
deviceA.sync({ type: "send" }),
|
||||
new Promise((resolve) => setTimeout(resolve, 40)).then(() =>
|
||||
new Promise((resolve) => setTimeout(resolve), 100).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);
|
||||
expect((await deviceA.notes.note(id))?.synced).toBe(false);
|
||||
await deviceA.sync({ type: "send" });
|
||||
await deviceB.sync({ type: "fetch" });
|
||||
expect((await deviceB.notes.note(id))?.title).toBe(`edit ${i}`);
|
||||
}
|
||||
@@ -140,17 +139,17 @@ test(
|
||||
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}` });
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await Promise.all([
|
||||
deviceA.sync({ type: "send" }),
|
||||
new Promise((resolve) => setTimeout(resolve, 40)).then(() =>
|
||||
new Promise((resolve) => setTimeout(resolve), 100).then(() =>
|
||||
deviceA.notes.add({ title: `note ${i}` })
|
||||
)
|
||||
]);
|
||||
expect(await deviceB.notes.all.count()).toBe(i);
|
||||
await deviceA.sync({ type: "send" });
|
||||
await deviceB.sync({ type: "fetch" });
|
||||
expect(await deviceB.notes.all.count()).toBe(i + 2);
|
||||
expect(await deviceB.notes.all.count()).toBe(i + 1);
|
||||
}
|
||||
},
|
||||
TEST_TIMEOUT * 10
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { expect, test } from "vitest";
|
||||
import { databaseTest, noteTest } from "./utils";
|
||||
import { TEST_NOTE, databaseTest, noteTest } from "./utils";
|
||||
|
||||
test("updating deleted content should not throw", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
|
||||
@@ -144,7 +144,6 @@ 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,7 +95,6 @@ class Database {
|
||||
isInitialized = false;
|
||||
eventManager = new EventManager();
|
||||
sseMutex = new Mutex();
|
||||
_fs?: FileStorage;
|
||||
|
||||
storage: StorageAccessor = () => {
|
||||
if (!this.options?.storage)
|
||||
@@ -110,10 +109,7 @@ class Database {
|
||||
throw new Error(
|
||||
"Database not initialized. Did you forget to call db.setup()?"
|
||||
);
|
||||
return (
|
||||
this._fs ||
|
||||
(this._fs = new FileStorage(this.options.fs, this.tokenManager))
|
||||
);
|
||||
return new FileStorage(this.options.fs, this.tokenManager);
|
||||
};
|
||||
|
||||
crypto: CryptoAccessor = () => {
|
||||
|
||||
@@ -25,7 +25,6 @@ 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>>;
|
||||
@@ -246,9 +245,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,15 +33,6 @@ 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
|
||||
|
||||
@@ -160,7 +160,7 @@ class Sync {
|
||||
)
|
||||
this.logger.info("New data sent");
|
||||
|
||||
await this.stop(options);
|
||||
await this.stop();
|
||||
|
||||
if (!(await checkSyncStatus(SYNC_CHECK_IDS.autoSync))) {
|
||||
await this.connection.stop();
|
||||
@@ -265,15 +265,7 @@ class Sync {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
async stop() {
|
||||
// refresh monographs
|
||||
await this.db.monographs.refresh().catch(this.logger.error);
|
||||
// update trash cache
|
||||
|
||||
@@ -82,7 +82,7 @@ class TokenManager {
|
||||
async getAccessToken(forceRenew = false) {
|
||||
return await getSafeToken(async () => {
|
||||
const token = await this.getToken(true, forceRenew);
|
||||
if (!token || token.scope.includes("auth:grant_types")) return;
|
||||
if (!token) return;
|
||||
return token.access_token;
|
||||
}, "Error getting access token:");
|
||||
}
|
||||
|
||||
@@ -99,9 +99,8 @@ class UserManager {
|
||||
async authenticateMultiFactorCode(code: string, method: string) {
|
||||
if (!code || !method) throw new Error("code & method are required.");
|
||||
|
||||
const token = await this.tokenManager.getToken();
|
||||
if (!token || token.scope !== "auth:grant_types:mfa")
|
||||
throw new Error("No token found.");
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
if (!token) throw new Error("Unauthorized.");
|
||||
|
||||
await this.tokenManager.saveToken(
|
||||
await http.post(
|
||||
@@ -112,7 +111,7 @@ class UserManager {
|
||||
"mfa:code": code,
|
||||
"mfa:method": method
|
||||
},
|
||||
token.access_token
|
||||
token
|
||||
)
|
||||
);
|
||||
return true;
|
||||
@@ -127,8 +126,7 @@ class UserManager {
|
||||
if (!email || !password) throw new Error("email & password are required.");
|
||||
|
||||
const token = await this.tokenManager.getToken();
|
||||
if (!token || token.scope !== "auth:grant_types:mfa_password")
|
||||
throw new Error("No token found.");
|
||||
if (!token) throw new Error("No token found.");
|
||||
|
||||
email = email.toLowerCase();
|
||||
if (!hashedPassword) {
|
||||
|
||||
@@ -255,7 +255,6 @@ export default class Vault {
|
||||
noteId,
|
||||
sessionId,
|
||||
data: encryptedContent,
|
||||
dateEdited: Date.now(),
|
||||
type: content.type
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,12 +22,10 @@ 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";
|
||||
@@ -38,7 +36,6 @@ 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";
|
||||
@@ -95,44 +92,10 @@ function isEncryptedBackup(
|
||||
return "encrypted" in backup ? backup.encrypted : isCipher(backup.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
function isLegacyBackupFile(
|
||||
backup: LegacyBackupFile | BackupFile
|
||||
): backup is LegacyBackupFile {
|
||||
return backup.version <= 5.8;
|
||||
}
|
||||
|
||||
const MAX_CHUNK_SIZE = 10 * 1024 * 1024;
|
||||
@@ -258,7 +221,7 @@ export default class Backup {
|
||||
yield {
|
||||
path: `${chunkIndex++}-${encrypt ? "encrypted" : "plain"}-${hash}`,
|
||||
data: `{
|
||||
"version": 5.9,
|
||||
"version": ${CURRENT_DATABASE_VERSION},
|
||||
"type": "${type}",
|
||||
"date": ${Date.now()},
|
||||
"data": ${itemsJSON},
|
||||
@@ -434,16 +397,13 @@ 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(
|
||||
normalizedData,
|
||||
backup.version === 6.1 && isLegacyBackup(normalizedData)
|
||||
? 5.9
|
||||
: backup.version
|
||||
Array.isArray(data)
|
||||
? (data as BackupDataItem[])
|
||||
: typeof data === "object"
|
||||
? Object.values(data)
|
||||
: [],
|
||||
backup.version
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,22 +30,16 @@ 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 {
|
||||
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>>()
|
||||
};
|
||||
|
||||
downloads = new Map<string, QueueItem[]>();
|
||||
uploads = new Map<string, QueueItem[]>();
|
||||
constructor(
|
||||
private readonly fs: IFileStorage,
|
||||
private readonly tokenManager: TokenManager
|
||||
@@ -56,26 +50,12 @@ export class FileStorage {
|
||||
groupId: string,
|
||||
eventData?: Record<string, unknown>
|
||||
) {
|
||||
let current = 0;
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
const total = files.length;
|
||||
const group = this.groups.downloads.get(groupId) || new Set();
|
||||
files.forEach((f) => group.add(f.filename));
|
||||
this.groups.downloads.set(groupId, group);
|
||||
let current = 0;
|
||||
this.downloads.set(groupId, files);
|
||||
|
||||
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++;
|
||||
@@ -88,13 +68,6 @@ export class FileStorage {
|
||||
continue;
|
||||
}
|
||||
|
||||
EV.publish(EVENTS.fileDownload, {
|
||||
total,
|
||||
current,
|
||||
groupId,
|
||||
filename
|
||||
});
|
||||
|
||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||
const { execute, cancel } = this.fs.downloadFile(filename, {
|
||||
url,
|
||||
@@ -102,15 +75,15 @@ export class FileStorage {
|
||||
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;
|
||||
EV.publish(EVENTS.fileDownload, {
|
||||
total,
|
||||
current,
|
||||
groupId,
|
||||
filename
|
||||
});
|
||||
|
||||
const result = await execute().catch(() => false);
|
||||
if (eventData)
|
||||
EV.publish(EVENTS.fileDownloaded, {
|
||||
success: result,
|
||||
@@ -121,31 +94,17 @@ 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;
|
||||
const group = this.groups.uploads.get(groupId) || new Set();
|
||||
files.forEach((f) => group.add(f.filename));
|
||||
this.groups.uploads.set(groupId, group);
|
||||
let current = 0;
|
||||
this.uploads.set(groupId, files);
|
||||
|
||||
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,
|
||||
@@ -153,16 +112,6 @@ 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,
|
||||
@@ -171,8 +120,13 @@ export class FileStorage {
|
||||
filename
|
||||
});
|
||||
|
||||
this.uploads.set(filename, file);
|
||||
const result = await file.operation;
|
||||
let error = null;
|
||||
const result = await execute().catch((e) => {
|
||||
logger.error(e, "failed to upload attachment", { hash: filename });
|
||||
error = e;
|
||||
return false;
|
||||
});
|
||||
|
||||
EV.publish(EVENTS.fileUploaded, {
|
||||
error,
|
||||
success: result,
|
||||
@@ -182,67 +136,44 @@ 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}` }
|
||||
});
|
||||
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;
|
||||
this.downloads.set(groupId, [{ cancel, filename, chunkSize }]);
|
||||
const result = await execute();
|
||||
this.downloads.delete(groupId);
|
||||
return result;
|
||||
}
|
||||
|
||||
async cancel(groupId: string) {
|
||||
const queues = [
|
||||
{
|
||||
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);
|
||||
{ type: "download", files: this.downloads.get(groupId) },
|
||||
{ type: "upload", files: this.uploads.get(groupId) }
|
||||
].filter((a) => !!a.files);
|
||||
|
||||
for (const queue of queues) {
|
||||
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.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.type === "download") {
|
||||
this.groups.downloads.delete(groupId);
|
||||
this.downloads.delete(groupId);
|
||||
EV.publish(EVENTS.downloadCanceled, { groupId, canceled: true });
|
||||
} else if (queue.type === "upload") {
|
||||
this.groups.uploads.delete(groupId);
|
||||
this.uploads.delete(groupId);
|
||||
EV.publish(EVENTS.uploadCanceled, { groupId, canceled: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { 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,7 +167,6 @@ 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,7 +24,6 @@ import {
|
||||
MigrationProvider,
|
||||
sql
|
||||
} from "kysely";
|
||||
import { rebuildSearchIndex } from "./fts";
|
||||
|
||||
const COLLATE_NOCASE: ColumnBuilderCallback = (col) =>
|
||||
col.modifyEnd(sql`collate nocase`);
|
||||
@@ -284,11 +283,6 @@ export class NNMigrationProvider implements MigrationProvider {
|
||||
.execute();
|
||||
},
|
||||
async down(db) {}
|
||||
},
|
||||
"2": {
|
||||
async up(db) {
|
||||
await rebuildSearchIndex(db);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
} from "../types";
|
||||
import { IndexedCollection } from "./indexed-collection";
|
||||
import { SQLCollection } from "./sql-collection";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export type RawItem = MaybeDeletedItem<Item>;
|
||||
type MigratableCollection = {
|
||||
@@ -137,16 +136,10 @@ 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)) {
|
||||
logger.debug("Skipping item during migration to SQLite", {
|
||||
table,
|
||||
version,
|
||||
item
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(item)) continue;
|
||||
if (!item) continue;
|
||||
|
||||
const itemId = item.id;
|
||||
let migrated = await migrateItem(
|
||||
item,
|
||||
version,
|
||||
@@ -168,7 +161,14 @@ class Migrator {
|
||||
);
|
||||
}
|
||||
|
||||
if (migrated !== "skip") toAdd.push(item);
|
||||
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 (toAdd.length > 0) {
|
||||
@@ -217,7 +217,7 @@ class Migrator {
|
||||
);
|
||||
}
|
||||
|
||||
if (!migrated || migrated === "skip") continue;
|
||||
if (!migrated) 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 } from "kysely";
|
||||
import { Kysely, Transaction } from "kysely";
|
||||
import { Sanitizer } from "./sanitizer";
|
||||
|
||||
export class SQLCachedCollection<
|
||||
@@ -171,10 +171,6 @@ export class SQLCachedCollection<
|
||||
}
|
||||
}
|
||||
|
||||
async unsyncedCount() {
|
||||
return this.collection.unsyncedCount();
|
||||
}
|
||||
|
||||
// has(id: string) {
|
||||
// return this.cache.has(id);
|
||||
// }
|
||||
|
||||
@@ -278,21 +278,6 @@ 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
|
||||
@@ -552,11 +537,9 @@ export class FilteredSelector<T extends Item> {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
let lastRow: any | null = null;
|
||||
const fields = this._fields.slice();
|
||||
if (fields.length > 0) {
|
||||
if (!fields.find((f) => f.includes(".dateCreated")))
|
||||
fields.push("dateCreated");
|
||||
if (!fields.find((f) => f.includes(".id"))) fields.push("id");
|
||||
}
|
||||
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.noteId", "is not", null),
|
||||
eb("new.data", "is not", null),
|
||||
eb("new.deleted", "is not", true)
|
||||
eb("new.deleted", "is not", true),
|
||||
eb("new.locked", "is not", true),
|
||||
eb("new.data", "is not", null)
|
||||
])
|
||||
)
|
||||
.addQuery((c) =>
|
||||
c.insertInto("content_fts").values({
|
||||
rowid: sql`new.rowid`,
|
||||
id: sql`new.id`,
|
||||
data: sql`IIF(new.locked == 1, '', new.data)`,
|
||||
data: sql`new.data`,
|
||||
noteId: sql`new.noteId`
|
||||
})
|
||||
)
|
||||
@@ -53,13 +53,6 @@ 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"),
|
||||
@@ -80,9 +73,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.deleted", "is not", true)
|
||||
eb("old.data", "is not", null)
|
||||
])
|
||||
)
|
||||
.addQuery((c) =>
|
||||
@@ -114,8 +107,7 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
|
||||
.addEvent("insert")
|
||||
.when((eb) =>
|
||||
eb.and([
|
||||
eb("new.title", "is not", null),
|
||||
eb("new.deleted", "is not", true)
|
||||
eb.or([eb("new.deleted", "is", null), eb("new.deleted", "==", false)])
|
||||
])
|
||||
)
|
||||
.addQuery((c) =>
|
||||
@@ -134,12 +126,6 @@ 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
packages/editor-mobile/.gitignore
vendored
1
packages/editor-mobile/.gitignore
vendored
@@ -22,4 +22,3 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
sourcemaps
|
||||
|
||||
44
packages/editor-mobile/package-lock.json
generated
44
packages/editor-mobile/package-lock.json
generated
@@ -17,7 +17,6 @@
|
||||
"@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",
|
||||
@@ -4376,7 +4375,7 @@
|
||||
"version": "15.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/q": {
|
||||
"version": "1.5.8",
|
||||
@@ -4400,7 +4399,7 @@
|
||||
"version": "18.2.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
|
||||
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
@@ -4435,7 +4434,7 @@
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
@@ -9727,16 +9726,11 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "9.0.21",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -12805,14 +12799,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||
@@ -12850,14 +12836,6 @@
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"dependencies": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -17822,20 +17800,6 @@
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"@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",
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
/* color: var(--nn_primary_paragraph) ## TODO: use fixed color */
|
||||
}
|
||||
|
||||
.main-editor > .ProseMirror:first-child {
|
||||
margin-top: -5px !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: white;
|
||||
background-color: var(--nn_primary_accent);
|
||||
|
||||
@@ -31,6 +31,7 @@ import { TabContext, useTabStore } from "./hooks/useTabStore";
|
||||
import { EmotionEditorTheme } from "./theme-factory";
|
||||
import { getTheme } from "./utils";
|
||||
import { ReadonlyEditorProvider } from "./components/readonly-editor";
|
||||
import { error } from "console";
|
||||
|
||||
const currentTheme = getTheme();
|
||||
if (currentTheme) {
|
||||
|
||||
@@ -40,19 +40,17 @@ import {
|
||||
import { useEditorController } from "../hooks/useEditorController";
|
||||
import { useSettings } from "../hooks/useSettings";
|
||||
import {
|
||||
NoteState,
|
||||
TabItem,
|
||||
TabStore,
|
||||
useTabContext,
|
||||
useTabStore
|
||||
} from "../hooks/useTabStore";
|
||||
import { EmotionEditorToolbarTheme } from "../theme-factory";
|
||||
import { EventTypes, postAsyncWithTimeout, randId, Settings } from "../utils";
|
||||
import { EventTypes, randId, Settings } from "../utils";
|
||||
import Header from "./header";
|
||||
import StatusBar from "./statusbar";
|
||||
import Tags from "./tags";
|
||||
import Title from "./title";
|
||||
import { pendingSaveRequests } from "../utils/pending-saves";
|
||||
|
||||
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
|
||||
|
||||
@@ -79,37 +77,6 @@ const Tiptap = ({
|
||||
const isFocusedRef = useRef<boolean>(false);
|
||||
tabRef.current = tab;
|
||||
|
||||
function restoreNoteSelection(state?: NoteState) {
|
||||
try {
|
||||
if (!tabRef.current.noteId) return;
|
||||
const noteState =
|
||||
state || useTabStore.getState().noteState[tabRef.current.noteId];
|
||||
|
||||
if (noteState && (noteState.to || noteState.from)) {
|
||||
const size = editors[tabRef.current.id]?.state.doc.content.size || 0;
|
||||
if (
|
||||
noteState.to > 0 &&
|
||||
noteState.to <= size &&
|
||||
noteState.from > 0 &&
|
||||
noteState.from <= size
|
||||
) {
|
||||
editors[tabRef.current.id]?.chain().setTextSelection({
|
||||
to: noteState.to,
|
||||
from: noteState.from
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
containerRef.current?.scrollTo({
|
||||
left: 0,
|
||||
top: noteState?.top || 0,
|
||||
behavior: "auto"
|
||||
});
|
||||
} catch (e) {
|
||||
logger("error", (e as Error).message, (e as Error).stack);
|
||||
}
|
||||
}
|
||||
|
||||
usePermissionHandler({
|
||||
claims: {
|
||||
premium: settings.premium
|
||||
@@ -146,8 +113,20 @@ const Tiptap = ({
|
||||
) as Promise<string | undefined>;
|
||||
},
|
||||
createInternalLink(attributes) {
|
||||
return postAsyncWithTimeout(EventTypes.createInternalLink, {
|
||||
attributes
|
||||
logger("info", "create internal link");
|
||||
return new Promise((resolve) => {
|
||||
const id = randId("createInternalLink");
|
||||
|
||||
globalThis.pendingResolvers[id] = (value) => {
|
||||
delete globalThis.pendingResolvers[id];
|
||||
resolve(value);
|
||||
logger("info", "resolved create link request:", id);
|
||||
};
|
||||
|
||||
post("editor-events:create-internal-link", {
|
||||
attributes: attributes,
|
||||
resolverId: id
|
||||
});
|
||||
});
|
||||
},
|
||||
element: getContentDiv(),
|
||||
@@ -213,7 +192,23 @@ const Tiptap = ({
|
||||
},
|
||||
onCreate() {
|
||||
setTimeout(() => {
|
||||
restoreNoteSelection();
|
||||
const noteState = tabRef.current.noteId
|
||||
? useTabStore.getState().noteState[tabRef.current.noteId]
|
||||
: undefined;
|
||||
const top = noteState?.top;
|
||||
logger("info", tabRef.current.noteId, noteState?.top);
|
||||
|
||||
if (noteState?.to || noteState?.from) {
|
||||
editors[tabRef.current.id]?.chain().setTextSelection({
|
||||
to: noteState.to,
|
||||
from: noteState.from
|
||||
});
|
||||
}
|
||||
containerRef.current?.scrollTo({
|
||||
left: 0,
|
||||
top: top || 0,
|
||||
behavior: "auto"
|
||||
});
|
||||
}, 32);
|
||||
},
|
||||
downloadOptions: {
|
||||
@@ -239,6 +234,7 @@ const Tiptap = ({
|
||||
const _editor = useTiptap(tiptapOptions, [tiptapOptions]);
|
||||
|
||||
const update = useCallback(() => {
|
||||
logger("info", "LOADING NOTE...");
|
||||
editors[tabRef.current.id]?.commands.setTextSelection(0);
|
||||
setTick((tick) => tick + 1);
|
||||
globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder(
|
||||
@@ -255,9 +251,6 @@ const Tiptap = ({
|
||||
return !containerRef.current
|
||||
? []
|
||||
: getTableOfContents(containerRef.current);
|
||||
},
|
||||
scrollTo: (top) => {
|
||||
containerRef.current?.scrollTo({ top, behavior: "auto" });
|
||||
}
|
||||
});
|
||||
const controllerRef = useRef(controller);
|
||||
@@ -275,17 +268,6 @@ const Tiptap = ({
|
||||
if (!didCallOnLoad) {
|
||||
didCallOnLoad = true;
|
||||
post("editor-events:load");
|
||||
pendingSaveRequests
|
||||
.getPendingContentIds()
|
||||
.then(async (result) => {
|
||||
if (result && result.length) {
|
||||
dbLogger("log", "Pending save requests found... restoring");
|
||||
await pendingSaveRequests.postPendingRequests();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
logger("info", "Error restoring pending contents...");
|
||||
});
|
||||
}
|
||||
|
||||
const updateScrollPosition = (state: TabStore) => {
|
||||
@@ -295,26 +277,18 @@ const Tiptap = ({
|
||||
const noteState = tabRef.current.noteId
|
||||
? state.noteState[tabRef.current.noteId]
|
||||
: undefined;
|
||||
|
||||
post(
|
||||
EventTypes.tabFocused,
|
||||
!!globalThis.editorControllers[tabRef.current.id]?.content.current &&
|
||||
!editorControllers[tabRef.current.id]?.loading,
|
||||
tabRef.current.id,
|
||||
state.getCurrentNoteId()
|
||||
);
|
||||
editorControllers[tabRef.current.id]?.updateTab();
|
||||
|
||||
if (noteState) {
|
||||
if (
|
||||
containerRef.current &&
|
||||
containerRef.current?.scrollHeight < noteState.top
|
||||
) {
|
||||
console.log("Container too small to scroll.");
|
||||
return;
|
||||
containerRef.current?.scrollTo({
|
||||
left: 0,
|
||||
top: noteState.top,
|
||||
behavior: "auto"
|
||||
});
|
||||
if (noteState.to || noteState.from) {
|
||||
editors[tabRef.current.id]?.chain().setTextSelection({
|
||||
to: noteState.to,
|
||||
from: noteState.from
|
||||
});
|
||||
}
|
||||
|
||||
restoreNoteSelection(noteState);
|
||||
} else {
|
||||
containerRef.current?.scrollTo({
|
||||
left: 0,
|
||||
@@ -329,13 +303,20 @@ const Tiptap = ({
|
||||
) {
|
||||
editorControllers[tabRef.current.id]?.setLoading(true);
|
||||
}
|
||||
post(
|
||||
EventTypes.tabFocused,
|
||||
!!globalThis.editorControllers[tabRef.current.id]?.content.current &&
|
||||
!editorControllers[tabRef.current.id]?.loading,
|
||||
tabRef.current.id,
|
||||
state.getCurrentNoteId()
|
||||
);
|
||||
editorControllers[tabRef.current.id]?.updateTab();
|
||||
} else {
|
||||
isFocusedRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
updateScrollPosition(useTabStore.getState());
|
||||
|
||||
const unsub = useTabStore.subscribe((state, prevState) => {
|
||||
if (state.currentTab !== tabRef.current.id) {
|
||||
isFocusedRef.current = false;
|
||||
@@ -433,105 +414,6 @@ const Tiptap = ({
|
||||
noHeader={settings.noHeader || false}
|
||||
/>
|
||||
|
||||
<div
|
||||
id="editor-saving-failed-overlay"
|
||||
style={{
|
||||
display: "none",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
rowGap: 10
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
color: colors.primary.paragraph,
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
padding: "0px 20px",
|
||||
marginBottom: 0,
|
||||
userSelect: "none"
|
||||
}}
|
||||
>
|
||||
Your changes could not be saved.
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
color: colors.primary.paragraph,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
userSelect: "none",
|
||||
textAlign: "center",
|
||||
maxWidth: "90%",
|
||||
fontSize: "0.9rem"
|
||||
}}
|
||||
>
|
||||
It seems that your changes could not be saved. What to do next:
|
||||
</p>
|
||||
|
||||
<p
|
||||
style={{
|
||||
width: "90%",
|
||||
fontSize: "0.9rem"
|
||||
}}
|
||||
>
|
||||
<ol>
|
||||
<li>
|
||||
<p>
|
||||
Tap on "Dismiss" and copy the contents of your note so they
|
||||
are not lost.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Restart the app.</p>
|
||||
</li>
|
||||
</ol>
|
||||
</p>
|
||||
|
||||
<button
|
||||
style={{
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 5,
|
||||
boxSizing: "border-box",
|
||||
border: "none",
|
||||
color: colors.static.white,
|
||||
width: 250,
|
||||
fontSize: "1em",
|
||||
height: 45,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (globalThis.keyboardShown) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
const element = document.getElementById(
|
||||
"editor-saving-failed-overlay"
|
||||
);
|
||||
if (element) {
|
||||
element.style.display = "none";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
userSelect: "none"
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onScroll={controller.scroll}
|
||||
ref={containerRef}
|
||||
|
||||
@@ -30,15 +30,8 @@ import {
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import {
|
||||
EventTypes,
|
||||
getRoot,
|
||||
post,
|
||||
postAsyncWithTimeout,
|
||||
saveTheme
|
||||
} from "../utils";
|
||||
import { EventTypes, isReactNative, post, randId, saveTheme } from "../utils";
|
||||
import { injectCss, transform } from "../utils/css";
|
||||
import { pendingSaveRequests } from "../utils/pending-saves";
|
||||
import { useTabContext, useTabStore } from "./useTabStore";
|
||||
|
||||
type Attachment = {
|
||||
@@ -61,7 +54,6 @@ type Timers = {
|
||||
selectionChange: NodeJS.Timeout | null;
|
||||
change: NodeJS.Timeout | null;
|
||||
wordCounter: NodeJS.Timeout | null;
|
||||
scroll: NodeJS.Timeout | null;
|
||||
};
|
||||
|
||||
function isInViewport(element: any) {
|
||||
@@ -123,12 +115,10 @@ export type EditorController = {
|
||||
};
|
||||
export function useEditorController({
|
||||
update,
|
||||
getTableOfContents,
|
||||
scrollTo
|
||||
getTableOfContents
|
||||
}: {
|
||||
update: () => void;
|
||||
getTableOfContents: () => any[];
|
||||
scrollTo: (top: number) => void;
|
||||
}): EditorController {
|
||||
const passwordInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const tab = useTabContext();
|
||||
@@ -145,8 +135,7 @@ export function useEditorController({
|
||||
const timers = useRef<Timers>({
|
||||
selectionChange: null,
|
||||
change: null,
|
||||
wordCounter: null,
|
||||
scroll: null
|
||||
wordCounter: null
|
||||
});
|
||||
|
||||
if (!tabRef.current.noteId && loading) {
|
||||
@@ -155,49 +144,14 @@ export function useEditorController({
|
||||
|
||||
const selectionChange = useCallback((_editor: Editor) => {}, []);
|
||||
|
||||
const titleChange = useCallback(async (title: string) => {
|
||||
const currentSessionId = globalThis.sessionId;
|
||||
const titleChange = useCallback((title: string) => {
|
||||
post(
|
||||
EventTypes.contentchange,
|
||||
undefined,
|
||||
tabRef.current.id,
|
||||
tabRef.current.noteId
|
||||
);
|
||||
const params = [
|
||||
{
|
||||
title
|
||||
},
|
||||
tabRef.current.id,
|
||||
tabRef.current.noteId,
|
||||
currentSessionId
|
||||
];
|
||||
const pendingTitleIds = await pendingSaveRequests.getPendingTitleIds();
|
||||
postAsyncWithTimeout(EventTypes.title, ...params, 1000)
|
||||
.then(() => {
|
||||
if (pendingTitleIds.length) {
|
||||
dbLogger(
|
||||
"log",
|
||||
`Title saved: ${title}, removing ${pendingTitleIds.length} pending requests `
|
||||
);
|
||||
}
|
||||
pendingSaveRequests.removePendingTitlesById(pendingTitleIds);
|
||||
})
|
||||
.catch((e) => {
|
||||
dbLogger("error", e);
|
||||
dbLogger(
|
||||
"log",
|
||||
`Saving title failed, setting pending request ${pendingTitleIds.length}`
|
||||
);
|
||||
if (params[2]) {
|
||||
pendingSaveRequests.setTitle(params);
|
||||
}
|
||||
const element = document.getElementById("editor-saving-failed-overlay");
|
||||
if (element) {
|
||||
element.style.display = "flex";
|
||||
editors[tabRef.current.id]?.commands?.blur();
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
post(EventTypes.title, title, tabRef.current.id, tabRef.current.noteId);
|
||||
}, []);
|
||||
|
||||
const countWords = useCallback((ms = 300) => {
|
||||
@@ -229,10 +183,10 @@ export function useEditorController({
|
||||
if (typeof timers.current.change === "number") {
|
||||
clearTimeout(timers.current?.change);
|
||||
}
|
||||
timers.current.change = setTimeout(async () => {
|
||||
timers.current.change = setTimeout(() => {
|
||||
htmlContentRef.current = editor.getHTML();
|
||||
|
||||
const params = [
|
||||
post(
|
||||
EventTypes.content,
|
||||
{
|
||||
html: htmlContentRef.current,
|
||||
ignoreEdit: ignoreEdit
|
||||
@@ -240,40 +194,7 @@ export function useEditorController({
|
||||
tabRef.current.id,
|
||||
tabRef.current.noteId,
|
||||
currentSessionId
|
||||
];
|
||||
const pendingContentIds =
|
||||
await pendingSaveRequests.getPendingContentIds();
|
||||
postAsyncWithTimeout(EventTypes.content, ...params, 5000)
|
||||
.then(() => {
|
||||
if (pendingContentIds.length) {
|
||||
dbLogger(
|
||||
"log",
|
||||
`Content saved, removing ${pendingContentIds.length} pending requests`
|
||||
);
|
||||
}
|
||||
pendingSaveRequests.removePendingContentsById(pendingContentIds);
|
||||
})
|
||||
.catch((e) => {
|
||||
dbLogger("error", e);
|
||||
dbLogger(
|
||||
"log",
|
||||
`Saving content failed, setting pending request ${
|
||||
pendingContentIds.length + 1
|
||||
}`
|
||||
);
|
||||
if (params[2]) {
|
||||
pendingSaveRequests.setContent(params);
|
||||
}
|
||||
|
||||
const element = document.getElementById(
|
||||
"editor-saving-failed-overlay"
|
||||
);
|
||||
if (element) {
|
||||
element.style.display = "flex";
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
);
|
||||
logger(
|
||||
"info",
|
||||
"Editor saving content",
|
||||
@@ -289,18 +210,14 @@ export function useEditorController({
|
||||
|
||||
const scroll = useCallback(
|
||||
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||
const value = _event.currentTarget.scrollTop;
|
||||
if (timers.current.scroll !== null) clearTimeout(timers.current.scroll);
|
||||
timers.current.scroll = setTimeout(() => {
|
||||
if (
|
||||
tabRef.current.noteId &&
|
||||
tabRef.current.noteId === useTabStore.getState().getCurrentNoteId()
|
||||
) {
|
||||
useTabStore.getState().setNoteState(tabRef.current.noteId, {
|
||||
top: value
|
||||
});
|
||||
}
|
||||
}, 16);
|
||||
const noteId = useTabStore
|
||||
.getState()
|
||||
.getNoteIdForTab(useTabStore.getState().currentTab);
|
||||
if (noteId) {
|
||||
useTabStore.getState().setNoteState(noteId, {
|
||||
top: _event.currentTarget.scrollTop
|
||||
});
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -333,29 +250,19 @@ export function useEditorController({
|
||||
switch (type) {
|
||||
case "native:updatehtml": {
|
||||
htmlContentRef.current = value;
|
||||
logger("info", "UPDATING NOTE HTML");
|
||||
if (tabRef.current.id !== useTabStore.getState().currentTab) {
|
||||
updateTabOnFocus.current = true;
|
||||
} else {
|
||||
if (!editor) break;
|
||||
|
||||
const noteState = tabRef.current?.noteId
|
||||
? useTabStore.getState().noteState[tabRef.current?.noteId]
|
||||
: null;
|
||||
|
||||
const { from, to } = editor.state.selection;
|
||||
editor?.commands.setContent(htmlContentRef.current, false, {
|
||||
preserveWhitespace: true
|
||||
});
|
||||
|
||||
if (noteState) {
|
||||
editor.commands.setTextSelection({
|
||||
from: noteState.from,
|
||||
to: noteState.to
|
||||
});
|
||||
}
|
||||
|
||||
scrollTo?.(noteState?.top || 0);
|
||||
|
||||
editor.commands.setTextSelection({
|
||||
from,
|
||||
to
|
||||
});
|
||||
countWords(0);
|
||||
}
|
||||
|
||||
@@ -389,8 +296,9 @@ export function useEditorController({
|
||||
scrollIntoView(editor as any);
|
||||
}
|
||||
break;
|
||||
case "native:resolve":
|
||||
case "native:attachment-data":
|
||||
if (pendingResolvers[value.resolverId]) {
|
||||
logger("info", "resolved data for attachment", value.resolverId);
|
||||
pendingResolvers[value.resolverId](value.data);
|
||||
}
|
||||
break;
|
||||
@@ -403,9 +311,16 @@ export function useEditorController({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getRoot()?.addEventListener("message", onMessage);
|
||||
if (!isReactNative()) return; // Subscribe only in react native webview.
|
||||
const isSafari = navigator.vendor.match(/apple/i);
|
||||
let root: Document | Window = document;
|
||||
if (isSafari) {
|
||||
root = window;
|
||||
}
|
||||
root.addEventListener("message", onMessage);
|
||||
|
||||
return () => {
|
||||
getRoot()?.removeEventListener("message", onMessage);
|
||||
root.removeEventListener("message", onMessage);
|
||||
};
|
||||
}, [onMessage]);
|
||||
|
||||
@@ -439,8 +354,16 @@ export function useEditorController({
|
||||
};
|
||||
|
||||
const getAttachmentData = (attachment: Partial<Attachment>) => {
|
||||
return postAsyncWithTimeout(EventTypes.getAttachmentData, {
|
||||
attachment
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const resolverId = randId("get_attachment_data");
|
||||
pendingResolvers[resolverId] = (data) => {
|
||||
delete pendingResolvers[resolverId];
|
||||
resolve(data);
|
||||
};
|
||||
post(EventTypes.getAttachmentData, {
|
||||
attachment,
|
||||
resolverId: resolverId
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export type TabItem = {
|
||||
pinned?: boolean;
|
||||
};
|
||||
|
||||
export type NoteState = {
|
||||
type NoteState = {
|
||||
top: number;
|
||||
to: number;
|
||||
from: number;
|
||||
@@ -89,8 +89,6 @@ export const useTabStore = create(
|
||||
currentTab: 0,
|
||||
scrollPosition: {},
|
||||
setNoteState: (noteId: string, state: Partial<NoteState>) => {
|
||||
if (editorControllers[get().currentTab]?.loading) return;
|
||||
|
||||
const noteState = {
|
||||
...get().noteState
|
||||
};
|
||||
@@ -98,7 +96,6 @@ export const useTabStore = create(
|
||||
...get().noteState[noteId],
|
||||
...state
|
||||
};
|
||||
|
||||
set({
|
||||
noteState
|
||||
});
|
||||
|
||||
@@ -135,10 +135,7 @@ declare global {
|
||||
| undefined
|
||||
>;
|
||||
|
||||
var __DEV__: boolean;
|
||||
|
||||
function logger(type: "info" | "warn" | "error", ...logs: unknown[]): void;
|
||||
function dbLogger(type: "log" | "error", ...logs: unknown[]): void;
|
||||
/**
|
||||
* Function to post message to react native
|
||||
* @param type
|
||||
@@ -161,24 +158,6 @@ declare global {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getRoot() {
|
||||
if (!isReactNative()) return; // Subscribe only in react native webview.
|
||||
const isSafari = navigator.vendor.match(/apple/i);
|
||||
let root: Document | Window = document;
|
||||
if (isSafari) {
|
||||
root = window;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
export function getOnMessageListener(callback: () => void) {
|
||||
getRoot()?.addEventListener("onMessage", callback);
|
||||
return {
|
||||
remove: getRoot()?.removeEventListener("onMessage", callback)
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-enable no-var */
|
||||
|
||||
export const EventTypes = {
|
||||
@@ -213,8 +192,7 @@ export const EventTypes = {
|
||||
unlockWithBiometrics: "editor-events:unlock-biometrics",
|
||||
disableReadonlyMode: "editor-events:disable-readonly-mode",
|
||||
readonlyEditorLoaded: "readonlyEditorLoaded",
|
||||
error: "editorError",
|
||||
dbLogger: "editor-events:dbLogger"
|
||||
error: "editorError"
|
||||
} as const;
|
||||
|
||||
export function randId(prefix: string) {
|
||||
@@ -231,8 +209,6 @@ export function logger(
|
||||
type: "info" | "warn" | "error",
|
||||
...logs: unknown[]
|
||||
): void {
|
||||
if (typeof globalThis.__DEV__ !== "undefined" && !globalThis.__DEV__) return;
|
||||
|
||||
const logString = logs
|
||||
.map((log) => {
|
||||
return typeof log !== "string" ? JSON.stringify(log) : log;
|
||||
@@ -242,81 +218,29 @@ export function logger(
|
||||
post(EventTypes.logger, `[${type}]: ` + logString);
|
||||
}
|
||||
|
||||
export function dbLogger(type: "error" | "log", ...logs: unknown[]): void {
|
||||
const logString = logs
|
||||
.map((log) => {
|
||||
return typeof log !== "string" ? JSON.stringify(log) : log;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
post(EventTypes.dbLogger, {
|
||||
message: `[${type}]: ` + logString,
|
||||
error: logs[0] instanceof Error ? logs[0] : undefined
|
||||
});
|
||||
}
|
||||
|
||||
export function post(
|
||||
type: string,
|
||||
export function post<T extends keyof typeof EventTypes>(
|
||||
type: (typeof EventTypes)[T],
|
||||
value?: unknown,
|
||||
tabId?: number,
|
||||
noteId?: string,
|
||||
sessionId?: string,
|
||||
hasTimeout?: boolean
|
||||
): string {
|
||||
const id = randId(type);
|
||||
sessionId?: string
|
||||
): void {
|
||||
if (isReactNative()) {
|
||||
setTimeout(() =>
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({
|
||||
type,
|
||||
value: value,
|
||||
sessionId: sessionId || globalThis.sessionId,
|
||||
tabId,
|
||||
noteId,
|
||||
resolverId: id,
|
||||
hasTimeout: hasTimeout
|
||||
})
|
||||
)
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({
|
||||
type,
|
||||
value: value,
|
||||
sessionId: sessionId || globalThis.sessionId,
|
||||
tabId,
|
||||
noteId
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// console.log(type, value);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export async function postAsyncWithTimeout<R = any>(
|
||||
type: string,
|
||||
value?: unknown,
|
||||
tabId?: number,
|
||||
noteId?: string,
|
||||
sessionId?: string,
|
||||
waitFor?: number
|
||||
): Promise<R> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = post(
|
||||
type,
|
||||
value,
|
||||
tabId,
|
||||
noteId,
|
||||
sessionId,
|
||||
waitFor !== undefined ? true : false
|
||||
);
|
||||
globalThis.pendingResolvers[id] = (result) => {
|
||||
delete globalThis.pendingResolvers[id];
|
||||
logger("info", `Async post request resolved for ${id}`);
|
||||
resolve(result);
|
||||
};
|
||||
if (waitFor !== undefined) {
|
||||
setTimeout(() => {
|
||||
if (globalThis.pendingResolvers[id]) {
|
||||
delete globalThis.pendingResolvers[id];
|
||||
reject(new Error(`Async post request timed out for ${id}`));
|
||||
}
|
||||
}, waitFor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
globalThis.logger = logger;
|
||||
globalThis.dbLogger = dbLogger;
|
||||
globalThis.post = post;
|
||||
|
||||
export function saveTheme(theme: ThemeDefinition) {
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { EventTypes, postAsyncWithTimeout, randId } from ".";
|
||||
|
||||
class PendingSaveRequests {
|
||||
static TITLES = "pendingTitles";
|
||||
static CONTENT = "pendingContents";
|
||||
|
||||
async setTitle(value: any) {
|
||||
const pendingTitles = JSON.parse(
|
||||
this.get(PendingSaveRequests.TITLES) || "[]"
|
||||
);
|
||||
|
||||
(pendingTitles as any[]).push({
|
||||
id: randId("title-pending"),
|
||||
params: value
|
||||
});
|
||||
return localStorage.setItem(
|
||||
PendingSaveRequests.TITLES,
|
||||
JSON.stringify(pendingTitles)
|
||||
);
|
||||
}
|
||||
|
||||
async getPendingTitles() {
|
||||
const pendingTitles = JSON.parse(
|
||||
this.get(PendingSaveRequests.TITLES) || "[]"
|
||||
);
|
||||
return pendingTitles;
|
||||
}
|
||||
|
||||
async setContent(value: any) {
|
||||
const pendingContents = JSON.parse(
|
||||
this.get(PendingSaveRequests.CONTENT) || "[]"
|
||||
);
|
||||
|
||||
(pendingContents as any[]).push({
|
||||
id: randId("content-pending"),
|
||||
params: value
|
||||
});
|
||||
return localStorage.setItem(
|
||||
PendingSaveRequests.CONTENT,
|
||||
JSON.stringify(pendingContents)
|
||||
);
|
||||
}
|
||||
|
||||
async getPendingContent() {
|
||||
const pendingContents = this.get(PendingSaveRequests.CONTENT);
|
||||
return JSON.parse(pendingContents || "[]");
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
remove(key: string) {
|
||||
return localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
return localStorage.clear();
|
||||
}
|
||||
|
||||
keys() {
|
||||
return localStorage.keys();
|
||||
}
|
||||
|
||||
async getPendingTitleIds() {
|
||||
const pendingTitles = await this.getPendingTitles();
|
||||
return (pendingTitles as any[]).map((pending) => pending.id);
|
||||
}
|
||||
|
||||
async getPendingContentIds() {
|
||||
const pendingContents = await this.getPendingContent();
|
||||
return (pendingContents as any[]).map((pending) => pending.id);
|
||||
}
|
||||
|
||||
async removePendingTitlesById(ids: string[]) {
|
||||
const pendingTitles = await this.getPendingTitles();
|
||||
const filtered = (pendingTitles as any[]).filter(
|
||||
(pending) => !ids.includes(pending.id)
|
||||
);
|
||||
return localStorage.setItem(
|
||||
PendingSaveRequests.TITLES,
|
||||
JSON.stringify(filtered)
|
||||
);
|
||||
}
|
||||
|
||||
async removePendingContentsById(ids: string[]) {
|
||||
const pendingContents = await this.getPendingContent();
|
||||
const filtered = (pendingContents as any[]).filter(
|
||||
(pending) => !ids.includes(pending.id)
|
||||
);
|
||||
return localStorage.setItem(
|
||||
PendingSaveRequests.CONTENT,
|
||||
JSON.stringify(filtered)
|
||||
);
|
||||
}
|
||||
|
||||
async postPendingRequests() {
|
||||
const postPendingTitles = async () => {
|
||||
const pendingTitles = await this.getPendingTitles();
|
||||
this.remove(PendingSaveRequests.TITLES);
|
||||
for (const pending of pendingTitles) {
|
||||
if (pending.params[0]) pending.params[0].pendingChanges = true;
|
||||
await postAsyncWithTimeout(EventTypes.title, ...pending.params, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const postPendingContent = async () => {
|
||||
const pendingContents = await this.getPendingContent();
|
||||
this.remove(PendingSaveRequests.CONTENT);
|
||||
for (const pending of pendingContents) {
|
||||
if (pending.params[0]) pending.params[0].pendingChanges = true;
|
||||
await postAsyncWithTimeout(EventTypes.content, ...pending.params, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
await postPendingTitles();
|
||||
await postPendingContent();
|
||||
}
|
||||
}
|
||||
|
||||
export const pendingSaveRequests = new PendingSaveRequests();
|
||||
@@ -1,24 +0,0 @@
|
||||
diff --git a/node_modules/prosemirror-model/dist/index.cjs b/node_modules/prosemirror-model/dist/index.cjs
|
||||
index 1c45f1c..c92ec8d 100644
|
||||
--- a/node_modules/prosemirror-model/dist/index.cjs
|
||||
+++ b/node_modules/prosemirror-model/dist/index.cjs
|
||||
@@ -98,6 +98,7 @@ var Fragment = function () {
|
||||
var nodeStart = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
|
||||
var parent = arguments.length > 4 ? arguments[4] : undefined;
|
||||
for (var i = 0, pos = 0; pos < to; i++) {
|
||||
+ if (i >= this.content.length) break;
|
||||
var child = this.content[i],
|
||||
end = pos + child.nodeSize;
|
||||
if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
|
||||
diff --git a/node_modules/prosemirror-model/dist/index.js b/node_modules/prosemirror-model/dist/index.js
|
||||
index 9c37a40..a215c60 100644
|
||||
--- a/node_modules/prosemirror-model/dist/index.js
|
||||
+++ b/node_modules/prosemirror-model/dist/index.js
|
||||
@@ -84,6 +84,7 @@ class Fragment {
|
||||
*/
|
||||
nodesBetween(from, to, f, nodeStart = 0, parent) {
|
||||
for (let i = 0, pos = 0; pos < to; i++) {
|
||||
+ if (i >= this.content.length) break;
|
||||
let child = this.content[i], end = pos + child.nodeSize;
|
||||
if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
|
||||
let start = pos + 1;
|
||||
@@ -70,13 +70,7 @@ export const useEditor = (
|
||||
EditorState.create({
|
||||
doc,
|
||||
plugins: editor.extensionManager.plugins,
|
||||
selection:
|
||||
selection.from > 0 &&
|
||||
selection.from <= doc.content.size &&
|
||||
selection.to > 0 &&
|
||||
selection.to <= doc.content.size
|
||||
? selection
|
||||
: undefined
|
||||
selection: selection || undefined
|
||||
})
|
||||
);
|
||||
if (oldIsFocused && !editor.isFocused) editor.commands.focus();
|
||||
|
||||
Reference in New Issue
Block a user